packages feed

settei-0.3.0.0: test/Settei/ContractTest.hs

module Settei.ContractTest (tests) where

import Data.Generics.Labels ()
import Data.Text qualified as Text
import Data.Text.IO qualified as TextIO
import Settei
import Settei.Prelude
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "Settei.Contract"
    [ testCase "renders the golden contract text" $ do
        expected <- TextIO.readFile "test/golden/contract.txt"
        renderContractText comprehensive @?= expected,
      testCase "renders the golden contract JSON" $ do
        expected <- TextIO.readFile "test/golden/contract.json"
        renderContractJson comprehensive <> "\n" @?= expected,
      testCase "joins bindings in supplied order and keeps unbound keys" $ do
        let contract = comprehensive
        fmap inputBindingName (entry "required" contract ^. #inputs) @?= ["FIRST", "SECOND"]
        entry "optional" contract ^. #inputs @?= []
        assertBool "missing orphan binding diagnostic" (UndeclaredBindingKey (inputBinding EnvironmentVariable "ORPHAN" (validKey "nothing.here")) `elem` contractDiagnostics contract),
      testCase "reports all default shapes without evaluating computations or case values" $ do
        let contract = comprehensive
        entry "constant" contract ^. #declaredDefaults @?= [DeclaredDefault (RuleName "constant-rule") "constant" (ConstantShape (VisibleDefault "42"))]
        shapes "opaque" contract @?= [ConstantShape UnrenderableDefault]
        shapes "secret" contract @?= [ConstantShape RedactedDefault]
        shapes "derived" contract @?= [DerivedShape [validKey "required"]]
        shapes "case" contract @?= [CaseShape [validKey "required"] 1 False]
        shapes "caseFallback" contract @?= [CaseShape [validKey "required"] 1 True]
        assertBool "deferred computation omitted" ("value not computed" `Text.isInfixOf` renderContractText contract),
      testCase "source candidates retain validity, winning origin and ordered shadows" $ do
        let contract = comprehensive
        fmap
          (\value -> (value ^. #origin . #name, fmap (\origin -> origin ^. #name) (value ^. #shadowed), value ^. #validity))
          (entry "source" contract ^. #sourceDefault)
          @?= Just ("high", ["middle", "low"], ValidDefault)
        verdict "source" contract @?= InputDefaulted CoveredBySource
        verdict "invalid" contract @?= InputRequired InvalidSourceDefault
        assertBool "missing invalid validity" ("\"validity\":\"invalid\"" `Text.isInfixOf` renderContractJson contract),
      testCase "diagnostics contain no candidate values and retain all inconsistency kinds" $ do
        let output = renderContractJson comprehensive
        mapM_
          (\kind -> assertBool (Text.unpack kind) (kind `Text.isInfixOf` output))
          ["conflicting-sensitivity", "default-structural-conflict", "undeclared-default-leaf", "undeclared-binding-key", "undeclared-profile-key"]
        assertBool "conflict did not become secret" (entry "mixed" comprehensive ^. #sensitivity == Secret),
      testCase "secret sentinel is absent from Show, text, JSON and diagnostics" $
        mapM_
          (\output -> assertBool "secret leaked" (not (sentinel `Text.isInfixOf` output)))
          [Text.pack (show comprehensive), renderContractText comprehensive, renderContractJson comprehensive, Text.pack (show (contractDiagnostics comprehensive))],
      testCase "identical inputs produce byte-identical outputs" $ do
        renderContractText comprehensive @?= renderContractText (build comprehensiveInputs comprehensiveConfig)
        renderContractJson comprehensive @?= renderContractJson (build comprehensiveInputs comprehensiveConfig),
      testCase "profile requires explicit input while retaining default metadata" $ do
        verdict "profiled" comprehensive @?= InputRequired ProfileRequirement
        assertBool "profile default lost" (not (null (entry "profiled" comprehensive ^. #declaredDefaults))),
      testCase "required occurrence is not satisfied by another occurrence's default" $ do
        let declaration = (,) <$> required x <*> withDefault x (constantDefault (RuleName "x-default") "fallback" "fallback")
            contract = build emptyContractInputs declaration
        verdict "x" contract @?= InputRequired RequiredRequest
        assertBool "required request unexpectedly resolved" (isFailure (resolve defaultResolveOptions [] declaration)),
      testCase "optional plus conditional required keeps occurrence-based verdict and merged schema" $ do
        let declaration = (,) <$> optional x <*> conditional x
            contract = build emptyContractInputs declaration
        entry "x" contract ^. #requirement @?= Required
        entry "x" contract ^. #presence @?= Necessary
        assertVerdict "x" (InputConditional NoDefault) contract
        assertBool "unselected branch failed" (not (isFailure (resolve defaultResolveOptions [environment "development"] declaration)))
        assertBool "selected branch succeeded" (isFailure (resolve defaultResolveOptions [environment "production"] declaration)),
      testCase "declaration default plus conditional required stays conditional" $ do
        let declaration = (,) <$> withDefault x (constantDefault (RuleName "x-default") "fallback" "fallback") <*> conditional x
            contract = build emptyContractInputs declaration
        assertVerdict "x" (InputConditional RequiredRequest) contract
        assertBool "development failed" (not (isFailure (resolve defaultResolveOptions [environment "development"] declaration)))
        assertBool "production succeeded" (isFailure (resolve defaultResolveOptions [environment "production"] declaration)),
      testCase "conditional-only source rejection remains conditional and branch-local" $ do
        let declaration = conditional integerX
            sources = [pairs "bad" [("x", RawText "bad")]]
            contract = build (ContractInputs [] sources Nothing) declaration
        assertVerdict "x" (InputConditional InvalidSourceDefault) contract
        assertBool "unselected invalid branch failed" (not (isFailure (resolve defaultResolveOptions (environment "development" : sources) declaration)))
        assertBool "selected invalid branch succeeded" (isFailure (resolve defaultResolveOptions (environment "production" : sources) declaration)),
      testCase "necessary acceptance plus conditional rejection remains conditional" $ do
        let declaration = (,) <$> required x <*> conditional integerX
            sources = [pairs "bad" [("x", RawText "bad")]]
            contract = build (ContractInputs [] sources Nothing) declaration
        assertVerdict "x" (InputConditional InvalidSourceDefault) contract
        assertBool "necessary accepting request failed" (not (isFailure (resolve defaultResolveOptions (environment "development" : sources) declaration)))
        assertBool "conditional rejecting request succeeded" (isFailure (resolve defaultResolveOptions (environment "production" : sources) declaration)),
      testCase "later necessary rejection outranks earlier conditional rejection" $ do
        let declaration = (,) <$> conditional integerX <*> required integerX
            contract = build (ContractInputs [] [pairs "bad" [("x", RawText "bad")]] Nothing) declaration
        assertVerdict "x" (InputRequired InvalidSourceDefault) contract,
      testCase "secret constants never force their renderer or value" $ do
        let settingSpec = withRenderer (\_ -> error "secret renderer invoked") (secretSetting (validKey "poison") "secret" textDecoder)
            contract = build emptyContractInputs (withDefault settingSpec (constantDefault (RuleName "poison") "secret" (error "secret value forced")))
        assertBool "secret default unavailable" ("redacted" `Text.isInfixOf` renderContractJson contract)
        assertBool "secret default unavailable in Show" ("RedactedDefault" `Text.isInfixOf` Text.pack (show contract)),
      testCase "cycle preflight avoids all source hooks" $ do
        let cyclic = withDefault x (derivedDefault (RuleName "cycle") "recursive" cyclic id)
            poison = locateSource (\_ -> error "source hook evaluated") (pairs "poison" [("x", RawText sentinel)])
        case deploymentContract (ContractInputs [] [poison] Nothing) cyclic of
          Left (DefaultCycle _ :| []) -> pure ()
          _ -> assertFailure "expected a default cycle"
    ]

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

publicText :: Text -> Setting Text
publicText name = publicShowSetting (validKey name) name textDecoder

publicInt :: Text -> Setting Int
publicInt name = publicShowSetting (validKey name) name boundedIntegralDecoder

x :: Setting Text
x = publicText "x"

integerX :: Setting Int
integerX = publicInt "x"

conditional :: Setting a -> Config (Maybe a)
conditional settingSpec = whenEq (required (publicText "environment")) "production" (required settingSpec)

environment :: Text -> Source
environment value = pairs "environment" [("environment", RawText value)]

pairs :: Text -> [(Text, RawValue)] -> Source
pairs name values = either (error . show) id (sourceFromPairs name BuiltInSource [(validKey target, value) | (target, value) <- values])

build :: ContractInputs -> Config a -> DeploymentContract
build supplied = either (error . show) id . deploymentContract supplied

entry :: Text -> DeploymentContract -> ContractSetting
entry target contract = case filter ((== validKey target) . view #key) (contractSettings contract) of
  value : _ -> value
  [] -> error ("missing contract key: " <> Text.unpack target)

verdict :: Text -> DeploymentContract -> ExternalInput
verdict target contract = entry target contract ^. #externalInput

shapes :: Text -> DeploymentContract -> [DefaultShape]
shapes target contract = fmap (view #shape) (entry target contract ^. #declaredDefaults)

isFailure :: ResolveResult a -> Bool
isFailure result = case result ^. #answer of Left _ -> True; Right _ -> False

assertVerdict :: Text -> ExternalInput -> DeploymentContract -> IO ()
assertVerdict target expected contract = do
  verdict target contract @?= expected
  let verdictTag = case expected of InputRequired _ -> "required"; InputConditional _ -> "conditional"; InputDefaulted _ -> "defaulted"; InputOptional -> "optional"
  assertBool "text verdict absent" (("input: " <> verdictTag) `Text.isInfixOf` renderContractText contract)
  assertBool "JSON verdict absent" (("\"verdict\":\"" <> verdictTag <> "\"") `Text.isInfixOf` renderContractJson contract)

sentinel :: Text
sentinel = "SECRET-CONTRACT-SENTINEL-雪"

comprehensive :: DeploymentContract
comprehensive = build comprehensiveInputs comprehensiveConfig

comprehensiveInputs :: ContractInputs
comprehensiveInputs =
  ContractInputs
    [inputBinding EnvironmentVariable "FIRST" (validKey "required"), inputBinding (CustomInput "mounted") "SECOND" (validKey "required"), inputBinding EnvironmentVariable "ORPHAN" (validKey "nothing.here")]
    [ pairs "low" [("source", RawText "low"), ("secretSource", RawText sentinel), ("invalid", RawText "bad"), ("unknown", RawText sentinel), ("blocked", RawText sentinel)],
      pairs "middle" [("source", RawText "middle")],
      pairs "high" [("source", RawText "high")]
    ]
    (Just (deploymentProfile "production" [(validKey "profiled", "state explicitly"), (validKey "unknownProfile", "absent")]))

comprehensiveConfig :: Config ()
comprehensiveConfig =
  sequenceA_
    [ () <$ required (publicText "required"),
      () <$ required (secretSetting (validKey "requiredSecret") "secret" textDecoder),
      () <$ optional (publicText "optional"),
      () <$ withDefault (publicInt "constant") (constantDefault (RuleName "constant-rule") "constant" 42),
      () <$ withDefault (publicSetting (validKey "opaque") "opaque" textDecoder) (constantDefault (RuleName "opaque-rule") "opaque" "opaque"),
      () <$ withDefault (secretSetting (validKey "secret") "secret" textDecoder) (constantDefault (RuleName "secret-rule") "secret" sentinel),
      () <$ withDefault (publicText "derived") (derivedDefault (RuleName "derived-rule") "derived" (required (publicText "required")) (\_ -> error "derived evaluated")),
      () <$ withDefault (publicText "case") (caseDefault (RuleName "case-rule") "case" (required (publicText "required")) (("development", error "case evaluated") :| []) Nothing),
      () <$ withDefault (publicText "caseFallback") (caseDefault (RuleName "fallback-rule") "fallback" (required (publicText "required")) (("development", error "case evaluated") :| []) (Just (error "fallback evaluated"))),
      () <$ conditional (publicText "conditional"),
      () <$ required (publicText "requiredDefault"),
      () <$ withDefault (publicText "requiredDefault") (constantDefault (RuleName "competing-default") "fallback on another request" "fallback"),
      () <$ required (publicText "source"),
      () <$ required (publicInt "invalid"),
      () <$ required (secretSetting (validKey "secretSource") "secret source" textDecoder),
      () <$ required (publicText "blocked.child"),
      () <$ withDefault (publicText "mixed") (constantDefault (RuleName "mixed-rule") "mixed" sentinel),
      () <$ optional (secretSetting (validKey "mixed") "mixed" textDecoder),
      () <$ withDefault (publicText "profiled") (constantDefault (RuleName "profile-rule") "profile" "default")
    ]
  where
    sequenceA_ = foldr (*>) (pure ())