{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} module BootstrapSpec where import Test.Hspec import Control.Concurrent (newEmptyMVar, putMVar, takeMVar) import Control.Monad.Catch (fromException, throwM) import Control.Monad.IO.Class (liftIO) import Control.Monad.Reader (ReaderT (..)) import Control.Monad.Writer.Lazy (runWriterT) 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 (Right result, _) <- liftIO $ runWriterT $ 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 (Right result, _) <- liftIO $ runWriterT $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 (Right result, _) <- liftIO $ runWriterT $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 (Right result, _) <- runWriterT $ 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 (Right result, _) <- runWriterT $ 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 (Right textResult, _) <- liftIO $ runWriterT $ runReaderT readerT (defaultRequest {pathInfo = ["foo"]}) (Right htmlResult, _) <- liftIO $ runWriterT $ 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 (Right textResult, _) <- liftIO $ runWriterT $ runReaderT readerT (defaultRequest {pathInfo = ["foo"], requestHeaders = [(hAccept, "text/plain; q=1.0, text/html; q=0.9")]}) (Right htmlResult, _) <- liftIO $ runWriterT $ 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 (Right textResult, _) <- liftIO $ runWriterT $ 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 (Right htmlResult, _) <- liftIO $ runWriterT $ 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) it "catches unhandled exception as Left" $ do let error = TestException "test" let readerT = bootstrap @TextPlain (lift (throwM error :: IO Int)) & compile (Left e, _) <- liftIO $ runWriterT $runReaderT readerT defaultRequest fromException e `shouldBe` Just error