packages feed

sydtest-wai-0.2.0.0: src/Test/Syd/Wai/Client.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-unused-imports #-}

module Test.Syd.Wai.Client where

import Control.Monad.Fail
import Control.Monad.Reader
import Control.Monad.State as State
import qualified Data.ByteString.Lazy as LB
import GHC.Generics (Generic)
import Network.HTTP.Client as HTTP
import Network.Socket (PortNumber)
import Test.Syd

-- | A client environment for a 'Wai.Application' with a user-defined environment as well
data WaiClient env = WaiClient
  { -- The 'HTTP.Manager' tto make the requests
    waiClientManager :: !HTTP.Manager,
    -- | The user-defined environment
    waiClientEnv :: !env,
    -- The port that the application is running on, using @warp@
    waiClientPort :: !PortNumber
  }
  deriving (Generic)

data WaiClientState = WaiClientState
  { -- | The last request and response pair
    waiClientStateLast :: !(Maybe (HTTP.Request, HTTP.Response LB.ByteString)),
    -- | The cookies to pass along
    waiClientStateCookies :: !CookieJar
  }
  deriving (Generic)

initWaiClientState :: WaiClientState
initWaiClientState =
  WaiClientState
    { waiClientStateLast = Nothing,
      waiClientStateCookies = createCookieJar []
    }

-- | A Wai testing monad that carries client state, information about how to call the application,
-- a user-defined environment, and everything necessary to show nice error messages.
newtype WaiClientM env a = WaiClientM
  { unWaiClientM :: StateT WaiClientState (ReaderT (WaiClient env) IO) a
  }
  deriving
    ( Functor,
      Applicative,
      Monad,
      MonadIO,
      MonadReader (WaiClient env),
      MonadState WaiClientState,
      MonadFail
    )

instance IsTest (WaiClientM env ()) where
  type Arg1 (WaiClientM env ()) = ()
  type Arg2 (WaiClientM env ()) = WaiClient env
  runTest func = runTest (\() -> func)

instance IsTest (outerArgs -> WaiClientM env ()) where
  type Arg1 (outerArgs -> WaiClientM env ()) = outerArgs
  type Arg2 (outerArgs -> WaiClientM env ()) = WaiClient env
  runTest func = runTest (\outerArgs waiClient -> runWaiClientM waiClient (func outerArgs))

-- | For compatibility with @hspec-wai@
type WaiSession st a = WaiClientM st a

-- | For compatibility with @hspec-wai@
type WaiExpectation st = WaiSession st ()

-- | Run a WaiClientM env using a WaiClient env
runWaiClientM :: WaiClient env -> WaiClientM env a -> IO a
runWaiClientM cenv (WaiClientM func) = runReaderT (evalStateT func initWaiClientState) cenv

-- | Get the most recently sent request.
getRequest :: WaiClientM env (Maybe HTTP.Request)
getRequest = State.gets (fmap fst . waiClientStateLast)

-- | Get the most recently received response.
getResponse :: WaiClientM env (Maybe (HTTP.Response LB.ByteString))
getResponse = State.gets (fmap snd . waiClientStateLast)

-- | Get the most recently sent request and the response to it.
getLast :: WaiClientM env (Maybe (HTTP.Request, HTTP.Response LB.ByteString))
getLast = State.gets waiClientStateLast

-- | Annotate the given test code with the last request and its response, if one has been made already.
withLastRequestContext :: WaiClientM site a -> WaiClientM site a
withLastRequestContext wfunc@(WaiClientM func) = do
  mLast <- getLast
  case mLast of
    Nothing -> wfunc
    Just (req, resp) ->
      WaiClientM $ do
        s <- get
        c <- ask
        let ctx = lastRequestResponseContext req resp
        (r, s') <- liftIO $ context ctx $ runReaderT (runStateT func s) c
        put s'
        pure r

-- | An assertion context, for 'Context', that shows the last request and response
lastRequestResponseContext :: Show respBody => HTTP.Request -> HTTP.Response respBody -> String
lastRequestResponseContext req resp =
  unlines
    [ "last request:",
      ppShow req,
      "full response:",
      ppShow resp
    ]