cloud-seeder-0.1.0.0: test-suite/Network/CloudSeeder/MainSpec.hs
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Network.CloudSeeder.MainSpec (spec) where
import Control.Lens (review)
import Control.Monad.Except (ExceptT, runExceptT)
import Control.Monad.Mock (MockT, WithResult(..), runMockT)
import Control.Monad.Mock.TH (makeAction, ts)
import Data.Function ((&))
import Data.Functor.Identity (runIdentity)
import Data.Semigroup ((<>))
import GHC.Exts (IsList(..))
import Test.Hspec
import Network.CloudSeeder.DSL
import Network.CloudSeeder.Interfaces
import Network.CloudSeeder.Main
import Network.CloudSeeder.Test.Stubs
import qualified Data.Text as T
makeAction "CloudAction" [ts| MonadCloud |]
mockCloudT :: Monad m => [WithResult CloudAction] -> MockT CloudAction m a -> m a
mockCloudT = runMockT
type TagList = forall a. (IsList a, Item a ~ (T.Text, T.Text)) => a
spec :: Spec
spec =
describe "cli" $ do
let rootTemplate = "Parameters:\n"
<> " Env:\n"
<> " Type: String\n"
rootExpectedTags = [("cj:application", "foo"), ("cj:environment", "test")]
rootParams = [("Env", "test")]
serverTestArgs = ["provision", "server", "test"]
baseTestArgs = ["provision", "base", "test"]
let stubExceptT :: ExceptT CliError m a -> m (Either CliError a)
stubExceptT = runExceptT
runSuccess x = runIdentity x `shouldBe` Right ()
runFailure p y x = runIdentity x `shouldBe` Left (review p y)
it "fails if the template doesn't exist" $ do
let config = deployment "foo" $ do
stack_ "base"
stack_ "server"
runFailure _FileNotFound "server.yaml" $ cli config
& stubFileSystemT []
& stubEnvironmentT []
& stubCommandLineT serverTestArgs
& stubExceptT
& mockCloudT []
it "fails if the template parameters can't be parsed" $ do
let config = deployment "foo" $ stack_ "base"
err = "YAML parse exception at line 0, column 8,\nwhile scanning a directive:\nfound unknown directive name"
runFailure _CliTemplateDecodeFail err $ cli config
& stubFileSystemT [("base.yaml", "%invalid")]
& stubEnvironmentT []
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT []
it "fails if user attempts to deploy a stack that doesn't exist in the config" $ do
let config = deployment "foo" $ stack_ "base"
fakeCliInput = ["provision", "foo", "test"]
runFailure _CliStackNotConfigured "foo" $ cli config
& stubFileSystemT
[ ("base.yaml", rootTemplate)]
& stubEnvironmentT []
& stubCommandLineT fakeCliInput
& stubExceptT
& mockCloudT []
it "fails if parameters required in the template are not supplied" $ do
let baseTemplate =
rootTemplate
<> " foo:\n"
<> " Type: String\n"
<> " bar:\n"
<> " Type: String\n"
config = deployment "foo" $ do
stack_ "base"
runFailure _CliMissingRequiredParameters ["foo", "bar"] $ cli config
& stubFileSystemT [ ("base.yaml", baseTemplate) ]
& stubEnvironmentT []
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT []
context "the configuration does not have environment variables" $ do
let config = deployment "foo" $ do
stack_ "base"
stack_ "server"
stack_ "frontend"
it "applies a changeset to a stack" $ example $
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", rootTemplate) ]
& stubEnvironmentT []
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT
[ ComputeChangeset "test-foo-base" rootTemplate rootParams rootExpectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
it "passes only the outputs from previous stacks that are listed in this template's Parameters" $ do
let serverTemplate = rootTemplate
<> " foo:\n"
<> " Type: String\n"
<> " bar:\n"
<> " Type: String\n"
baseOutputs = [ ("first", "output")
, ("foo", "baz")
, ("bar", "qux")
, ("last", "output") ]
runSuccess $ cli config
& stubFileSystemT
[ ("server.yaml", serverTemplate) ]
& stubEnvironmentT []
& stubCommandLineT serverTestArgs
& stubExceptT
& mockCloudT
[ GetStackOutputs "test-foo-base" :-> Just baseOutputs
, ComputeChangeset
"test-foo-server"
serverTemplate
(rootParams <> [("bar", "qux"), ("foo", "baz")])
rootExpectedTags
:-> "csid"
, RunChangeSet "csid" :-> () ]
let frontendtemplate = rootTemplate
<> " foo:\n"
<> " Type: String\n"
runSuccess $ cli config
& stubFileSystemT
[ ("frontend.yaml", frontendtemplate) ]
& stubEnvironmentT []
& stubCommandLineT ["provision", "frontend", "test"]
& stubExceptT
& mockCloudT
[ GetStackOutputs "test-foo-base" :-> Just baseOutputs
, GetStackOutputs "test-foo-server" :-> Just []
, ComputeChangeset
"test-foo-frontend"
frontendtemplate
(rootParams <> [("foo", "baz")])
rootExpectedTags
:-> "csid"
, RunChangeSet "csid" :-> () ]
it "fails if a dependency stack does not exist" $ do
runFailure _CliMissingDependencyStacks ["base"] $ cli config
& stubFileSystemT
[ ("frontend.yaml", rootTemplate) ]
& stubEnvironmentT []
& stubCommandLineT ["provision", "frontend", "test"]
& stubExceptT
& mockCloudT
[ GetStackOutputs "test-foo-base" :-> Nothing
, GetStackOutputs "test-foo-server" :-> Just [] ]
runFailure _CliMissingDependencyStacks ["server"] $ cli config
& stubFileSystemT
[ ("frontend.yaml", rootTemplate) ]
& stubEnvironmentT []
& stubCommandLineT ["provision", "frontend", "test"]
& stubExceptT
& mockCloudT
[ GetStackOutputs "test-foo-base" :-> Just []
, GetStackOutputs "test-foo-server" :-> Nothing ]
runFailure _CliMissingDependencyStacks ["base", "server"] $ cli config
& stubFileSystemT
[ ("frontend.yaml", rootTemplate) ]
& stubEnvironmentT []
& stubCommandLineT ["provision", "frontend", "test"]
& stubExceptT
& mockCloudT
[ GetStackOutputs "test-foo-base" :-> Nothing
, GetStackOutputs "test-foo-server" :-> Nothing ]
context "the configuration has global environment variables" $ do
let config = deployment "foo" $ do
environment ["Domain", "SecretsStore"]
stack_ "base"
stack_ "server"
stack_ "frontend"
it "passes the value in each global environment variable as a parameter" $ do
let template = rootTemplate
<> " Domain:\n"
<> " Type: String\n"
<> " SecretsStore:\n"
<> " Type: String\n"
let env = rootParams <> [ ("Domain", "example.com"), ("SecretsStore", "arn::aws:1234") ]
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", template) ]
& stubEnvironmentT env
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT
[ ComputeChangeset "test-foo-base" template env rootExpectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
it "fails when a global environment variable is missing" $ do
let env = [ ("Env", "test"), ("SecretsStore", "arn::aws:1234") ]
runFailure _CliMissingEnvVars ["Domain"] $ cli config
& stubFileSystemT
[ ("base.yaml", rootTemplate) ]
& stubEnvironmentT env
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT []
it "reports all missing environment variables at once in alphabetical order" $
runFailure _CliMissingEnvVars ["Domain", "SecretsStore"] $ cli config
& stubFileSystemT
[ ("base.yaml", rootTemplate) ]
& stubEnvironmentT []
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT []
context "the configuration has global and local environment variables" $ do
let config = deployment "foo" $ do
environment ["Domain", "SecretsStore"]
stack "base" $ environment ["Base"]
stack "server" $ environment ["Server1", "Server2"]
stack "frontend" $ environment ["Frontend"]
let template = rootTemplate
<> " Domain:\n"
<> " Type: String\n"
<> " SecretsStore:\n"
<> " Type: String\n"
it "passes the value in each local environment variable to the proper stack" $ do
let env = [ ("Domain", "example.com"), ("Env", "test"), ("SecretsStore", "arn::aws:1234") ]
baseEnv = env <> [ ("Base", "a") ]
baseTemplate = template
<> " Base:\n"
<> " Type: String\n"
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", baseTemplate) ]
& stubEnvironmentT baseEnv
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT
[ ComputeChangeset "test-foo-base" baseTemplate baseEnv rootExpectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
let serverEnv = env <> [ ("Server1", "b"), ("Server2", "c") ]
serverTemplate = template
<> " Server1:\n"
<> " Type: String\n"
<> " Server2:\n"
<> " Type: String\n"
runSuccess $ cli config
& stubFileSystemT
[ ("server.yaml", serverTemplate) ]
& stubEnvironmentT serverEnv
& stubCommandLineT serverTestArgs
& stubExceptT
& mockCloudT
[ GetStackOutputs "test-foo-base" :-> Just []
, ComputeChangeset "test-foo-server" serverTemplate serverEnv rootExpectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
context "the configuration has global tags" $ do
let globalTags :: TagList
globalTags = [("cj:squad", "lambda"), ("taggo", "oggat")]
config = deployment "foo" $ do
tags globalTags
stack_ "base"
stack_ "server"
stack_ "frontend"
it "passes the value in each tag" $ do
let expectedTags = rootExpectedTags <> globalTags
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", rootTemplate) ]
& stubEnvironmentT []
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT
[ ComputeChangeset "test-foo-base" rootTemplate rootParams expectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
context "the configuration has global and local tags" $ do
let globalTags :: TagList
globalTags = [("cj:squad", "lambda"), ("taggo", "oggat")]
serverTags :: TagList
serverTags = [("x", "z")]
expectedGlobalTags = rootExpectedTags <> globalTags
expectedServerTags = expectedGlobalTags <> serverTags
config = deployment "foo" $ do
tags globalTags
stack_ "base"
stack "server" $ tags serverTags
stack "frontend" $ tags [("frontendTag1", "ft1"), ("frontendTag2", "ft2")]
it "passes the value in each tag to the proper stack" $ do
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", rootTemplate) ]
& stubEnvironmentT []
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT
[ ComputeChangeset "test-foo-base" rootTemplate rootParams expectedGlobalTags :-> "csid"
, RunChangeSet "csid" :-> () ]
runSuccess $ cli config
& stubFileSystemT
[ ("server.yaml", rootTemplate) ]
& stubEnvironmentT []
& stubCommandLineT serverTestArgs
& stubExceptT
& mockCloudT
[ GetStackOutputs "test-foo-base" :-> Just []
, ComputeChangeset "test-foo-server" rootTemplate rootParams expectedServerTags :-> "csid"
, RunChangeSet "csid" :-> () ]
context "monadic logic" $
context "can make decisions based on the env passed in" $ do
let template = "Parameters:\n"
<> " Env:\n"
<> " Type: String\n"
<> " foo:\n"
<> " Type: String\n"
<> " baz:\n"
<> " Type: String\n"
<> " Default: prod"
let config = deployment "foo" $ do
param "foo" "bar"
whenEnv "prod" $ param "baz" "qux"
stack_ "base"
expectedParams = [("Env", "prod"), ("baz", "qux"), ("foo", "bar")]
expectedTags = [("cj:application","foo"),("cj:environment","prod")]
expectedParamsTest = [("foo", "bar")] <> rootParams
it "can add a param based on prod" $
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", template) ]
& stubEnvironmentT []
& stubCommandLineT ["provision", "base", "prod"]
& stubExceptT
& mockCloudT
[ ComputeChangeset "prod-foo-base" template expectedParams expectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
it "does not provide prod-only params when not in prod" $
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", template) ]
& stubEnvironmentT []
& stubCommandLineT baseTestArgs
& stubExceptT
& mockCloudT
[ ComputeChangeset "test-foo-base" template expectedParamsTest rootExpectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
context "flags" $ do
let template = "Parameters:\n"
<> " Env:\n"
<> " Type: String\n"
<> " baz:\n"
<> " Type: String\n"
<> " Default: prod"
let config = deployment "foo" $ do
flag "baz"
stack_ "base"
it "accepts optional flags where the config calls for them" $
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", template) ]
& stubEnvironmentT []
& stubCommandLineT ["provision", "base", "test", "--baz", "zab"]
& stubExceptT
& mockCloudT
[ ComputeChangeset "test-foo-base" template (rootParams <> [("baz", "zab")]) rootExpectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
it "provides a default if an optional flag isn't provided" $
runSuccess $ cli config
& stubFileSystemT
[ ("base.yaml", template) ]
& stubEnvironmentT []
& stubCommandLineT ["provision", "base", "test"]
& stubExceptT
& mockCloudT
[ ComputeChangeset "test-foo-base" template (rootParams <> [("baz", "prod")]) rootExpectedTags :-> "csid"
, RunChangeSet "csid" :-> () ]
it "raises an error if a flag is provided that does not exist in the template" $
let config' = deployment "foo" $ do
mapM_ flag (["foo", "bar", "baz"] :: [T.Text])
stack_ "base"
in runFailure _CliExtraParameterFlags ["foo", "bar"] $ cli config'
& stubFileSystemT
[ ("base.yaml", template) ]
& stubEnvironmentT []
& stubCommandLineT
["provision", "base", "test", "--foo", "oof", "--bar", "rab", "--baz", "zab"]
& stubExceptT
& mockCloudT []