packages feed

tmp-proc-example-0.5.3.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 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 Test.Hspec
import Test.Hspec.TmpProc
  ( AreProcs
  , HasHandle
  , ServerHandle
  , handleOf
  , handles
  , runServer
  , serverPort
  , shutdown
  , tdescribe
  , terminateAll
  , withConnOf
  , (&:&)
  )
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


-- | 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"
    }