packages feed

etc-0.3.0.0: 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 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 "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 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\":{\"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\":{\"cli\":{\"input\":\"option\",\"short\":\"g\",\"type\":\"string\"}}}}}"
        keys  = ["greeting"]

      (config :: ConfigSpec ()) <- parseConfigSpec input

      let
        result = do
          value    <- getConfigValue keys (specConfigValues config)
          PlainEntry metadata <- cliEntry (configSources value)
          short <- optShort metadata
          valueType <- pure $ optValueType 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" StringOpt valueType

  , testCase "cli option entry works when setting long and type" $ do
      let
        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"cli\":{\"input\":\"option\",\"long\":\"greeting\",\"type\":\"string\"}}}}}"
        keys  = ["greeting"]

      (config :: ConfigSpec ()) <- parseConfigSpec input

      let
        result = do
          value    <- getConfigValue keys (specConfigValues config)
          PlainEntry metadata <- cliEntry (configSources value)
          long <- optLong metadata
          valueType <- pure $ optValueType 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" StringOpt valueType

  , testCase "cli entry accepts command" $ do
      let
        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"cli\":{\"input\":\"option\",\"long\":\"greeting\",\"type\":\"string\",\"commands\":[\"foo\"]}}}}}"
        keys  = ["greeting"]

      (config :: ConfigSpec Text) <- parseConfigSpec input

      let
        result = do
          value <- getConfigValue keys (specConfigValues config)
          CmdEntry cmd metadata <- cliEntry (configSources value)
          long <- optLong metadata
          valueType <- pure $ optValueType 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" StringOpt 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\":{\"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"
      mconfig <- YAML.decodeFile path

      case mconfig of
        Nothing ->
          assertFailure "yaml file did not have a configuration spec"

        Just (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
  ]