{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-redundant-constraints -fno-warn-unused-imports #-} module Test.Syd.Yesod.Client where import Control.Monad.Catch import Control.Monad.Fail import Control.Monad.Reader import Control.Monad.State import qualified Control.Monad.State as State import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy as LB import Data.Text (Text) import qualified Data.Text as T import GHC.Generics (Generic) import Network.HTTP.Client as HTTP import Network.HTTP.Types as HTTP import Network.Socket (PortNumber) import Test.Syd import Test.Syd.Wai.Client (lastRequestResponseContext) import Yesod.Core as Yesod -- | A client environment to call a Yesod app. data YesodClient site = YesodClient { -- | The site itself yesodClientSite :: !site, -- | The 'HTTP.Manager' to make the requests yesodClientManager :: !HTTP.Manager, -- | The port that the site is running on, using @warp@ yesodClientSitePort :: !PortNumber } deriving (Generic) -- | The state that is maintained throughout a 'YesodClientM' data YesodClientState = YesodClientState { -- | The last request and response pair yesodClientStateLast :: !(Maybe (Request, Response LB.ByteString)), -- | The cookies to pass along yesodClientStateCookies :: !CookieJar } deriving (Generic) -- | The starting point of the 'YesodClientState site' of a 'YesodClientM site' initYesodClientState :: YesodClientState initYesodClientState = YesodClientState { yesodClientStateLast = Nothing, yesodClientStateCookies = createCookieJar [] } -- | A monad to call a Yesod app. -- -- This has access to a 'YesodClient site'. newtype YesodClientM site a = YesodClientM { unYesodClientM :: StateT YesodClientState (ReaderT (YesodClient site) IO) a } deriving ( Functor, Applicative, Monad, MonadIO, MonadReader (YesodClient site), MonadState YesodClientState, MonadFail, MonadThrow ) -- | For backward compatibility type YesodExample site a = YesodClientM site a -- | Run a YesodClientM site using a YesodClient site runYesodClientM :: YesodClient site -> YesodClientM site a -> IO a runYesodClientM cenv (YesodClientM func) = runReaderT (evalStateT func initYesodClientState) cenv -- | Get the most recently sent request. getRequest :: YesodClientM site (Maybe Request) getRequest = State.gets (fmap fst . yesodClientStateLast) -- | Get the most recently received response. getResponse :: YesodClientM site (Maybe (Response LB.ByteString)) getResponse = State.gets (fmap snd . yesodClientStateLast) -- | Get the most recently sent request and the response to it. getLast :: YesodClientM site (Maybe (Request, Response LB.ByteString)) getLast = State.gets yesodClientStateLast -- | Get the 'Location' header of most recently received response. getLocation :: ParseRoute site => YesodClientM site (Either Text (Route site)) getLocation = do mr <- getResponse case mr of Nothing -> return $ Left "getLocation called, but there was no previous response, so no Location header" Just r -> case lookup "Location" (responseHeaders r) of Nothing -> return $ Left "getLocation called, but the previous response has no Location header" Just h -> case parseRoute $ decodePath' h of Nothing -> return $ Left $ "getLocation called, but couldn’t parse it into a route: " <> T.pack (show h) Just l -> return $ Right l where decodePath' :: ByteString -> ([Text], [(Text, Text)]) decodePath' b = let (ss, q) = decodePath $ extractPath b in (ss, map unJust $ queryToQueryText q) unJust (a, Just b) = (a, b) unJust (a, Nothing) = (a, mempty) -- | Annotate the given test code with the last request and its response, if one has been made already. withLastRequestContext :: YesodClientM site a -> YesodClientM site a withLastRequestContext yfunc@(YesodClientM func) = do mLast <- getLast case mLast of Nothing -> yfunc Just (req, resp) -> YesodClientM $ do s <- get c <- ask let ctx = lastRequestResponseContext req resp (r, s') <- liftIO $ context ctx $ runReaderT (runStateT func s) c put s' pure r