packages feed

settei-0.1.0.0: test/Settei/ConfigTest.hs

module Settei.ConfigTest (tests) where

import Control.Selective (select)
import Data.Bifunctor (first)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Settei
import Settei.Internal.Config (Request (..), runConfig)
import Settei.Prelude
import Settei.Prototype.Free (freeNecessaryKeys, freePossibleKeys)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "Settei.Config"
    [ testCase "applicative settings are necessary" $ do
        let schema = describe ((,) <$> required environmentSetting <*> required passwordSetting)
        keySet (schemaNecessary schema)
          @?= Set.fromList [environmentKey, passwordKey],
      testCase "selective production secret is conditional" $ do
        let schema = describe productionOnly
        keySet (schemaPossible schema)
          @?= Set.fromList [environmentKey, passwordKey]
        keySet (schemaNecessary schema)
          @?= Set.singleton environmentKey
        case schemaConditions schema of
          [condition] -> do
            conditionDependencies condition @?= Set.singleton environmentKey
            conditionSettings condition @?= Set.singleton passwordKey
          conditions -> fail ("expected one condition, found " <> show (length conditions)),
      testCase "free-selective prototype agrees on possible effects" $
        Set.fromList (freePossibleKeys environmentKey passwordKey)
          @?= Set.fromList [environmentKey, passwordKey],
      testCase "free-selective prototype agrees on necessary effects" $
        Set.fromList (freeNecessaryKeys environmentKey passwordKey)
          @?= Set.singleton environmentKey,
      testCase "development skips production secret" $
        runConfig (interpret developmentValues) productionOnly
          @?= Right Nothing,
      testCase "production requests its secret" $
        runConfig (interpret productionValues) productionOnly
          @?= Left (Missing passwordKey)
    ]

data RuntimeError
  = Missing !Key
  | Decode !DecodeFailure
  deriving stock (Generic, Eq, Show)

productionOnly :: Config (Maybe Text)
productionOnly = select selector productionBranch
  where
    selector =
      (\environment -> if environment == "production" then Left () else Right Nothing)
        <$> required environmentSetting
    productionBranch = (\password _ -> Just password) <$> required passwordSetting

interpret :: Map Key RawValue -> Request a -> Either RuntimeError a
interpret values = \case
  RequiredRequest settingSpec ->
    case Map.lookup (settingKey settingSpec) values of
      Nothing -> Left (Missing (settingKey settingSpec))
      Just rawValue -> first Decode (decodeSetting settingSpec rawValue)
  OptionalRequest settingSpec ->
    case Map.lookup (settingKey settingSpec) values of
      Nothing -> Right Nothing
      Just rawValue -> Just <$> first Decode (decodeSetting settingSpec rawValue)

developmentValues :: Map Key RawValue
developmentValues = Map.singleton environmentKey (RawText "development")

productionValues :: Map Key RawValue
productionValues = Map.singleton environmentKey (RawText "production")

environmentSetting :: Setting Text
environmentSetting =
  publicSetting environmentKey "Runtime environment" textDecoder

passwordSetting :: Setting Text
passwordSetting =
  secretSetting passwordKey "Database password" textDecoder

environmentKey :: Key
environmentKey = validKey "runtime.environment"

passwordKey :: Key
passwordKey = validKey "database.password"

validKey :: Text -> Key
validKey value = either (error . show) id (parseKey value)

keySet :: [SchemaSetting] -> Set Key
keySet = Set.fromList . fmap schemaSettingKey