{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} module TestCookies where import Control.Monad.IO.Class (liftIO) import Data.Bifunctor (bimap) import Data.ByteString (ByteString) import Data.CaseInsensitive (mk) import Data.Map.Strict (Map) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8) import Network.HTTP.Client (method) import Network.Wai import Servant import Servant.API.Cookies import Servant.Server.Internal.Delayed (addAcceptCheck) import Servant.Server.Internal.DelayedIO (DelayedIO, delayedFailFatal, withRequest) import Test.QuickCheck.Monadic (PropertyM (..), assert, monadicIO) import Test.Tasty import TestLib (returns400, success) import Web.ClientSession import Web.Cookie import qualified Data.Map.Strict as Map import qualified Data.Vault.Lazy as Vault import qualified Network.HTTP.Simple as S import qualified Network.HTTP.Types.Header as NTH import qualified Test.Tasty.QuickCheck as QC data CheckTestCookie instance ( HasServer api ctx , HasContextEntry ctx HasCookies , HasContextEntry ctx (Vault.Key SessionMap) ) => HasServer (CheckTestCookie :> api) ctx where type ServerT (CheckTestCookie :> api) m = ServerT api m hoistServerWithContext _ ctx nt server = hoistServerWithContext (Proxy @api) ctx nt server route _ ctx server = route (Proxy @api) ctx $ server `addAcceptCheck` checkTestCookie where checkTestCookie :: DelayedIO () checkTestCookie = withRequest $ \req -> do let key = getContextEntry ctx :: Vault.Key SessionMap vlookup = Vault.lookup key (vault req) >>= Map.lookup "TEST_COOKIE" case vlookup of Just _ -> pure () Nothing -> delayedFailFatal $ err400 { errBody = "TEST_COOKIE cookie not set." } type CookieAPI = ProvideCookies '[Required] :> ( "add-cookie" :> Get '[JSON] (SetCookieHeader NoContent) :<|> "check-cookies" :> CheckTestCookie :> WithCookies '[Required] :> Get '[JSON] (Map Text Text) ) cookieServer :: Key -> Server CookieAPI cookieServer key = addCookie key :<|> showCookie where addCookie :: Key -> Handler (SetCookieHeader NoContent) addCookie keyArg = let sMap = Map.fromList [("TEST_COOKIE", "FOOBAR")] in liftIO $ updateCookies keyArg sMap defaultSetCookie "servant_cookie" NoContent showCookie :: SessionMap -> Handler (Map Text Text) showCookie sMap = do let kvs = Map.toList sMap pure $ Map.fromList $ fmap (bimap decodeUtf8 decodeUtf8) kvs cookieProps :: Key -> Int -> TestTree cookieProps encKey port = testGroup "Cookies" [ QC.testProperty "Fetching a non-existent cookie returns a 400" $ monadicIO $ do result <- (fetchCheckCookieEndpoint port Nothing) >>= returns400 assert $ result == True , QC.testProperty "Calling add-cookie returns a 200, and adds a Set-Cookie header" $ monadicIO $ do res1 <- (fetchCreateCookieEndpoint port) assert1 <- success res1 assert $ assert1 == True let setCookieStr = lookup NTH.hSetCookie $ S.getResponseHeaders res1 setCookieParsed = setCookieValue . parseSetCookie <$> setCookieStr sCookieVal = setCookieParsed >>= decrypt encKey assert $ sCookieVal == Just "TEST_COOKIE=FOOBAR" , QC.testProperty "Fetching the create-cookie, then check-cookie endpoints should work" $ monadicIO $ do res1 <- fetchCreateCookieEndpoint port assert1 <- success res1 assert $ assert1 == True encCookieVal <- liftIO $ encryptIO encKey "TEST_COOKIE=FOOBAR" let cookieHeader :: [NTH.Header] cookieHeader = [((mk "Cookie" :: NTH.HeaderName), encCookieVal)] res2 <- fetchCheckCookieEndpoint port $ Just cookieHeader assert2 <- success res2 assert $ assert2 == True ] where fetchCheckCookieEndpoint :: Int -> Maybe [NTH.Header] -> PropertyM IO (S.Response ByteString) fetchCheckCookieEndpoint port' mHeader = do let initReq = S.parseRequest_ $ "http://localhost:" <> (show port') <> "/check-cookies" headerList = maybe [] id mHeader foldFunction pair reqAcc = uncurry S.addRequestHeader pair reqAcc reqHeader = foldr foldFunction initReq headerList req = reqHeader {method = "GET"} in do resp <- S.httpBS req pure resp fetchCreateCookieEndpoint :: Int -> PropertyM IO (S.Response ByteString) fetchCreateCookieEndpoint port' = do let initReq = S.parseRequest_ $ "http://localhost:" <> (show port') <> "/add-cookie" req = initReq {method = "GET"} in do resp <- S.httpBS req pure resp