packages feed

settei-0.2.0.0: test/Settei/ValueTest.hs

module Settei.ValueTest (tests) where

import Data.Int (Int16)
import Data.Ratio ((%))
import Data.Text qualified as Text
import Settei
import Settei.Prelude
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "Settei.Value"
    [ testCase "text values decode" $
        decodeSetting publicText (RawText "hello") @?= Right "hello",
      testCase "textual booleans decode case-insensitively" $
        decodeSetting publicBoolean (RawText "TRUE") @?= Right True,
      testCase "bounded integral values decode" $
        decodeSetting publicInteger (RawText "42") @?= Right (42 :: Int),
      testCase "explicit enumerations decode" $
        decodeSetting environment (RawText "production") @?= Right Production,
      testCase "secret decoder failures omit rejected values" $ case decodeSetting secretText (RawText "supersecret") of
        Right _ -> fail "expected decoding to fail"
        Left failureValue ->
          assertBool
            "rendered failure leaked the rejected secret"
            (not (Text.isInfixOf "supersecret" (renderDecodeFailure failureValue))),
      testCase "fmap maps a successful decode" $
        runDecoder (Text.toUpper <$> textDecoder) exampleKey (RawText "hello")
          @?= Right "HELLO",
      testCase "fmap passes failures through unchanged" $
        case runDecoder (Text.toUpper <$> textDecoder) exampleKey (RawBool True) of
          Right _ -> fail "expected decoding to fail"
          Left failureValue -> decodeFailureExpected failureValue @?= "text",
      testCase "listDecoder decodes arrays element-wise" $
        runDecoder (listDecoder textDecoder) exampleKey (RawArray [RawText "a", RawText "b"])
          @?= Right ["a", "b"],
      testCase "listDecoder rejects non-arrays" $
        case runDecoder (listDecoder textDecoder) exampleKey (RawText "a") of
          Right _ -> fail "expected decoding to fail"
          Left failureValue -> decodeFailureExpected failureValue @?= "an array",
      testCase "listDecoder wraps element expectations" $
        case runDecoder (listDecoder textDecoder) exampleKey (RawArray [RawText "a", RawBool True]) of
          Right _ -> fail "expected decoding to fail"
          Left failureValue -> decodeFailureExpected failureValue @?= "an array of text",
      testCase "nonEmptyDecoder decodes non-empty arrays" $
        runDecoder (nonEmptyDecoder textDecoder) exampleKey (RawArray [RawText "a", RawText "b"])
          @?= Right ("a" :| ["b"]),
      testCase "nonEmptyDecoder rejects empty arrays" $
        case runDecoder (nonEmptyDecoder textDecoder) exampleKey (RawArray []) of
          Right _ -> fail "expected decoding to fail"
          Left failureValue -> decodeFailureExpected failureValue @?= "a non-empty array",
      testCase "parsedDecoder applies the parser" $
        runDecoder portFromText exampleKey (RawText "8080") @?= Right (8080 :: Int),
      testCase "parsedDecoder reports only the caller's expectation" $
        case runDecoder portFromText exampleKey (RawText sentinel) of
          Right _ -> fail "expected decoding to fail"
          Left failureValue ->
            decodeFailureExpected failureValue @?= "a port number",
      testCase "parsedDecoder discards parser messages that echo input" $
        case runDecoder portFromText exampleKey (RawText sentinel) of
          Right _ -> fail "expected decoding to fail"
          Left failureValue ->
            assertBool
              "rendered failure leaked the parser input"
              (not (Text.isInfixOf sentinel (renderDecodeFailure failureValue))),
      testCase "listDecoder element failures never leak the value" $
        case runDecoder (listDecoder boolDecoder) exampleKey (RawArray [RawText sentinel]) of
          Right _ -> fail "expected decoding to fail"
          Left failureValue ->
            assertBool
              "rendered failure leaked the array element"
              (not (Text.isInfixOf sentinel (renderDecodeFailure failureValue))),
      testCase "rationalDecoder accepts numbers and numeric text" $ do
        runDecoder rationalDecoder exampleKey (RawNumber (5 % 2)) @?= Right (5 % 2)
        runDecoder rationalDecoder exampleKey (RawText "2.5") @?= Right (5 % 2)
        runDecoder rationalDecoder exampleKey (RawText "-3") @?= Right (negate 3),
      testCase "rationalDecoder rejects trailing garbage" $
        case runDecoder rationalDecoder exampleKey (RawText "1.5x") of
          Right _ -> fail "expected decoding to fail"
          Left failureValue -> decodeFailureExpected failureValue @?= "a number",
      testCase "doubleDecoder rounds through the exact rational" $ do
        runDecoder doubleDecoder exampleKey (RawText "2.5") @?= Right 2.5
        runDecoder doubleDecoder exampleKey (RawNumber (1 % 10)) @?= Right 0.1,
      testCase "boundedIntegralDecoder reports its range" $
        case runDecoder (boundedIntegralDecoder :: Decoder Int16) exampleKey (RawNumber 100000) of
          Right _ -> fail "expected decoding to fail"
          Left failureValue ->
            decodeFailureExpected failureValue @?= "integer between -32768 and 32767"
    ]

data Environment = Development | Production
  deriving stock (Generic, Eq, Show)

publicText :: Setting Text
publicText = publicSetting exampleKey "Example text" textDecoder

secretText :: Setting Bool
secretText = secretSetting exampleKey "Example secret" boolDecoder

publicBoolean :: Setting Bool
publicBoolean = publicSetting exampleKey "Example boolean" boolDecoder

publicInteger :: Setting Int
publicInteger = publicSetting exampleKey "Example integer" boundedIntegralDecoder

environment :: Setting Environment
environment =
  publicSetting
    exampleKey
    "Runtime environment"
    (enumDecoder [("development", Development), ("production", Production)])

exampleKey :: Key
exampleKey = validKey "example.value"

portFromText :: Decoder Int
portFromText =
  parsedDecoder "a port number" $ \value ->
    case runDecoder (boundedIntegralDecoder :: Decoder Int) exampleKey (RawText value) of
      Right port -> Right port
      Left _ -> Left ("could not parse port from input: " <> value)

sentinel :: Text
sentinel = "s3cr3t\"\\\SOH!☃パス"

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