packages feed

shomei-server-0.3.0.0: test/Shomei/Server/ConfigSpec.hs

-- | Tests for EP-5's Dhall + environment configuration loader. Writes a small Dhall file to a
-- temp path, loads it through 'loadConfig' (which renders it via @dhall-to-json@ and decodes the
-- result), asserts the parsed values win over the built-in defaults, and then proves an
-- environment variable overrides the file value (twelve-factor precedence). Using a temp file
-- avoids any dependency on the test's working directory.
module Main (main) where

import Control.Exception (try)
import Data.Aeson (Value (Object), eitherDecodeStrict', encode, toJSON)
import Data.Aeson.KeyMap qualified as KeyMap
import Data.ByteString.Lazy.Char8 qualified as LBS8
import Data.List (isInfixOf)
import Data.Set qualified as Set
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Shomei.Account.Password.Domain (PasswordPolicy (..))
import Shomei.Account.Password.Hash.Postgres (Argon2Params (..))
import Shomei.Authorization.Claims.Domain (Issuer (..))
import Shomei.Config (CookieConfig (..), MachineTokenConfig (..), MfaConfig (..), NotifierConfig (..), NotifierTransport (..), RateLimitConfig (..), ShomeiConfig (..), SigningKeyConfig (..), SmtpConfig (..), SmtpTlsMode (..), WebAuthnConfig (..), WebhookConfig (..))
import Shomei.Notify (NotifierSecrets (..), smtpPasswordText, webhookSecretBytes)
import Shomei.Server.Config (FileConfig, ServerSettings (..), SweepSettings (..), loadConfig, loadConfigFromEnv, loadCoreConfig, loadNotifierSecretsFromEnv)
import Shomei.Server.Keys (loadKekFromEnv)
import System.Directory (makeAbsolute)
import System.Environment (setEnv, unsetEnv)
import System.IO.Error (isUserError)
import System.Process (readProcess)
import Test.Tasty (TestTree, defaultMain)
import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, testCase, (@?=))

configPath :: FilePath
configPath = "/tmp/shomei-config-test.dhall"

schemaProbePath :: FilePath
schemaProbePath = "/tmp/shomei-schema-probe.dhall"

-- A partial config (FileConfig's fields are all optional, so absent keys fall back to defaults).
dhallContents :: String
dhallContents =
  "{ issuer = \"shomei-prod\""
    <> ", databaseUrl = \"host=fromfile dbname=shomei\""
    <> ", port = 8080"
    <> ", bindHost = \"127.0.0.1\""
    <> ", maxConnections = 32"
    <> ", slowClientTimeoutSeconds = 12"
    <> ", dbPoolSize = 25"
    <> ", dbPoolAcquisitionTimeoutMs = 2500"
    <> ", dbStatementTimeoutMs = 5000"
    <> ", notifierQueueSize = 12"
    <> ", maxFailedLoginsPerAccount = 7"
    <> ", metricsEnabled = False"
    <> ", keyRefreshIntervalSeconds = 15"
    <> ", allowedClockSkewSeconds = 20"
    <> ", passwordMinLength = 16"
    <> ", passwordRejectCommon = False"
    <> ", webauthnRpId = \"auth.fromfile.test\""
    <> ", webauthnOrigins = [ \"https://auth.fromfile.test\" ]"
    <> ", webauthnUserVerification = \"required\""
    <> ", mfaRequireSecondFactor = False"
    <> ", machineTokenTtlSeconds = 120"
    <> " }"

main :: IO ()
main = do
  writeFile configPath dhallContents
  defaultMain testLoadAndOverride

-- | With neither Dhall file nor pool env vars, the pool knobs reproduce the values that were
-- hardcoded before they became configuration.
poolDefaults :: Assertion
poolDefaults = do
  unsetEnv "SHOMEI_CONFIG"
  unsetEnv "SHOMEI_DB_POOL_SIZE"
  unsetEnv "SHOMEI_DB_POOL_ACQUISITION_TIMEOUT_MS"
  unsetEnv "SHOMEI_DB_STATEMENT_TIMEOUT_MS"
  unsetEnv "SHOMEI_NOTIFIER_QUEUE_SIZE"
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  (_, settings) <- loadConfigFromEnv
  settings.serverBindHost @?= "*4"
  settings.serverMaxConnections @?= 1024
  settings.serverSlowClientTimeoutSeconds @?= 30
  settings.serverDbPoolSize @?= 10
  settings.serverDbPoolAcquisitionTimeoutMs @?= 10000
  settings.serverDbStatementTimeoutMs @?= 30000
  settings.serverDbTransactionMaxAttempts @?= 3
  settings.serverDbTransactionDeadlineMs @?= 5000
  settings.serverNotifierQueueSize @?= 1024
  unsetEnv "PG_CONNECTION_STRING"

-- | A zero-size pool deadlocks every request and a zero acquisition timeout fails every
-- checkout, so the loader refuses to start rather than booting a server that cannot serve.
poolRejectsNonPositive :: Assertion
poolRejectsNonPositive = do
  unsetEnv "SHOMEI_CONFIG"
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_DB_POOL_SIZE" "0"
  sizeResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_DB_POOL_SIZE" sizeResult
  unsetEnv "SHOMEI_DB_POOL_SIZE"
  setEnv "SHOMEI_DB_POOL_ACQUISITION_TIMEOUT_MS" "-1"
  timeoutResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_DB_POOL_ACQUISITION_TIMEOUT_MS" timeoutResult
  unsetEnv "SHOMEI_DB_POOL_ACQUISITION_TIMEOUT_MS"
  setEnv "SHOMEI_DB_STATEMENT_TIMEOUT_MS" "-1"
  statementTimeoutResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_DB_STATEMENT_TIMEOUT_MS" statementTimeoutResult
  unsetEnv "SHOMEI_DB_STATEMENT_TIMEOUT_MS"
  setEnv "SHOMEI_NOTIFIER_QUEUE_SIZE" "0"
  notifierQueueResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_NOTIFIER_QUEUE_SIZE" notifierQueueResult
  unsetEnv "SHOMEI_NOTIFIER_QUEUE_SIZE"
  mapM_
    ( \key -> do
        setEnv key "0"
        invalid <- try loadConfigFromEnv
        expectUserErrorNaming key invalid
        unsetEnv key
    )
    ["SHOMEI_DB_TRANSACTION_MAX_ATTEMPTS", "SHOMEI_DB_TRANSACTION_DEADLINE_MS", "SHOMEI_MAX_CONNECTIONS", "SHOMEI_SLOW_CLIENT_TIMEOUT_SECONDS"]
  setEnv "SHOMEI_DB_TRANSACTION_MAX_ATTEMPTS" "2"
  setEnv "SHOMEI_DB_TRANSACTION_DEADLINE_MS" "250"
  (_, bounded) <- loadConfigFromEnv
  bounded.serverDbTransactionMaxAttempts @?= 2
  bounded.serverDbTransactionDeadlineMs @?= 250
  unsetEnv "SHOMEI_DB_TRANSACTION_MAX_ATTEMPTS"
  unsetEnv "SHOMEI_DB_TRANSACTION_DEADLINE_MS"
  unsetEnv "PG_CONNECTION_STRING"

sweepEnvVars :: [String]
sweepEnvVars =
  [ "SHOMEI_SWEEP_ENABLED",
    "SHOMEI_SWEEP_INTERVAL_SECONDS",
    "SHOMEI_SWEEP_BATCH_SIZE",
    "SHOMEI_SWEEP_DEAD_SESSION_GRACE_DAYS",
    "SHOMEI_SWEEP_ONE_TIME_TOKEN_GRACE_DAYS",
    "SHOMEI_SWEEP_CEREMONY_GRACE_MINUTES",
    "SHOMEI_LOGIN_ATTEMPT_RETENTION_DAYS",
    "SHOMEI_AUTH_EVENT_RETENTION_DAYS"
  ]

-- | With no file and no env, the sweeper is on, hourly, and retains the audit trail forever.
sweepDefaults :: Assertion
sweepDefaults = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv sweepEnvVars
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  (_, settings) <- loadConfigFromEnv
  let sweep = settings.serverSweep
  sweep.sweepEnabled @?= True
  sweep.sweepIntervalSeconds @?= 3600
  sweep.sweepBatchSize @?= 1000
  sweep.sweepDeadSessionGraceDays @?= 30
  sweep.sweepOneTimeTokenGraceDays @?= 7
  sweep.sweepCeremonyGraceMinutes @?= 60
  sweep.sweepLoginAttemptRetentionDays @?= 90
  -- The one conservative default: never delete the compliance record on your own initiative.
  sweep.sweepAuthEventRetentionDays @?= Nothing
  unsetEnv "PG_CONNECTION_STRING"

-- | Every sweep knob is settable from the environment, and a non-positive audit-retention
-- window means "retain forever" rather than "delete everything" — the failure mode of getting
-- that backwards is unrecoverable.
sweepEnvOverrides :: Assertion
sweepEnvOverrides = do
  unsetEnv "SHOMEI_CONFIG"
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_SWEEP_ENABLED" "false"
  setEnv "SHOMEI_SWEEP_INTERVAL_SECONDS" "900"
  setEnv "SHOMEI_SWEEP_BATCH_SIZE" "250"
  setEnv "SHOMEI_SWEEP_DEAD_SESSION_GRACE_DAYS" "0"
  setEnv "SHOMEI_LOGIN_ATTEMPT_RETENTION_DAYS" "30"
  setEnv "SHOMEI_AUTH_EVENT_RETENTION_DAYS" "365"
  (_, settings) <- loadConfigFromEnv
  let sweep = settings.serverSweep
  sweep.sweepEnabled @?= False
  sweep.sweepIntervalSeconds @?= 900
  sweep.sweepBatchSize @?= 250
  -- A zero grace period is legal: sweep a session the moment it expires.
  sweep.sweepDeadSessionGraceDays @?= 0
  sweep.sweepLoginAttemptRetentionDays @?= 30
  sweep.sweepAuthEventRetentionDays @?= Just 365

  -- Zero (or negative) turns audit deletion back off.
  setEnv "SHOMEI_AUTH_EVENT_RETENTION_DAYS" "0"
  (_, offSettings) <- loadConfigFromEnv
  offSettings.serverSweep.sweepAuthEventRetentionDays @?= Nothing

  mapM_ unsetEnv sweepEnvVars
  unsetEnv "PG_CONNECTION_STRING"

-- | A zero interval would spin the sweeper thread and a zero batch size would make it delete
-- nothing forever, so the loader refuses both. @sweepEnabled = false@ is the off-switch.
sweepRejectsNonPositive :: Assertion
sweepRejectsNonPositive = do
  unsetEnv "SHOMEI_CONFIG"
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_SWEEP_INTERVAL_SECONDS" "0"
  intervalResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_SWEEP_INTERVAL_SECONDS" intervalResult
  unsetEnv "SHOMEI_SWEEP_INTERVAL_SECONDS"

  setEnv "SHOMEI_SWEEP_BATCH_SIZE" "0"
  batchResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_SWEEP_BATCH_SIZE" batchResult
  unsetEnv "SHOMEI_SWEEP_BATCH_SIZE"

  -- A negative grace period would sweep rows that have not expired yet.
  setEnv "SHOMEI_SWEEP_CEREMONY_GRACE_MINUTES" "-1"
  graceResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_SWEEP_CEREMONY_GRACE_MINUTES" graceResult
  unsetEnv "SHOMEI_SWEEP_CEREMONY_GRACE_MINUTES"
  unsetEnv "PG_CONNECTION_STRING"

-- | Argon2 defaults, env overrides, and the refusal of non-positive costs.
argon2Settings :: Assertion
argon2Settings = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv argon2EnvVars
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"

  (_, defaults) <- loadConfigFromEnv
  defaults.serverArgon2.memoryKiB @?= 65536
  defaults.serverArgon2.iterations @?= 3
  defaults.serverArgon2.parallelism @?= 1
  defaults.serverHashingMaxConcurrency @?= 2

  setEnv "SHOMEI_ARGON2_MEMORY_KIB" "32768"
  setEnv "SHOMEI_ARGON2_ITERATIONS" "4"
  setEnv "SHOMEI_HASHING_MAX_CONCURRENCY" "4"
  (_, overridden) <- loadConfigFromEnv
  overridden.serverArgon2.memoryKiB @?= 32768
  overridden.serverArgon2.iterations @?= 4
  overridden.serverArgon2.parallelism @?= 1
  overridden.serverHashingMaxConcurrency @?= 4
  mapM_ unsetEnv argon2EnvVars

  setEnv "SHOMEI_ARGON2_MEMORY_KIB" "64"
  setEnv "SHOMEI_ARGON2_PARALLELISM" "16"
  hardFloorResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_ARGON2_MEMORY_KIB" hardFloorResult

  setEnv "SHOMEI_ARGON2_MEMORY_KIB" "128"
  (_, boundary) <- loadConfigFromEnv
  boundary.serverArgon2 @?= Argon2Params 128 3 16
  mapM_ unsetEnv argon2EnvVars

  -- crypton would reject these deep inside a login; the loader names the variable instead.
  -- A zero permit count would block every login forever.
  setEnv "SHOMEI_ARGON2_MEMORY_KIB" "0"
  memResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_ARGON2_MEMORY_KIB" memResult
  unsetEnv "SHOMEI_ARGON2_MEMORY_KIB"

  setEnv "SHOMEI_HASHING_MAX_CONCURRENCY" "0"
  concResult <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_HASHING_MAX_CONCURRENCY" concResult
  mapM_ unsetEnv argon2EnvVars
  unsetEnv "PG_CONNECTION_STRING"

argon2EnvVars :: [String]
argon2EnvVars =
  [ "SHOMEI_ARGON2_MEMORY_KIB",
    "SHOMEI_ARGON2_ITERATIONS",
    "SHOMEI_ARGON2_PARALLELISM",
    "SHOMEI_HASHING_MAX_CONCURRENCY"
  ]

-- | Misspelled keys, enum typos, and an empty origin set are deployment errors, not requests to
-- silently keep a default. The email-verification policy is available from the environment too.
strictConfigurationSettings :: Assertion
strictConfigurationSettings = do
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_CONFIG" configPath

  writeFile configPath "{ cookieSecue = False }"
  unknownKey <- try loadConfig
  expectUserErrorNaming "invalid SHOMEI_CONFIG" unknownKey

  writeFile configPath "{ webauthnAttestation = \"nope\" }"
  badAttestation <- try loadConfig
  expectUserErrorNaming "webauthnAttestation" badAttestation

  writeFile configPath "{ webauthnOrigins = ([] : List Text) }"
  emptyFileOrigins <- try loadConfig
  expectUserErrorNaming "webauthnOrigins" emptyFileOrigins

  unsetEnv "SHOMEI_CONFIG"
  setEnv "SHOMEI_WEBAUTHN_ORIGINS" ","
  emptyEnvOrigins <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_WEBAUTHN_ORIGINS" emptyEnvOrigins
  unsetEnv "SHOMEI_WEBAUTHN_ORIGINS"

  setEnv "SHOMEI_EMAIL_VERIFICATION_REQUIRED" "true"
  (emailRequired, _) <- loadConfigFromEnv
  emailRequired.notifierConfig.emailVerificationRequired @?= True
  unsetEnv "SHOMEI_EMAIL_VERIFICATION_REQUIRED"

  writeFile configPath dhallContents
  unsetEnv "PG_CONNECTION_STRING"

-- | Render the schema's completed default while preserving nulls, then compare its key set to
-- the all-'Nothing' JSON representation of 'FileConfig'. This also asks Dhall to check that the
-- shipped default still has exactly the exported 'Type'.
dhallSchemaMatchesFileConfig :: Assertion
dhallSchemaMatchesFileConfig = do
  schema <- makeAbsolute "../config/shomei-types.dhall"
  writeFile schemaProbePath ("(" <> schema <> ")::{=}")
  rendered <- readProcess "dhall-to-json" ["--preserve-null", "--file", schemaProbePath] ""
  dhallKeys <-
    either assertFailure (pure . KeyMap.keys) $
      (eitherDecodeStrict' (Text.encodeUtf8 (Text.pack rendered)) :: Either String (KeyMap.KeyMap Value))
  blank <- either assertFailure pure (eitherDecodeStrict' "{}" :: Either String FileConfig)
  loaderKeys <- case toJSON blank of
    Object fields -> pure (KeyMap.keys fields)
    _ -> assertFailure "FileConfig did not encode as an object"
  Set.fromList dhallKeys @?= Set.fromList loaderKeys

-- | Assert a config load failed with a 'userError' whose message names the offending variable.
expectUserErrorNaming :: String -> Either IOError a -> Assertion
expectUserErrorNaming name = \case
  Left e ->
    assertBool
      ("expected a userError naming " <> name <> ", got: " <> show e)
      (isUserError e && Text.isInfixOf (Text.pack name) (Text.pack (show e)))
  Right _ -> assertFailure (name <> " out of range should have failed the config load")

testLoadAndOverride :: TestTree
testLoadAndOverride = testCase "Dhall file is loaded and env vars override it" do
  setEnv "SHOMEI_CONFIG" configPath
  setEnv "PG_CONNECTION_STRING" "host=fromenv dbname=shomei"
  unsetEnv "SHOMEI_PORT"
  unsetEnv "SHOMEI_BIND_HOST"
  unsetEnv "SHOMEI_MAX_CONNECTIONS"
  unsetEnv "SHOMEI_SLOW_CLIENT_TIMEOUT_SECONDS"
  unsetEnv "SHOMEI_ISSUER"
  unsetEnv "SHOMEI_WEBAUTHN_RP_ID"
  unsetEnv "SHOMEI_MFA_REQUIRE_SECOND_FACTOR"
  unsetEnv "SHOMEI_MACHINE_TOKEN_TTL"
  unsetEnv "SHOMEI_KEY_REFRESH_INTERVAL"
  unsetEnv "SHOMEI_ALLOWED_CLOCK_SKEW_SECONDS"
  unsetEnv "SHOMEI_NOTIFIER_LOG_SECRETS"
  unsetEnv "SHOMEI_DB_POOL_SIZE"
  unsetEnv "SHOMEI_DB_POOL_ACQUISITION_TIMEOUT_MS"
  unsetEnv "SHOMEI_DB_STATEMENT_TIMEOUT_MS"
  unsetEnv "SHOMEI_NOTIFIER_QUEUE_SIZE"
  (cfg, settings) <- loadConfig
  -- File values beat the defaults (default maxFailedLoginsPerAccount is 5, metrics default True):
  settings.serverPort @?= 8080
  settings.serverBindHost @?= "127.0.0.1"
  settings.serverMaxConnections @?= 32
  settings.serverSlowClientTimeoutSeconds @?= 12
  -- The pool knobs load from the file (defaults are 10 / 10000):
  settings.serverDbPoolSize @?= 25
  settings.serverDbPoolAcquisitionTimeoutMs @?= 2500
  settings.serverDbStatementTimeoutMs @?= 5000
  settings.serverNotifierQueueSize @?= 12
  cfg.rateLimitConfig.maxFailedLoginsPerAccount @?= 7
  -- The signing-key refresh interval loads from the file (default is 60):
  cfg.signingKeyConfig.refreshIntervalSeconds @?= 15
  cfg.signingKeyConfig.allowedClockSkewSeconds @?= 20
  -- Raw one-time tokens are never logged unless the env flag opts in (there is deliberately
  -- no Dhall field for it):
  cfg.notifierConfig.logRawTokens @?= False
  -- File values beat the default password policy (default minLength is 12, rejectCommon True):
  cfg.passwordPolicy.minLength @?= 16
  cfg.passwordPolicy.rejectCommonPasswords @?= False
  -- PG_CONNECTION_STRING (env) overrides the file's databaseUrl:
  settings.serverConnStr @?= "host=fromenv dbname=shomei"
  -- WebAuthn and MFA fields load from their respective Dhall settings.
  -- WebAuthnConfig is read via record destructuring, not value.field dot syntax (HasField is
  -- unreliable for it under DuplicateRecordFields — MasterPlan 3, EP-1 discovery).
  let WebAuthnConfig {rpId = fileRpId, origins = fileOrigins} = webauthnConfig cfg
  fileRpId @?= "auth.fromfile.test"
  fileOrigins @?= ["https://auth.fromfile.test"]
  cfg.mfaConfig.requireSecondFactor @?= False
  cfg.machineTokenConfig.machineTokenTTL @?= 120
  -- An env var overrides the file's port:
  setEnv "SHOMEI_PORT" "9999"
  setEnv "SHOMEI_BIND_HOST" "::1"
  setEnv "SHOMEI_MAX_CONNECTIONS" "64"
  setEnv "SHOMEI_SLOW_CLIENT_TIMEOUT_SECONDS" "8"
  -- and the file's pool knobs (file says 25 / 2500):
  setEnv "SHOMEI_DB_POOL_SIZE" "33"
  setEnv "SHOMEI_DB_POOL_ACQUISITION_TIMEOUT_MS" "2000"
  setEnv "SHOMEI_DB_STATEMENT_TIMEOUT_MS" "7000"
  setEnv "SHOMEI_NOTIFIER_QUEUE_SIZE" "24"
  -- Concept-specific env vars override the file (twelve-factor precedence):
  setEnv "SHOMEI_WEBAUTHN_RP_ID" "auth.fromenv.test"
  setEnv "SHOMEI_MFA_REQUIRE_SECOND_FACTOR" "true"
  setEnv "SHOMEI_MACHINE_TOKEN_TTL" "60"
  (cfg2, settings2) <- loadConfig
  settings2.serverPort @?= 9999
  settings2.serverBindHost @?= "::1"
  settings2.serverMaxConnections @?= 64
  settings2.serverSlowClientTimeoutSeconds @?= 8
  settings2.serverDbPoolSize @?= 33
  settings2.serverDbPoolAcquisitionTimeoutMs @?= 2000
  settings2.serverDbStatementTimeoutMs @?= 7000
  settings2.serverNotifierQueueSize @?= 24
  let WebAuthnConfig {rpId = envRpId} = webauthnConfig cfg2
  envRpId @?= "auth.fromenv.test"
  cfg2.mfaConfig.requireSecondFactor @?= True
  cfg2.machineTokenConfig.machineTokenTTL @?= 60
  -- An env var overrides the file's password min length (file says 16):
  setEnv "SHOMEI_PASSWORD_MIN_LENGTH" "20"
  -- and the file's signing-key refresh interval (file says 15); 0 disables the reload:
  setEnv "SHOMEI_KEY_REFRESH_INTERVAL" "0"
  setEnv "SHOMEI_ALLOWED_CLOCK_SKEW_SECONDS" "45"
  -- SHOMEI_NOTIFIER_LOG_SECRETS is env-only; it is the sole way to turn raw-token logging on:
  setEnv "SHOMEI_NOTIFIER_LOG_SECRETS" "true"
  (cfg3, _) <- loadConfig
  cfg3.passwordPolicy.minLength @?= 20
  cfg3.signingKeyConfig.refreshIntervalSeconds @?= 0
  cfg3.signingKeyConfig.allowedClockSkewSeconds @?= 45
  cfg3.notifierConfig.logRawTokens @?= True
  unsetEnv "SHOMEI_NOTIFIER_LOG_SECRETS"
  unsetEnv "SHOMEI_KEY_REFRESH_INTERVAL"
  unsetEnv "SHOMEI_ALLOWED_CLOCK_SKEW_SECONDS"
  unsetEnv "SHOMEI_PASSWORD_MIN_LENGTH"
  unsetEnv "SHOMEI_CONFIG"
  unsetEnv "SHOMEI_PORT"
  unsetEnv "SHOMEI_BIND_HOST"
  unsetEnv "SHOMEI_MAX_CONNECTIONS"
  unsetEnv "SHOMEI_SLOW_CLIENT_TIMEOUT_SECONDS"
  unsetEnv "SHOMEI_DB_POOL_SIZE"
  unsetEnv "SHOMEI_DB_POOL_ACQUISITION_TIMEOUT_MS"
  unsetEnv "SHOMEI_DB_STATEMENT_TIMEOUT_MS"
  unsetEnv "SHOMEI_NOTIFIER_QUEUE_SIZE"
  unsetEnv "PG_CONNECTION_STRING"
  unsetEnv "SHOMEI_WEBAUTHN_RP_ID"
  unsetEnv "SHOMEI_MFA_REQUIRE_SECOND_FACTOR"
  unsetEnv "SHOMEI_MACHINE_TOKEN_TTL"
  -- Run inline rather than as sibling tasty test cases: each of these mutates process-wide
  -- environment variables, and tasty runs the members of a test group in parallel.
  poolDefaults
  poolRejectsNonPositive
  sweepDefaults
  sweepEnvOverrides
  sweepRejectsNonPositive
  argon2Settings
  strictConfigurationSettings
  dhallSchemaMatchesFileConfig
  coreLoaderNeedsNoConnectionString
  verifierSettings
  rateLimitAndCookieEdgeSettings
  notifierDefaults
  notifierSmtpDhallAndEnv
  passkeyQuotaSettings
  sessionQuotaSettings
  actorCompatibilitySettings

  notifierWebhookEnv
  notifierSmtpMissingHostFails
  notifierSmtpUsernameWithoutPasswordFails
  notifierWebhookMissingSecretFails
  notifierSecretsAreStrippedAndConfigIsSecretFree
  notifierInsecureTransportFlags
  kekIsRequired

passkeyQuotaSettings :: Assertion
passkeyQuotaSettings = do
  unsetEnv "SHOMEI_CONFIG"
  unsetEnv "SHOMEI_MAX_PASSKEYS_PER_USER"
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  (defaults, _) <- loadConfigFromEnv
  defaults.maxPasskeysPerUser @?= 20
  setEnv "SHOMEI_MAX_PASSKEYS_PER_USER" "1"
  (limited, _) <- loadConfigFromEnv
  limited.maxPasskeysPerUser @?= 1
  setEnv "SHOMEI_MAX_PASSKEYS_PER_USER" "101"
  bad <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_MAX_PASSKEYS_PER_USER" bad
  unsetEnv "SHOMEI_MAX_PASSKEYS_PER_USER"

-- | Administrative workflows consume the deployment's core policy without pretending to be a
-- listening server or requiring the server's PostgreSQL variable.
coreLoaderNeedsNoConnectionString :: Assertion
coreLoaderNeedsNoConnectionString = do
  setEnv "SHOMEI_CONFIG" configPath
  unsetEnv "PG_CONNECTION_STRING"
  cfg <- loadCoreConfig
  cfg.passwordPolicy.minLength @?= 16
  cfg.passwordPolicy.rejectCommonPasswords @?= False
  unsetEnv "SHOMEI_CONFIG"

-- | The verifier skew has a safe default, supports env override, and rejects
-- negative values. Issuer and audience are validated as RFC 7519 StringOrURI
-- values before the signer can reach jose's partial IsString conversion.
verifierSettings :: Assertion
verifierSettings = do
  unsetEnv "SHOMEI_CONFIG"
  unsetEnv "SHOMEI_ALLOWED_CLOCK_SKEW_SECONDS"
  unsetEnv "SHOMEI_ISSUER"
  unsetEnv "SHOMEI_AUDIENCE"
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"

  (defaults, _) <- loadConfigFromEnv
  defaults.signingKeyConfig.allowedClockSkewSeconds @?= 30

  setEnv "SHOMEI_ALLOWED_CLOCK_SKEW_SECONDS" "45"
  (overridden, _) <- loadConfigFromEnv
  overridden.signingKeyConfig.allowedClockSkewSeconds @?= 45

  setEnv "SHOMEI_ALLOWED_CLOCK_SKEW_SECONDS" "-1"
  negativeSkew <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_ALLOWED_CLOCK_SKEW_SECONDS" negativeSkew
  unsetEnv "SHOMEI_ALLOWED_CLOCK_SKEW_SECONDS"

  -- RFC 3986 permits a scheme with a rootless path, so this is a valid URI.
  setEnv "SHOMEI_ISSUER" "shomei:prod"
  (schemeIssuer, _) <- loadConfigFromEnv
  let Issuer schemeIssuerText = schemeIssuer.issuer
  schemeIssuerText @?= "shomei:prod"

  setEnv "SHOMEI_ISSUER" "https://bad host"
  invalidIssuer <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_ISSUER" invalidIssuer
  unsetEnv "SHOMEI_ISSUER"

  setEnv "SHOMEI_AUDIENCE" "https://bad audience"
  invalidAudience <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_AUDIENCE" invalidAudience

  unsetEnv "SHOMEI_AUDIENCE"
  unsetEnv "PG_CONNECTION_STRING"

rateLimitEnvVars :: [String]
rateLimitEnvVars =
  [ "SHOMEI_RATE_LIMIT_ENABLED",
    "SHOMEI_MAX_FAILED_LOGINS_PER_ACCOUNT",
    "SHOMEI_MAX_FAILED_LOGINS_PER_IP",
    "SHOMEI_PER_IP_REQUESTS_PER_MINUTE",
    "SHOMEI_PER_IP_BURST",
    "SHOMEI_LOCKOUT_WINDOW_SECONDS",
    "SHOMEI_LOCKOUT_DURATION_SECONDS"
  ]

-- | The complete abuse-policy surface is configurable, invalid zero bounds fail closed,
-- and browser origins are canonical before exact CSRF comparison and warning generation.
rateLimitAndCookieEdgeSettings :: Assertion
rateLimitAndCookieEdgeSettings = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv rateLimitEnvVars
  unsetEnv "SHOMEI_TOKEN_TRANSPORT"
  unsetEnv "SHOMEI_CSRF_ALLOWED_ORIGINS"
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"

  setEnv "SHOMEI_RATE_LIMIT_ENABLED" "false"
  setEnv "SHOMEI_MAX_FAILED_LOGINS_PER_ACCOUNT" "7"
  setEnv "SHOMEI_MAX_FAILED_LOGINS_PER_IP" "23"
  setEnv "SHOMEI_PER_IP_REQUESTS_PER_MINUTE" "71"
  setEnv "SHOMEI_PER_IP_BURST" "11"
  setEnv "SHOMEI_LOCKOUT_WINDOW_SECONDS" "601"
  setEnv "SHOMEI_LOCKOUT_DURATION_SECONDS" "902"
  (configured, _) <- loadConfigFromEnv
  let rate = configured.rateLimitConfig
  rate.rateLimitEnabled @?= False
  rate.maxFailedLoginsPerAccount @?= 7
  rate.maxFailedLoginsPerIp @?= 23
  rate.perIpRequestsPerMinute @?= 71
  rate.perIpBurst @?= 11
  rate.lockoutWindow @?= 601
  rate.lockoutDuration @?= 902

  setEnv "SHOMEI_PER_IP_BURST" "0"
  zeroBurst <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_PER_IP_BURST" zeroBurst
  mapM_ unsetEnv rateLimitEnvVars

  setEnv "SHOMEI_CSRF_ALLOWED_ORIGINS" "HTTPS://App.Example.com/"
  (normalized, _) <- loadConfigFromEnv
  normalized.cookieConfig.allowedOrigins @?= ["https://app.example.com"]
  setEnv "SHOMEI_CSRF_ALLOWED_ORIGINS" "https://app.example.com/login"
  invalidOrigin <- try loadConfigFromEnv
  expectUserErrorNaming "must be scheme://host[:port] with no path" invalidOrigin

  unsetEnv "SHOMEI_CSRF_ALLOWED_ORIGINS"
  setEnv "SHOMEI_TOKEN_TRANSPORT" "cookie"
  (_, defaultWarningSettings) <- loadConfigFromEnv
  defaultWarningSettings.serverWarnings
    @?= ["cookie transport is on with the development default csrfAllowedOrigins = [http://localhost:8080]; browsers on any other origin get 403 csrf_rejected — set SHOMEI_CSRF_ALLOWED_ORIGINS"]

  setEnv "SHOMEI_CSRF_ALLOWED_ORIGINS" "https://app.example.com"
  (_, productionSettings) <- loadConfigFromEnv
  productionSettings.serverWarnings @?= []

  unsetEnv "SHOMEI_TOKEN_TRANSPORT"
  unsetEnv "SHOMEI_CSRF_ALLOWED_ORIGINS"
  unsetEnv "PG_CONNECTION_STRING"

kekIsRequired :: Assertion
kekIsRequired = do
  unsetEnv "SHOMEI_KEY_ENCRYPTION_KEY"
  missing <- try loadKekFromEnv
  expectUserErrorNaming "SHOMEI_KEY_ENCRYPTION_KEY" missing
  setEnv "SHOMEI_KEY_ENCRYPTION_KEY" "not-base64"
  malformed <- try loadKekFromEnv
  expectUserErrorNaming "SHOMEI_KEY_ENCRYPTION_KEY" malformed
  setEnv "SHOMEI_KEY_ENCRYPTION_KEY" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
  _ <- loadKekFromEnv
  unsetEnv "SHOMEI_KEY_ENCRYPTION_KEY"

-- EP-8 notifier transport ------------------------------------------------------

notifierEnvVars :: [String]
notifierEnvVars =
  [ "SHOMEI_NOTIFIER_TRANSPORT",
    "SHOMEI_NOTIFIER_ALSO_LOG",
    "SHOMEI_SMTP_HOST",
    "SHOMEI_SMTP_PORT",
    "SHOMEI_SMTP_TLS_MODE",
    "SHOMEI_SMTP_USERNAME",
    "SHOMEI_SMTP_PASSWORD",
    "SHOMEI_SMTP_FROM",
    "SHOMEI_SMTP_TIMEOUT",
    "SHOMEI_WEBHOOK_URL",
    "SHOMEI_WEBHOOK_SECRET",
    "SHOMEI_WEBHOOK_TIMEOUT",
    "SHOMEI_WEBHOOK_MAX_ATTEMPTS",
    "SHOMEI_WEBHOOK_ALLOW_INSECURE",
    "SHOMEI_SMTP_ALLOW_PLAINTEXT_AUTH",
    "SHOMEI_NOTIFIER_QUEUE_SIZE"
  ]

-- | With no config and no env, the notifier is the log sender and both sub-configs are absent.
notifierDefaults :: Assertion
notifierDefaults = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv notifierEnvVars
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  (cfg, _) <- loadConfigFromEnv
  let nc = cfg.notifierConfig
  nc.notifierTransport @?= LogNotifier
  nc.smtpConfig @?= Nothing
  nc.webhookConfig @?= Nothing
  nc.alsoLogNotifications @?= False
  unsetEnv "PG_CONNECTION_STRING"

smtpConfigPath :: FilePath
smtpConfigPath = "/tmp/shomei-config-smtp-test.dhall"

-- | The SMTP transport loads from the Dhall file, while the relay password enters the separate
-- runtime-only secret value.
notifierSmtpDhallAndEnv :: Assertion
notifierSmtpDhallAndEnv = do
  mapM_ unsetEnv notifierEnvVars
  writeFile
    smtpConfigPath
    ( "{ databaseUrl = \"host=fromfile dbname=shomei\""
        <> ", notifierTransport = \"smtp\""
        <> ", smtpHost = \"email-smtp.us-east-1.amazonaws.com\""
        <> ", smtpPort = 587"
        <> ", smtpTlsMode = \"starttls\""
        <> ", smtpUsername = \"AKIAEXAMPLE\""
        <> ", smtpFromAddress = \"auth@example.com\""
        <> ", smtpTimeoutSeconds = 20"
        <> ", publicBaseUrl = \"https://auth.example.com\""
        <> " }"
    )
  setEnv "SHOMEI_CONFIG" smtpConfigPath
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_SMTP_PASSWORD" "smtp-secret-from-env"
  -- publicBaseUrl is set in the Dhall file; the env var overrides it (twelve-factor).
  setEnv "SHOMEI_PUBLIC_BASE_URL" "https://auth.env.example.com"
  (cfg, _) <- loadConfig
  let nc = cfg.notifierConfig
  nc.notifierTransport @?= SmtpNotifier
  nc.publicBaseUrl @?= "https://auth.env.example.com"
  case nc.smtpConfig of
    Just (SmtpConfig {host = h, port = p, tlsMode = tls, username = u, fromAddress = fromA, timeoutSeconds = to}) -> do
      h @?= "email-smtp.us-east-1.amazonaws.com"
      p @?= 587
      tls @?= SmtpStartTls
      u @?= Just "AKIAEXAMPLE"
      fromA @?= "auth@example.com"
      to @?= 20
    Nothing -> assertFailure "expected smtpConfig to be populated for transport=smtp"
  secrets <- loadNotifierSecretsFromEnv cfg
  fmap smtpPasswordText secrets.smtpPassword @?= Just "smtp-secret-from-env"
  unsetEnv "SHOMEI_SMTP_PASSWORD"
  unsetEnv "SHOMEI_PUBLIC_BASE_URL"
  unsetEnv "SHOMEI_CONFIG"
  unsetEnv "PG_CONNECTION_STRING"

-- | The webhook transport can be configured entirely from the environment, with its secret
-- held separately from the printable config.
notifierWebhookEnv :: Assertion
notifierWebhookEnv = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv notifierEnvVars
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_NOTIFIER_TRANSPORT" "webhook"
  setEnv "SHOMEI_WEBHOOK_URL" "https://hooks.example.com/shomei"
  setEnv "SHOMEI_WEBHOOK_SECRET" "webhook-secret"
  setEnv "SHOMEI_WEBHOOK_MAX_ATTEMPTS" "5"
  setEnv "SHOMEI_NOTIFIER_ALSO_LOG" "true"
  (cfg, _) <- loadConfigFromEnv
  let nc = cfg.notifierConfig
  nc.notifierTransport @?= WebhookNotifier
  nc.alsoLogNotifications @?= True
  case nc.webhookConfig of
    Just (WebhookConfig {url = u, maxAttempts = ma}) -> do
      u @?= "https://hooks.example.com/shomei"
      ma @?= 5
    Nothing -> assertFailure "expected webhookConfig to be populated for transport=webhook"
  secrets <- loadNotifierSecretsFromEnv cfg
  fmap webhookSecretBytes secrets.webhookSecret @?= Just "webhook-secret"
  mapM_ unsetEnv notifierEnvVars
  unsetEnv "PG_CONNECTION_STRING"

-- | Selecting smtp without a host fails the boot, naming the missing variable.
notifierSmtpMissingHostFails :: Assertion
notifierSmtpMissingHostFails = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv notifierEnvVars
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_NOTIFIER_TRANSPORT" "smtp"
  setEnv "SHOMEI_SMTP_FROM" "auth@example.com"
  result <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_SMTP_HOST" result
  mapM_ unsetEnv notifierEnvVars
  unsetEnv "PG_CONNECTION_STRING"

-- | An SMTP username with no password (or vice versa) is refused: it is neither a valid
-- authenticated relay nor a valid lab sink.
notifierSmtpUsernameWithoutPasswordFails :: Assertion
notifierSmtpUsernameWithoutPasswordFails = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv notifierEnvVars
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_NOTIFIER_TRANSPORT" "smtp"
  setEnv "SHOMEI_SMTP_HOST" "smtp.example.com"
  setEnv "SHOMEI_SMTP_FROM" "auth@example.com"
  setEnv "SHOMEI_SMTP_USERNAME" "relay-user"
  (cfg, _) <- loadConfigFromEnv
  result <- try (loadNotifierSecretsFromEnv cfg)
  expectUserErrorNaming "SHOMEI_SMTP_PASSWORD" result
  mapM_ unsetEnv notifierEnvVars
  unsetEnv "PG_CONNECTION_STRING"

-- | Selecting webhook without a signing secret fails the boot, naming the missing variable.
notifierWebhookMissingSecretFails :: Assertion
notifierWebhookMissingSecretFails = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv notifierEnvVars
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_NOTIFIER_TRANSPORT" "webhook"
  setEnv "SHOMEI_WEBHOOK_URL" "https://hooks.example.com/shomei"
  (cfg, _) <- loadConfigFromEnv
  result <- try (loadNotifierSecretsFromEnv cfg)
  expectUserErrorNaming "SHOMEI_WEBHOOK_SECRET" result
  mapM_ unsetEnv notifierEnvVars
  unsetEnv "PG_CONNECTION_STRING"

notifierSecretsAreStrippedAndConfigIsSecretFree :: Assertion
notifierSecretsAreStrippedAndConfigIsSecretFree = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv notifierEnvVars
  setEnv "PG_CONNECTION_STRING" "  host=localhost dbname=shomei  "
  setEnv "SHOMEI_NOTIFIER_TRANSPORT" "webhook"
  setEnv "SHOMEI_WEBHOOK_URL" "  https://hooks.example.com/shomei  "
  setEnv "SHOMEI_WEBHOOK_SECRET" "  webhook-secret\n"
  setEnv "SHOMEI_SMTP_PASSWORD" "  smtp-secret\n"
  (cfg, settings) <- loadConfigFromEnv
  settings.serverConnStr @?= "host=localhost dbname=shomei"
  secrets <- loadNotifierSecretsFromEnv cfg
  fmap webhookSecretBytes secrets.webhookSecret @?= Just "webhook-secret"
  fmap smtpPasswordText secrets.smtpPassword @?= Just "smtp-secret"
  let notifier = cfg.notifierConfig
      fullConfig =
        cfg
          { notifierConfig =
              notifier
                { smtpConfig =
                    Just
                      SmtpConfig
                        { host = "smtp.example.com",
                          port = 587,
                          tlsMode = SmtpStartTls,
                          username = Just "relay-user",
                          fromAddress = "auth@example.com",
                          timeoutSeconds = 10
                        }
                }
          }
      encoded = LBS8.unpack (encode fullConfig)
  assertBool "ShomeiConfig JSON must not have an SMTP password field" (not ("\"password\":" `isInfixOf` encoded))
  assertBool "ShomeiConfig JSON must not have a webhook secret field" (not ("\"secret\":" `isInfixOf` encoded))
  mapM_ unsetEnv notifierEnvVars
  unsetEnv "PG_CONNECTION_STRING"

notifierInsecureTransportFlags :: Assertion
notifierInsecureTransportFlags = do
  unsetEnv "SHOMEI_CONFIG"
  mapM_ unsetEnv notifierEnvVars
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  setEnv "SHOMEI_NOTIFIER_TRANSPORT" "webhook"
  setEnv "SHOMEI_WEBHOOK_URL" "http://127.0.0.1:9999/hook"
  setEnv "SHOMEI_WEBHOOK_SECRET" "secret"
  insecureWebhook <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_WEBHOOK_ALLOW_INSECURE" insecureWebhook
  setEnv "SHOMEI_WEBHOOK_ALLOW_INSECURE" "true"
  (webhookCfg, _) <- loadConfigFromEnv
  _ <- loadNotifierSecretsFromEnv webhookCfg
  setEnv "SHOMEI_WEBHOOK_URL" "http://hooks.example.com/hook?secret=hidden"
  (_, httpSettings) <- loadConfigFromEnv
  assertBool "non-loopback HTTP lab receiver warns" ("webhookUrl uses HTTP under SHOMEI_WEBHOOK_ALLOW_INSECURE; one-time links travel in the clear; lab receivers only" `elem` httpSettings.serverWarnings)
  assertBool "warning contains no URL secrets" (not (any (Text.isInfixOf "hidden") httpSettings.serverWarnings))

  mapM_ unsetEnv notifierEnvVars
  setEnv "SHOMEI_NOTIFIER_TRANSPORT" "smtp"
  setEnv "SHOMEI_SMTP_HOST" "smtp.example.com"
  setEnv "SHOMEI_SMTP_FROM" "auth@example.com"
  setEnv "SHOMEI_SMTP_TLS_MODE" "plain"
  setEnv "SHOMEI_SMTP_USERNAME" "relay-user"
  setEnv "SHOMEI_SMTP_PASSWORD" "relay-password"
  plaintextAuth <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_SMTP_ALLOW_PLAINTEXT_AUTH" plaintextAuth
  setEnv "SHOMEI_SMTP_ALLOW_PLAINTEXT_AUTH" "true"
  (smtpCfg, smtpSettings) <- loadConfigFromEnv
  assertBool "plaintext credentials warn under the explicit lab override" ("smtpTlsMode=plain sends mail and SMTP credentials in the clear; explicit lab override" `elem` smtpSettings.serverWarnings)
  _ <- loadNotifierSecretsFromEnv smtpCfg

  unsetEnv "SHOMEI_SMTP_USERNAME"
  unsetEnv "SHOMEI_SMTP_PASSWORD"
  unsetEnv "SHOMEI_SMTP_ALLOW_PLAINTEXT_AUTH"
  (_, warningSettings) <- loadConfigFromEnv
  assertBool
    "unauthenticated plaintext SMTP warns"
    ("smtpTlsMode=plain sends mail in the clear; lab sinks only" `elem` warningSettings.serverWarnings)
  mapM_ unsetEnv notifierEnvVars
  unsetEnv "PG_CONNECTION_STRING"

actorCompatibilitySettings :: Assertion
actorCompatibilitySettings = do
  unsetEnv "SHOMEI_CONFIG"
  unsetEnv "SHOMEI_LEGACY_ACTOR_UNTIL_UNIX"
  unsetEnv "SHOMEI_EMIT_LEGACY_ACTOR"
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  (defaults, _) <- loadConfigFromEnv
  defaults.signingKeyConfig.legacyActorUntilUnix @?= Nothing
  defaults.signingKeyConfig.emitLegacyActor @?= False
  setEnv "SHOMEI_EMIT_LEGACY_ACTOR" "true"
  missing <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_LEGACY_ACTOR_UNTIL_UNIX" missing
  setEnv "SHOMEI_LEGACY_ACTOR_UNTIL_UNIX" "2000000000"
  (compatible, _) <- loadConfigFromEnv
  compatible.signingKeyConfig.legacyActorUntilUnix @?= Just 2000000000
  compatible.signingKeyConfig.emitLegacyActor @?= True
  setEnv "SHOMEI_LEGACY_ACTOR_UNTIL_UNIX" "0"
  invalid <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_LEGACY_ACTOR_UNTIL_UNIX" invalid
  unsetEnv "SHOMEI_LEGACY_ACTOR_UNTIL_UNIX"
  unsetEnv "SHOMEI_EMIT_LEGACY_ACTOR"

sessionQuotaSettings :: Assertion
sessionQuotaSettings = do
  setEnv "PG_CONNECTION_STRING" "host=localhost dbname=shomei"
  unsetEnv "SHOMEI_CONFIG"
  unsetEnv "SHOMEI_MAX_ACTIVE_NON_INTERACTIVE_SESSIONS"
  (defaults, _) <- loadConfigFromEnv
  defaults.maxActiveNonInteractiveSessions @?= 1000
  setEnv "SHOMEI_MAX_ACTIVE_NON_INTERACTIVE_SESSIONS" "1"
  (limited, _) <- loadConfigFromEnv
  limited.maxActiveNonInteractiveSessions @?= 1
  setEnv "SHOMEI_MAX_ACTIVE_NON_INTERACTIVE_SESSIONS" "0"
  bad <- try loadConfigFromEnv
  expectUserErrorNaming "SHOMEI_MAX_ACTIVE_NON_INTERACTIVE_SESSIONS" bad
  unsetEnv "SHOMEI_MAX_ACTIVE_NON_INTERACTIVE_SESSIONS"