etc-0.4.0.1: test/System/Etc/SpecTest.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module System.Etc.SpecTest (tests) where
import RIO
import qualified RIO.HashMap as HashMap
import qualified RIO.Vector as Vector
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, assertFailure, testCase)
import qualified Data.Aeson as JSON
import System.Etc.Internal.Spec.Types
import System.Etc.Spec
#ifdef WITH_YAML
import qualified Data.Yaml as YAML
import Paths_etc (getDataFileName)
#endif
getConfigValue :: [Text] -> HashMap Text (ConfigValue cmd) -> Maybe (ConfigValue cmd)
getConfigValue keys hsh =
-- NOTE: For some reason, pattern matching doesn't work on lts-6, using
-- if/else instead
if null keys
then Nothing
else do
let (k : ks) = keys
cv <- HashMap.lookup k hsh
case cv of
SubConfig hsh1 -> getConfigValue ks hsh1
value -> if null ks then Just value else Nothing
general_tests :: TestTree
general_tests = testGroup
"general"
[ testCase "does not fail when etc/entries is not defined" $ do
let input = "{}"
case parseConfigSpec input of
Nothing -> assertFailure "should not fail if no etc/entries key is present"
Just (_ :: ConfigSpec ()) -> assertBool "" True
, testCase "fails when default JSON value doesn't correspond to type entry" $ do
let
input
= "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"default\":[123],\"type\":\"[string]\"}}}}"
case parseConfigSpec input of
Left err -> case fromException err of
Just InvalidConfiguration{} -> assertBool "" True
_ ->
assertFailure
$ "expecting to get an InvalidConfiguration error; but got "
<> show err
Right (_ :: ConfigSpec ()) ->
assertFailure "Expecting config spec parsing to fail, but didn't"
, testCase "entries cannot finish in an empty map" $ do
let input = "{\"etc/entries\":{\"greeting\":{}}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "entries should not accept empty maps as values " ++ show result
Nothing -> assertBool "" True
, testCase "entries cannot finish in an array" $ do
let input = "{\"etc/entries\":{\"greeting\":[]}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "entries should not accept arrays as values " ++ show result
Nothing -> assertBool "" True
, testCase "entries that finish with raw values sets them as default value" $ do
let input = "{\"etc/entries\":{\"greeting\":123}}"
keys = ["greeting"]
config <- parseConfigSpec input
case getConfigValue keys (specConfigValues config) of
Nothing -> assertFailure
(show keys ++ " should map to a config value, got sub config map instead")
Just (value :: ConfigValue ()) -> assertEqual "should contain default value"
(Just (JSON.Number 123))
(defaultValue value)
, testCase "entries that finish with arrays sets them as default value" $ do
let input = "{\"etc/entries\":{\"greeting\":[123]}}"
keys = ["greeting"]
config <- parseConfigSpec input
case getConfigValue keys (specConfigValues config) of
Nothing -> assertFailure
(show keys ++ " should map to a config value, got sub config map instead")
Just (value :: ConfigValue ()) -> assertEqual
"should contain default value"
(Just (JSON.Array (Vector.fromList [JSON.Number 123])))
(defaultValue value)
, testCase "entries with empty arrays as values fail because type cannot be infered" $ do
let input = "{\"etc/entries\":{\"greeting\": []}}"
case parseConfigSpec input of
Left err -> case fromException err of
Just InvalidConfiguration{} -> assertBool "" True
_ ->
assertFailure $ "expecting InvalidConfiguration error; got instead " <> show err
Right (_configSpec :: ConfigSpec ()) ->
assertFailure "expecting config spec parse to fail, but didn't"
, testCase "entries with empty arrays as default values and a type do not fail" $ do
let
input
= "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"default\":[],\"type\":\"[string]\"}}}}"
keys = ["greeting"]
config <- parseConfigSpec input
case getConfigValue keys (specConfigValues config) of
Nothing -> assertFailure
(show keys ++ " should map to an array config value, got sub config map instead")
Just (value :: ConfigValue ()) -> assertEqual
"should contain default array value"
(Just (JSON.Array (Vector.fromList [])))
(defaultValue value)
, testCase "entries with array of objects do not fail" $ do
let
input
= "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"default\":[{\"hello\":\"world\"}],\"type\":\"[object]\"}}}}"
keys = ["greeting"]
config <- parseConfigSpec input
case getConfigValue keys (specConfigValues config) of
Nothing -> assertFailure
(show keys ++ " should map to an array config value, got sub config map instead")
Just (value :: ConfigValue ()) -> assertEqual
"should contain default array value"
(Just
(JSON.Array (Vector.fromList [JSON.object ["hello" JSON..= ("world" :: Text)]]))
)
(defaultValue value)
, testCase "entries can have many levels of nesting" $ do
let input = "{\"etc/entries\":{\"english\":{\"greeting\":\"hello\"}}}"
keys = ["english", "greeting"]
config <- parseConfigSpec input
case getConfigValue keys (specConfigValues config) of
Nothing -> assertFailure
(show keys ++ " should map to a config value, got sub config map instead")
Just (value :: ConfigValue ()) -> assertEqual "should contain default value"
(Just (JSON.String "hello"))
(defaultValue value)
, testCase "spec map cannot be empty object" $ do
let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{}}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "etc/spec map should not be an empty object " ++ show result
Nothing -> assertBool "" True
, testCase "spec map cannot be a JSON array" $ do
let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":[]}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "etc/spec map should not be an array " ++ show result
Nothing -> assertBool "" True
, testCase "spec map cannot be a JSON bool" $ do
let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":true}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "etc/spec map should not be a boolean " ++ show result
Nothing -> assertBool "" True
, testCase "spec map cannot be a JSON string" $ do
let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":\"hello\"}}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "etc/spec map should not be a string " ++ show result
Nothing -> assertBool "" True
, testCase "spec map cannot be a JSON number" $ do
let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":123}}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "etc/spec map should not be a number " ++ show result
Nothing -> assertBool "" True
, testCase "spec map cannot have any other key that is not etc/spec" $ do
let
input
= "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"default\":\"hello\"},\"foobar\":123}}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "etc/spec map should not contain more than one key " ++ show result
Nothing -> assertBool "" True
]
#ifdef WITH_CLI
cli_tests :: TestTree
cli_tests =
testGroup "cli"
[
testCase "cli entry settings requires an input" $ do
let
input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"cli\":{}}}}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "cli entry should require a type " ++ show result
Nothing ->
assertBool "" True
, testCase "cli option entry requires either short or long" $ do
let
input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"type\": \"string\", \"cli\":{\"input\":\"option\"}}}}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "cli entry should require a type " ++ show result
Nothing ->
assertBool "" True
, testCase "cli option entry works when setting short and type" $ do
let
input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"type\":\"string\",\"cli\":{\"input\":\"option\",\"short\":\"g\"}}}}}"
keys = ["greeting"]
(config :: ConfigSpec ()) <- parseConfigSpec input
let
result = do
value <- getConfigValue keys (specConfigValues config)
let valueType = configValueType value
PlainEntry metadata <- cliEntry (configSources value)
short <- optShort metadata
return (short, valueType)
case result of
Nothing ->
assertFailure (show keys ++ " should map to a config value, got sub config map instead")
Just (short, valueType) -> do
assertEqual "should contain short" "g" short
assertEqual "should contain option type" (CVTSingle CVTString) valueType
, testCase "cli option entry works when setting long and type" $ do
let
input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"type\": \"string\",\"cli\":{\"input\":\"option\",\"long\":\"greeting\"}}}}}"
keys = ["greeting"]
(config :: ConfigSpec ()) <- parseConfigSpec input
let
result = do
value <- getConfigValue keys (specConfigValues config)
let valueType = configValueType value
PlainEntry metadata <- cliEntry (configSources value)
long <- optLong metadata
return (long, valueType)
case result of
Nothing ->
assertFailure (show keys ++ " should map to a config value, got sub config map instead")
Just (long, valueType) -> do
assertEqual "should contain long" "greeting" long
assertEqual "should contain option type" (CVTSingle CVTString) valueType
, testCase "cli entry accepts command" $ do
let
input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"type\": \"string\",\"cli\":{\"input\":\"option\",\"long\":\"greeting\",\"commands\":[\"foo\"]}}}}}"
keys = ["greeting"]
(config :: ConfigSpec Text) <- parseConfigSpec input
let
result = do
value <- getConfigValue keys (specConfigValues config)
let valueType = configValueType value
CmdEntry cmd metadata <- cliEntry (configSources value)
long <- optLong metadata
return (cmd, long, valueType)
case result of
Nothing ->
assertFailure (show config)
Just (cmd, long, valueType) -> do
assertEqual "should contain cmd" ["foo"] cmd
assertEqual "should contain long" "greeting" long
assertEqual "should contain option type" (CVTSingle CVTString) valueType
, testCase "cli entry does not accept unrecognized keys" $ do
let
-- error is a typo on key command (instead of command_s_)
input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"cli\":{\"input\":\"option\",\"long\":\"greeting\",\"type\":\"string\",\"command\":[\"foo\"]}}}}}"
case parseConfigSpec input of
Just (result :: ConfigSpec ()) ->
assertFailure $ "cli entry should fail on invalid entry " ++ show result
Nothing ->
assertBool "" True
]
#endif
envvar_tests :: TestTree
envvar_tests = testGroup
"env"
[ testCase "env key creates an ENV source" $ do
let
input
= "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"type\": \"string\",\"env\":\"GREETING\"}}}}"
keys = ["greeting"]
(config :: ConfigSpec ()) <- parseConfigSpec input
case getConfigValue keys (specConfigValues config) of
Nothing -> assertFailure
(show keys ++ " should map to a config value, got sub config map instead")
Just value -> assertEqual "should contain EnvVar value"
(ConfigSources (Just "GREETING") Nothing)
(configSources value)
]
#ifdef WITH_YAML
yaml_tests :: TestTree
yaml_tests =
testGroup "yaml parser"
[
testCase "should work with Aeson instances" $ do
let
keys = ["greeting"]
path <- getDataFileName "test/fixtures/config.spec.yaml"
econfig <- YAML.decodeFileEither path
case econfig of
Left err ->
assertFailure $ "yaml file did not have a configuration spec: " <> show err
Right (config :: ConfigSpec ()) ->
case getConfigValue keys (specConfigValues config) of
Nothing ->
assertFailure (show keys ++ " should map to a config value, got sub config map instead")
Just value ->
assertEqual "should contain EnvVar value"
(ConfigSources (Just "GREETING") Nothing)
(configSources value)
]
#endif
tests :: TestTree
tests = testGroup
"spec"
[ general_tests
#ifdef WITH_YAML
, yaml_tests
#endif
, envvar_tests
#ifdef WITH_CLI
, cli_tests
#endif
]