packages feed

tmp-proc-example-0.5.0.0: specs/TmpProc/Example2/IntegrationSpec.hs

{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE MonoLocalBinds    #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications  #-}
{-|
Copyright   : (c) 2020-2021 Tim Emiola
SPDX-License-Identifier: BSD3
Maintainer  : Tim Emiola <adetokunbo@users.noreply.github.com>

An demo @HSpec@ test that use @tmp-proc@

-}
module TmpProc.Example2.IntegrationSpec where

import           Test.Hspec
import           Test.Hspec.TmpProc             (AreProcs, HasHandle,
                                                 ServerHandle, handleOf,
                                                 handles, runServer, serverPort,
                                                 shutdown, tdescribe,
                                                 terminateAll, withConnOf, (&:))

import           Control.Exception              (onException)
import           Data.Either                    (isLeft)
import           Data.Maybe                     (isJust)
import           Data.Proxy                     (Proxy (..))
import           Network.HTTP.Client            (newManager)
import           Network.HTTP.Client.TLS        (tlsManagerSettings)
import           Servant.Client                 (BaseUrl (..), ClientEnv,
                                                 Scheme (..), mkClientEnv,
                                                 runClientM)

import           System.TmpProc.Docker.Postgres
import           System.TmpProc.Docker.Redis

import qualified TmpProc.Example2.Cache         as Cache
import qualified TmpProc.Example2.Client        as Client
import qualified TmpProc.Example2.Database      as DB
import           TmpProc.Example2.Schema        (Contact (..), ContactID)
import           TmpProc.Example2.Server        (AppEnv (..), waiApp)


{-| The test uses a Postgres database . -}
dbProc :: TmpPostgres
dbProc = TmpPostgres ["contacts"] -- 'reset' will empty the contacts table


{-| The test uses Redis as a cache. -}
cacheProc :: TmpRedis
cacheProc = TmpRedis []


{-| Specifies the procs to be launched as test fixtures.  -}
testProcs :: HList '[TmpPostgres, TmpRedis]
testProcs = dbProc &: cacheProc &: HNil


{-| Specifies the expected behaviour. -}
spec :: Spec
spec = tdescribe "Tmp.Proc:Demo of testing of DB/Cache server" $ do
  beforeAll mkFixture $ afterAll shutdown' $ do

    context "When the database is empty, using the client to fetch a contact" $ do

      it "should throw an error" $ \(_, clientEnv) ->
        (fmap isLeft $ runClientM (Client.fetch 1) clientEnv) `shouldReturn` True

      context "and the contact "$ do

        it "should not be found in the DB" $ \(sh, _) ->
          hasInDb sh 1 `shouldReturn` False

        it "should not be found in the cache" $ \(sh, _) -> do
          hasInCache sh 1 `shouldReturn` False


      context "and using the client to insert a contact" $ do

        it "should succeed" $ \(_, clientEnv) ->
          (fmap isLeft $ runClientM (Client.create testContact) clientEnv) `shouldReturn` False


    context "When the client is used to insert a contact" $ do

      context "then the contact "$ do

        it "should be found in the DB" $ \(sh, _) ->
          hasInDb sh 1 `shouldReturn` True

        it "should not be found in the cache" $ \(sh, _) -> do
          hasInCache sh 1 `shouldReturn` False

      context "and using the client to fetch the contact" $ do

        it "should succeed" $ \(_, clientEnv) ->
          (fmap isLeft $ runClientM (Client.fetch 1) clientEnv) `shouldReturn` False

    context "After fetching the contact with the client" $ do

      context "then the contact "$ do

        it "should be found in the cache" $ \(sh, _) -> do
          hasInCache sh 1 `shouldReturn` True


{-| Simplifies the test cases

Note the use of the 'HasHandle' constraint to indicate what TmpProcs the function uses.

-}
hasInCache :: HasHandle TmpRedis procs => ServerHandle procs -> ContactID -> IO Bool
hasInCache sh cid = withConnOf @TmpRedis Proxy (handles sh) $ \cache ->
  fmap isJust $ Cache.loadContact cache cid


{-| Simplifies the test cases

Here, ServerHandle specifies the full list of types required by the calling test code.

-}
hasInDb :: ServerHandle ('[TmpPostgres, TmpRedis]) -> ContactID -> IO Bool
hasInDb sh cid = do
  let dbUriOf = hUri . handleOf @"a-postgres-db" Proxy . handles
  fmap isJust $ flip DB.fetch cid $ dbUriOf sh


{-| The full test fixture.

It allows tests to

- use the servant client to invoke the backend
- check the state of service backends via the @ProcHandles@ in the 'ServerHandle'.

-}
type Fixture = (ServerHandle ('[TmpPostgres, TmpRedis]), ClientEnv)


mkFixture :: IO Fixture
mkFixture = do
  let mkApp someHandles = do

        -- handleOf can obtain a handle using either the Proc type ...
        let redisH = handleOf @TmpRedis Proxy someHandles

            -- or the Name of it's Proc type
            dbLoc  = hUri $ handleOf @"a-postgres-db" Proxy someHandles

        -- Create the database schema
        DB.migrateDB dbLoc `onException` terminateAll someHandles

        -- Determine the redis location
        cache <- openConn redisH  `onException` terminateAll someHandles

        pure $ waiApp $ AppEnv dbLoc cache

  sh <- runServer testProcs mkApp
  clientEnv <- clientEnvOf sh
  pure (sh, clientEnv)


shutdown' :: Fixture -> IO ()
shutdown' (sh, _) = shutdown sh


clientEnvOf :: AreProcs procs => ServerHandle procs -> IO ClientEnv
clientEnvOf s = do
  mgr <- newManager tlsManagerSettings
  pure $ mkClientEnv mgr $ BaseUrl Http "localhost" (serverPort s) ""


testContact :: Contact
testContact = Contact
  { contactName = "Bond"
  , contactEmail = "james@hmss.gov.uk"
  , contactAge = 45
  , contactTitle = "Mr"
  }