linnet-0.3.0.0: test/BootstrapSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
module BootstrapSpec where
import Test.Hspec
import Control.Concurrent (newEmptyMVar, putMVar, takeMVar)
import Control.Monad.Catch (throwM)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (ReaderT (..))
import qualified Data.CaseInsensitive as CI
import Data.Function ((&))
import Data.Functor.Identity (runIdentity)
import qualified Data.Text as T
import Instances
import Linnet
import Linnet.Bootstrap
import Linnet.Endpoint
import Linnet.Errors
import Linnet.Internal.Coproduct ((:+:), CNil)
import Linnet.Output
import Linnet.ToResponse (NotAcceptable406, toResponse)
import Network.HTTP.Types (hAccept, hContentType, methodPost,
status400, status404, status405,
status406)
import Network.HTTP.Types.Header (hAllow)
import Network.Wai (defaultRequest, pathInfo,
requestHeaders, responseHeaders,
responseStatus)
import Network.Wai.Internal (ResponseReceived (..))
import Test.QuickCheck (property)
import Test.QuickCheck.Monadic (assert, monadicIO)
spec :: Spec
spec = do
it "compiles endpoint to Kleisli" $
property $ \(out :: (Output T.Text)) ->
monadicIO $ do
let readerT = bootstrap @TextPlain (liftOutputM (return out)) & compile
result <- liftIO $ runReaderT readerT defaultRequest
assert (result == outputToResponse (toResponse @TextPlain) (toResponse @TextPlain) (toResponse @TextPlain) out)
it "responds with corresponding content-type" $
property $ \(out :: (Output T.Text)) ->
monadicIO $ do
let readerT = bootstrap @TextPlain (liftOutputM (return out)) & compile
result <- liftIO $ runReaderT readerT defaultRequest
let maybeContentType = lookup (CI.mk "Content-Type") (responseHeaders result)
assert (maybeContentType == Just "text/plain")
it "responds with 404" $ do
let readerT = bootstrap @TextPlain (get (p' "foo") ~>> (return . ok $ ("text" :: T.Text))) & compile
result <- liftIO $ runReaderT readerT defaultRequest
responseStatus result `shouldBe` status404
it "responds with 400 on LinnetError" $ do
let endpoint = liftOutputM (throwM $ DecodeError "oops") :: Endpoint IO T.Text
let readerT = bootstrap @TextPlain endpoint & compile
result <- runReaderT readerT defaultRequest
responseStatus result `shouldBe` status400
it "responds with 405 on method mismatch" $ do
let readerT = bootstrap @TextPlain (post (p' "foo") ~>> (return . ok $ ("text" :: T.Text))) & compile
result <- runReaderT readerT defaultRequest {pathInfo = ["foo"]}
responseStatus result `shouldBe` status405
responseHeaders result `shouldBe` [(hAllow, methodPost)]
it "serves different content-types" $
property $ \(out :: (Output T.Text)) ->
monadicIO $ do
let text = get (p' "foo") ~>> return out
let html = get (p' "bar") ~>> return out
let readerT = bootstrap @TextPlain text & serve @TextHtml html & compile
textResult <- liftIO $ runReaderT readerT (defaultRequest {pathInfo = ["foo"]})
htmlResult <- liftIO $ runReaderT readerT (defaultRequest {pathInfo = ["bar"]})
let maybeTextContentType = lookup hContentType (responseHeaders textResult)
let maybeHtmlContentType = lookup hContentType (responseHeaders htmlResult)
assert (maybeTextContentType == Just "text/plain")
assert (maybeHtmlContentType == Just "text/html")
it "negotiates content-type" $
property $ \(out :: (Output T.Text)) ->
monadicIO $ do
let text = get (p' "foo") ~>> return out
let readerT = bootstrap @(TextPlain :+: TextHtml :+: NotAcceptable406) text & compile
textResult <-
liftIO $
runReaderT
readerT
(defaultRequest {pathInfo = ["foo"], requestHeaders = [(hAccept, "text/plain; q=1.0, text/html; q=0.9")]})
htmlResult <-
liftIO $
runReaderT
readerT
(defaultRequest {pathInfo = ["foo"], requestHeaders = [(hAccept, "text/plain; q=0.9, text/html; q=1.0")]})
let maybeTextContentType = lookup hContentType (responseHeaders textResult)
let maybeHtmlContentType = lookup hContentType (responseHeaders htmlResult)
assert (maybeTextContentType == Just "text/plain")
assert (maybeHtmlContentType == Just "text/html")
it "returns 406 on failed negotiation" $
property $ \(out :: (Output T.Text)) ->
monadicIO $ do
let text = get (p' "foo") ~>> return out
let readerT = bootstrap @(TextPlain :+: TextHtml :+: NotAcceptable406) text & compile
textResult <-
liftIO $
runReaderT readerT (defaultRequest {pathInfo = ["foo"], requestHeaders = [(hAccept, "application/json")]})
assert (responseStatus textResult == status406)
it "falls back to the latest option when 406 is disabled" $
property $ \(out :: (Output T.Text)) ->
monadicIO $ do
let text = get (p' "foo") ~>> return out
let readerT = bootstrap @(TextPlain :+: TextHtml) text & compile
htmlResult <-
liftIO $
runReaderT readerT (defaultRequest {pathInfo = ["foo"], requestHeaders = [(hAccept, "application/json")]})
let maybeHtmlContentType = lookup hContentType (responseHeaders htmlResult)
assert (maybeHtmlContentType == Just "text/html")
it "compiles into WAI application" $
property $ \(out :: (Output T.Text)) ->
monadicIO $ do
let app = bootstrap @TextPlain (liftOutputM (return out)) & compile & toApp @IO
mvar <- liftIO newEmptyMVar
let callback req = ResponseReceived <$ putMVar mvar req
_ <- liftIO $ app defaultRequest callback
response <- liftIO $ takeMVar mvar
assert
(response == outputToResponse (toResponse @TextPlain) (toResponse @TextPlain) (toResponse @TextPlain) out)