packages feed

tmp-proc-example-0.5.0.0: specs/TmpProc/Example1/IntegrationTaste.hs

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

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

-}
module TmpProc.Example1.IntegrationTaste where

import           Test.Tasty
import           Test.Tasty.HUnit

import           Control.Exception              (onException)
import qualified Data.ByteString.Char8          as C8
import           Data.Either                    (isLeft)
import           Data.Maybe                     (isJust)
import           Data.Proxy                     (Proxy (..))
import           Database.Redis                 (parseConnectInfo)
import           Network.HTTP.Client            (newManager)
import           Network.HTTP.Client.TLS        (tlsManagerSettings)
import           Servant.Client                 (BaseUrl (..), ClientEnv,
                                                 Scheme (..), mkClientEnv,
                                                 runClientM)

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

import qualified TmpProc.Example1.Cache         as Cache
import qualified TmpProc.Example1.Client        as Client
import qualified TmpProc.Example1.Database      as DB
import           TmpProc.Example1.Schema        (Contact (..), ContactID)
import           TmpProc.Example1.Server        (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


main :: IO ()
main = defaultMain $ withResource mkFixture shutdown' tests


tests :: IO Fixture -> TestTree
tests getFixture = testGroup "Tmp.Proc:Demo of testing of DB/Cache server"
  [ testGroup "When the database is empty"
    [ testCase "Using the client to fetch a contact" $ do
        (_handle, client) <- getFixture
        fetched <- fmap isLeft $ runClientM (Client.fetch 1) client
        assertBool "should succeed" fetched

    , testCase "The contact should not be found in the DB" $ do
        (handle, _client) <- getFixture
        hasInDb handle 1 >>= assertEqual "contact in DB!" False

    , testCase "The contact should not be found in the cache" $ do
        (handle, _client) <- getFixture
        hasInCache handle 1 >>= assertEqual "contact in Cache!" False
    ],

    after AllFinish "empty" $ testCase "zz: the client should insert a contact" $ do
        (_handle, client) <- getFixture
        inserted <- fmap isLeft $ runClientM (Client.create testContact) client
        assertEqual "insert failed!" False inserted


  , after AllFinish "zz" $ testGroup "After the client is inserted"
    [ testCase "the contact should be found in the database" $ do
        (handle, _client) <- getFixture
        hasInDb handle 1 >>= assertEqual "contact not in DB!" True

    , testCase "yy: the contact should not be found in the cache" $ do
        (handle, _client) <- getFixture
        hasInCache handle 1 >>= assertEqual "contact in Cache!" False

    , after AllFinish "yy" $ testCase "and the client should fetch the contact" $ do
        (_handle, client) <- getFixture
        fetched <- fmap isLeft $ runClientM (Client.fetch 1) client
        assertEqual "notFetched" False fetched
    ]

  , after AllFinish "inserted" $ testGroup "After fetching the contact"
    [ testCase "the contact should be found in the cache" $ do
        (handle, _client) <- getFixture
        hasInCache handle 1 >>= assertEqual "contact in Cache!" True
    ]
  ]


hasInCache :: ServerHandle ('[TmpPostgres, TmpRedis]) -> ContactID -> IO Bool
hasInCache sh cid = do
  cacheLoc <- cacheLocFrom $ handleOf @TmpRedis Proxy $ handles sh
  fmap isJust $ Cache.loadContact cacheLoc cid


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 (ServerHandle ('[TmpPostgres, TmpRedis]), ClientEnv)
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
        cacheLoc <- cacheLocFrom redisH  `onException` terminateAll someHandles

        pure $ waiApp dbLoc cacheLoc

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


shutdown' :: (ServerHandle ('[TmpPostgres, TmpRedis]), ClientEnv) -> IO ()
shutdown' (sh, _) = shutdown sh


cacheLocFrom :: ProcHandle TmpRedis -> IO Cache.Locator
cacheLocFrom handle = case parseConnectInfo $ C8.unpack $ hUri handle of
  Left _  -> fail "Bad redis URI"
  Right x -> pure x


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