packages feed

dotenv-0.6.0.1: spec/Configuration/DotenvSpec.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}

module Configuration.DotenvSpec (main, spec) where

import Configuration.Dotenv.Types (Config(..))
import Configuration.Dotenv
  ( load
  , loadFile
  , loadSafeFile
  , parseFile
  , onMissingFile
  )


import Test.Hspec

import System.Process (readCreateProcess, shell)
import System.Environment (lookupEnv)
import Control.Monad (liftM, void)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import qualified Data.Map.Lazy as M
#if !MIN_VERSION_base(4,8,0)
import Data.Functor ((<$>))
#endif

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$))
#endif

#if MIN_VERSION_base(4,7,0)
import System.Environment (getEnvironment, setEnv, unsetEnv)
#else
import System.Environment.Compat (getEnvironment, setEnv, unsetEnv)
#endif

main :: IO ()
main = hspec spec

spec :: Spec
spec = do
  describe "load" $ after_ clearEnvs $ do
    it "loads the given list of configuration options to the environment" $ do
      lookupEnv "foo" `shouldReturn` Nothing

      load False [("foo", "bar")]

      lookupEnv "foo" `shouldReturn` Just "bar"

    it "preserves existing settings when overload is false" $ do
      setEnv "foo" "preset"

      load False [("foo", "new setting")]

      lookupEnv "foo" `shouldReturn` Just "preset"

    it "overrides existing settings when overload is true" $ do
      setEnv "foo" "preset"

      load True [("foo", "new setting")]

      lookupEnv "foo" `shouldReturn` Just "new setting"

  describe "loadFile" $ before_ setupEnv $ after_ clearEnvs $  do
    it "loads the configuration options to the environment from a file" $ do
      lookupEnv "DOTENV" `shouldReturn` Nothing

      void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] False

      lookupEnv "DOTENV" `shouldReturn` Just "true"

    it "respects predefined settings when overload is false" $ do
      setEnv "DOTENV" "preset"

      void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] False

      lookupEnv "DOTENV" `shouldReturn` Just "preset"

    it "overrides predefined settings when overload is true" $ do
      setEnv "DOTENV" "preset"

      void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] True

      lookupEnv "DOTENV" `shouldReturn` Just "true"

    context "when the .env.example is present" $ do
      let config = Config ["spec/fixtures/.dotenv"] ["spec/fixtures/.dotenv.example"] False

      context "when the needed env vars are missing" $
        it "should fail with an error call" $ do
          unsetEnv "ANOTHER_ENV"
          void $ loadFile config `shouldThrow` anyErrorCall

      context "when the needed env vars are not missing" $
        it "should succeed when loading all of the needed env vars" $ do
          -- Load extra information
          me <- init <$> readCreateProcess (shell "whoami") ""
          home <- fromMaybe "" <$> lookupEnv "HOME"

          -- Load envs
          void $ loadFile config

          -- Check existing envs
          lookupEnv "ENVIRONMENT" `shouldReturn` Just home
          lookupEnv "ME" `shouldReturn` Just me
          lookupEnv "DOTENV" `shouldReturn` Just "true"
          lookupEnv "UNICODE_TEST" `shouldReturn` Just "Manabí"
          lookupEnv "ANOTHER_ENV" `shouldReturn` Just "hello"

  describe "loadSafeFile" $ after_ clearEnvs $
    context "given a custom map" $ do
      context "when the envs are written accordingly to the rules in the map" $
        it "should validate accordingly to the rules in the custom map" $
          let hasTwoLetters text = T.length text == 2
              customMap = M.fromList
                [ ("twoLetters", hasTwoLetters)
                , ("bool", const True)
                , ("text", const True)
                , ("integer", const True)
                ]
              schemaFile = "spec/fixtures/.scheme.yml"
              config = Config ["spec/fixtures/.dotenv.safe"] [] False
              expectedEnvs =
                [ ("DOTENV","true")
                , ("OTHERENV","false")
                , ("PORT","8000")
                , ("URL","http://example.com")
                , ("TWO","xD")
                ]
           in do
             envs <- loadSafeFile customMap schemaFile config
             envs `shouldMatchList` expectedEnvs

      context "when the envs are written in an unexpected way" $
        it "should throw an errorCall" $
          let unexpectedFormat text = T.length text == 3
              customMap = M.fromList
                [ ("twoLetters", unexpectedFormat)
                , ("bool", const True)
                , ("text", const True)
                , ("integer", const True)
                ]
              schemaFile = "spec/fixtures/.scheme.yml"
              config = Config ["spec/fixtures/.dotenv.safe"] [] False
           in void $ loadSafeFile customMap schemaFile config
                `shouldThrow` anyErrorCall


  describe "parseFile" $ after_ clearEnvs $ do
    it "returns variables from a file without changing the environment" $ do
      lookupEnv "DOTENV" `shouldReturn` Nothing

      liftM head (parseFile "spec/fixtures/.dotenv") `shouldReturn`
        ("DOTENV", "true")

      lookupEnv "DOTENV" `shouldReturn` Nothing

    it "recognizes unicode characters" $
      liftM (!! 1) (parseFile "spec/fixtures/.dotenv") `shouldReturn`
        ("UNICODE_TEST", "Manabí")

    it "recognises environment variables" $ do
      home <- fromMaybe "" <$> lookupEnv "HOME"
      liftM (!! 2) (parseFile "spec/fixtures/.dotenv") `shouldReturn`
        ("ENVIRONMENT", home)

    it "recognises previous variables" $
      liftM (!! 3) (parseFile "spec/fixtures/.dotenv") `shouldReturn`
        ("PREVIOUS", "true")

    it "recognises commands" $ do
      me <- init <$> readCreateProcess (shell "whoami") ""
      liftM (!! 4) (parseFile "spec/fixtures/.dotenv") `shouldReturn`
        ("ME", me)

  describe "onMissingFile" $ after_ clearEnvs $ do
    context "when target file is present" $
      it "loading works as usual" $ do
        void $ onMissingFile (loadFile $ Config ["spec/fixtures/.dotenv"] [] True) (return [])
        lookupEnv "DOTENV" `shouldReturn` Just "true"

    context "when target file is missing" $
      it "executes supplied handler instead" $
        onMissingFile (True <$ loadFile (Config ["spec/fixtures/foo"] [] True)) (return False)
          `shouldReturn` False

clearEnvs :: IO ()
clearEnvs =
  fmap (fmap fst) getEnvironment >>=  mapM_ unsetEnv

setupEnv :: IO ()
setupEnv = do
  setEnv "ANOTHER_ENV" "hello"
  setEnv "HOME" "/home/me"