servant-client-0.20.1: test/Servant/MiddlewareSpec.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -freduction-depth=100 #-}
module Servant.MiddlewareSpec (spec) where
import Control.Arrow
( left,
)
import Control.Concurrent (newEmptyMVar, putMVar, takeMVar)
import Control.Exception (Exception, throwIO, try)
import Control.Monad.IO.Class
import Data.ByteString.Builder (toLazyByteString)
import Data.IORef (modifyIORef, newIORef, readIORef)
import Data.Monoid ()
import Prelude.Compat
import Servant.Client
import Servant.Client.Core (RequestF (..))
import Servant.Client.Internal.HttpClient (ClientMiddleware)
import Servant.ClientTestUtils
import Test.Hspec
import Prelude ()
runClientWithMiddleware :: ClientM a -> ClientMiddleware -> BaseUrl -> IO (Either ClientError a)
runClientWithMiddleware x mid baseUrl' =
runClientM x ((mkClientEnv manager' baseUrl') {middleware = mid})
data CustomException = CustomException deriving (Show, Eq)
instance Exception CustomException
spec :: Spec
spec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
it "Raw request and response can be accessed in middleware" $ \(_, baseUrl) -> do
mvarReq <- newEmptyMVar
mvarResp <- newEmptyMVar
let mid :: ClientMiddleware
mid oldApp req = do
-- "Log" request
liftIO $ putMVar mvarReq req
-- perform request
resp <- oldApp req
-- "Log" response
liftIO $ putMVar mvarResp resp
pure resp
-- Same as without middleware
left show <$> runClientWithMiddleware getGet mid baseUrl `shouldReturn` Right alice
-- Access some raw request data
req <- takeMVar mvarReq
toLazyByteString (requestPath req) `shouldBe` "/get"
-- Access some raw response data
resp <- takeMVar mvarResp
responseBody resp `shouldBe` "{\"_age\":42,\"_name\":\"Alice\"}"
it "errors can be thrown in middleware" $ \(_, baseUrl) -> do
let mid :: ClientMiddleware
mid oldApp req = do
-- perform request
resp <- oldApp req
-- throw error
_ <- liftIO $ throwIO CustomException
pure resp
try (runClientWithMiddleware getGet mid baseUrl) `shouldReturn` Left CustomException
it "runs in the expected order" $ \(_, baseUrl) -> do
ref <- newIORef []
let mid1 :: ClientMiddleware
mid1 oldApp req = do
liftIO $ modifyIORef ref (\xs -> xs <> ["req1"])
resp <- oldApp req
liftIO $ modifyIORef ref (\xs -> xs <> ["resp1"])
pure resp
let mid2 :: ClientMiddleware
mid2 oldApp req = do
liftIO $ modifyIORef ref (\xs -> xs <> ["req2"])
resp <- oldApp req
liftIO $ modifyIORef ref (\xs -> xs <> ["resp2"])
pure resp
let mid3 :: ClientMiddleware
mid3 oldApp req = do
liftIO $ modifyIORef ref (\xs -> xs <> ["req3"])
resp <- oldApp req
liftIO $ modifyIORef ref (\xs -> xs <> ["resp3"])
pure resp
let mid :: ClientMiddleware
mid = mid1 . mid2 . mid3
-- \^ Composition in "reverse order".
-- It is equivalent to the following, which is more intuitive:
-- mid :: ClientMiddleware
-- mid oldApp = mid1 (mid2 (mid3 oldApp))
-- Same as without middleware
left show <$> runClientWithMiddleware getGet mid baseUrl `shouldReturn` Right alice
ref <- readIORef ref
ref `shouldBe` ["req1", "req2", "req3", "resp3", "resp2", "resp1"]