{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -freduction-depth=100 #-} module Servant.ServerSpec where import Prelude () import Prelude.Compat import Control.Monad (forM_, join, unless, when) import Control.Monad.Error.Class (MonadError (..)) import Data.Aeson (FromJSON, ToJSON, decode', encode) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Base64 as Base64 import Data.Char (toUpper) import Data.Maybe (fromMaybe) import Data.Proxy (Proxy (Proxy)) import Data.String (fromString) import Data.String.Conversions (cs) import qualified Data.Text as T import GHC.Generics (Generic) import Network.HTTP.Types (QueryItem, Status (..), hAccept, hContentType, imATeapot418, methodDelete, methodGet, methodHead, methodPatch, methodPost, methodPut, ok200, parseQuery) import Network.Wai (Application, Middleware, Request, pathInfo, queryString, rawQueryString, requestHeaders, responseLBS) import Network.Wai.Test (defaultRequest, request, runSession, simpleBody, simpleHeaders, simpleStatus) import Servant.API ((:<|>) (..), (:>), AuthProtect, BasicAuth, BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll, DeepQuery, Delete, EmptyAPI, Fragment, Get, HasStatus (StatusOf), Header, Headers, HttpVersion, IsSecure (..), JSON, Lenient, NoContent (..), NoContentVerb, NoFraming, OctetStream, Patch, PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, QueryString, Raw, RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Strict, UVerb, Union, Verb, WithStatus (..), addHeader) import Servant.Server (Context ((:.), EmptyContext), FromDeepQuery (..), Handler, Server, Tagged (..), emptyServer, err401, err403, err404, respond, serve, serveWithContext) import Servant.Test.ComprehensiveAPI import qualified Servant.Types.SourceT as S import Test.Hspec (Spec, context, describe, it, shouldBe, shouldContain) import Test.Hspec.Wai (get, liftIO, matchHeaders, matchStatus, shouldRespondWith, with, (<:>)) import qualified Test.Hspec.Wai as THW import Text.Read (readMaybe) import Servant.Server.Experimental.Auth (AuthHandler, AuthServerData, mkAuthHandler) import Servant.Server.Internal.BasicAuth (BasicAuthCheck (BasicAuthCheck), BasicAuthResult (Authorized, Unauthorized)) import Servant.Server.Internal.Context (NamedContext (..)) -- * comprehensive api test -- This declaration simply checks that all instances are in place. _ = serveWithContext comprehensiveAPI comprehensiveApiContext comprehensiveApiContext :: Context '[NamedContext "foo" '[]] comprehensiveApiContext = NamedContext EmptyContext :. EmptyContext -- * Specs spec :: Spec spec = do verbSpec uverbSpec captureSpec captureAllSpec queryParamSpec fragmentSpec reqBodySpec headerSpec rawSpec alternativeSpec responseHeadersSpec uverbResponseHeadersSpec miscCombinatorSpec basicAuthSpec genAuthSpec ------------------------------------------------------------------------------ -- * verbSpec {{{ ------------------------------------------------------------------------------ type VerbApi method status = Verb method status '[JSON] Person :<|> "noContent" :> NoContentVerb method :<|> "header" :> Verb method status '[JSON] (Headers '[Header "H" Int] Person) :<|> "headerNC" :> Verb method status '[JSON] (Headers '[Header "H" Int] NoContent) :<|> "accept" :> ( Verb method status '[JSON] Person :<|> Verb method status '[PlainText] String ) :<|> "stream" :> Stream method status NoFraming OctetStream (SourceIO BS.ByteString) verbSpec :: Spec verbSpec = describe "Servant.API.Verb" $ do let server :: Server (VerbApi method status) server = return alice :<|> return NoContent :<|> return (addHeader 5 alice) :<|> return (addHeader 10 NoContent) :<|> (return alice :<|> return "B") :<|> return (S.source ["bytestring"]) get200 = Proxy :: Proxy (VerbApi 'GET 200) post210 = Proxy :: Proxy (VerbApi 'POST 210) put203 = Proxy :: Proxy (VerbApi 'PUT 203) delete280 = Proxy :: Proxy (VerbApi 'DELETE 280) patch214 = Proxy :: Proxy (VerbApi 'PATCH 214) wrongMethod m = if m == methodPatch then methodPost else methodPatch test desc api method (status :: Int) = context desc $ with (return $ serve api server) $ do -- HEAD and 214/215 need not return bodies unless (status `elem` [214, 215] || method == methodHead) $ it "returns the person" $ do response <- THW.request method "/" [] "" liftIO $ statusCode (simpleStatus response) `shouldBe` status liftIO $ decode' (simpleBody response) `shouldBe` Just alice it "returns no content on NoContent" $ do response <- THW.request method "/noContent" [] "" liftIO $ statusCode (simpleStatus response) `shouldBe` 204 liftIO $ simpleBody response `shouldBe` "" -- HEAD should not return body when (method == methodHead) $ it "HEAD returns no content body" $ do response <- THW.request method "/" [] "" liftIO $ simpleBody response `shouldBe` "" it "throws 405 on wrong method " $ do THW.request (wrongMethod method) "/" [] "" `shouldRespondWith` 405 it "returns headers" $ do response1 <- THW.request method "/header" [] "" liftIO $ statusCode (simpleStatus response1) `shouldBe` status liftIO $ simpleHeaders response1 `shouldContain` [("H", "5")] response2 <- THW.request method "/header" [] "" liftIO $ statusCode (simpleStatus response2) `shouldBe` status liftIO $ simpleHeaders response2 `shouldContain` [("H", "5")] it "handles trailing '/' gracefully" $ do response <- THW.request method "/headerNC/" [] "" liftIO $ statusCode (simpleStatus response) `shouldBe` status it "returns 406 if the Accept header is not supported" $ do THW.request method "" [(hAccept, "crazy/mime")] "" `shouldRespondWith` 406 it "responds if the Accept header is supported" $ do response <- THW.request method "" [(hAccept, "application/json")] "" liftIO $ statusCode (simpleStatus response) `shouldBe` status unless (status `elem` [214, 215] || method == methodHead) $ it "allows modular specification of supported content types" $ do response <- THW.request method "/accept" [(hAccept, "text/plain")] "" liftIO $ statusCode (simpleStatus response) `shouldBe` status liftIO $ simpleBody response `shouldBe` "B" it "sets the Content-Type header" $ do response <- THW.request method "" [] "" liftIO $ simpleHeaders response `shouldContain` [("Content-Type", "application/json;charset=utf-8")] it "works for Stream as for Result" $ do response <- THW.request method "/stream" [] "" liftIO $ statusCode (simpleStatus response) `shouldBe` status liftIO $ simpleBody response `shouldBe` "bytestring" test "GET 200" get200 methodGet 200 test "POST 210" post210 methodPost 210 test "PUT 203" put203 methodPut 203 test "DELETE 280" delete280 methodDelete 280 test "PATCH 214" patch214 methodPatch 214 test "GET 200 with HEAD" get200 methodHead 200 -- }}} ------------------------------------------------------------------------------ -- * captureSpec {{{ ------------------------------------------------------------------------------ type CaptureApi = Capture "legs" Integer :> Get '[JSON] Animal :<|> "ears" :> Capture' '[Lenient] "ears" Integer :> Get '[JSON] Animal :<|> "eyes" :> Capture' '[Strict] "eyes" Integer :> Get '[JSON] Animal captureApi :: Proxy CaptureApi captureApi = Proxy captureServer :: Server CaptureApi captureServer = getLegs :<|> getEars :<|> getEyes where getLegs :: Integer -> Handler Animal getLegs legs = case legs of 4 -> return jerry 2 -> return tweety _ -> throwError err404 getEars :: Either String Integer -> Handler Animal getEars (Left _) = return chimera -- ignore integer parse error, return weird animal getEars (Right 2) = return jerry getEars (Right _) = throwError err404 getEyes :: Integer -> Handler Animal getEyes 2 = return jerry getEyes _ = throwError err404 captureSpec :: Spec captureSpec = do describe "Servant.API.Capture" $ do with (return (serve captureApi captureServer)) $ do it "can capture parts of the 'pathInfo'" $ do response <- get "/2" liftIO $ decode' (simpleBody response) `shouldBe` Just tweety it "returns 400 if the decoding fails" $ do get "/notAnInt" `shouldRespondWith` 400 it "returns an animal if eyes or ears are 2" $ do get "/ears/2" `shouldRespondWith` 200 get "/eyes/2" `shouldRespondWith` 200 it "returns a weird animal on Lenient Capture" $ do response <- get "/ears/bla" liftIO $ decode' (simpleBody response) `shouldBe` Just chimera it "returns 400 if parsing integer fails on Strict Capture" $ do get "/eyes/bla" `shouldRespondWith` 400 with (return (serve (Proxy :: Proxy (Capture "captured" String :> Raw)) (\ "captured" -> Tagged $ \request_ sendResponse -> sendResponse $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do it "strips the captured path snippet from pathInfo" $ do get "/captured/foo" `shouldRespondWith` (fromString (show ["foo" :: String])) -- }}} ------------------------------------------------------------------------------ -- * captureAllSpec {{{ ------------------------------------------------------------------------------ type CaptureAllApi = CaptureAll "legs" Integer :> Get '[JSON] Animal captureAllApi :: Proxy CaptureAllApi captureAllApi = Proxy captureAllServer :: [Integer] -> Handler Animal captureAllServer legs = case sum legs of 4 -> return jerry 2 -> return tweety 0 -> return beholder _ -> throwError err404 captureAllSpec :: Spec captureAllSpec = do describe "Servant.API.CaptureAll" $ do with (return (serve captureAllApi captureAllServer)) $ do it "can capture a single element of the 'pathInfo'" $ do response <- get "/2" liftIO $ decode' (simpleBody response) `shouldBe` Just tweety it "can capture multiple elements of the 'pathInfo'" $ do response <- get "/2/2" liftIO $ decode' (simpleBody response) `shouldBe` Just jerry it "can capture arbitrarily many elements of the 'pathInfo'" $ do response <- get "/1/1/0/1/0/1" liftIO $ decode' (simpleBody response) `shouldBe` Just jerry it "can capture when there are no elements in 'pathInfo'" $ do response <- get "/" liftIO $ decode' (simpleBody response) `shouldBe` Just beholder it "returns 400 if the decoding fails" $ do get "/notAnInt" `shouldRespondWith` 400 it "returns 400 if the decoding fails, regardless of which element" $ do get "/1/0/0/notAnInt/3/" `shouldRespondWith` 400 it "returns 400 if the decoding fails, even when it's multiple elements" $ do get "/1/0/0/notAnInt/3/orange/" `shouldRespondWith` 400 with (return (serve (Proxy :: Proxy (CaptureAll "segments" String :> Raw)) (\ _captured -> Tagged $ \request_ sendResponse -> sendResponse $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do it "consumes everything from pathInfo" $ do get "/captured/foo/bar/baz" `shouldRespondWith` (fromString (show ([] :: [Int]))) -- }}} ------------------------------------------------------------------------------ -- * queryParamSpec {{{ ------------------------------------------------------------------------------ data Filter = Filter { ageFilter :: Integer , nameFilter :: String } deriving Show instance FromDeepQuery Filter where fromDeepQuery params = traceShowId $ do let maybeToRight l = maybe (Left l) Right age' <- maybeToRight "missing age" $ readMaybe . T.unpack =<< join (lookup ["age"] params) name' <- maybeToRight "missing name" $ join $ lookup ["name"] params return $ Filter age' (T.unpack name') type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person :<|> "a" :> QueryParams "names" String :> Get '[JSON] Person :<|> "b" :> QueryFlag "capitalize" :> Get '[JSON] Person :<|> "param" :> QueryParam "age" Integer :> Get '[JSON] Person :<|> "multiparam" :> QueryParams "ages" Integer :> Get '[JSON] Person :<|> "raw-query-string" :> QueryString :> Get '[JSON] Person :<|> "deep-query" :> DeepQuery "filter" Filter :> Get '[JSON] Person queryParamApi :: Proxy QueryParamApi queryParamApi = Proxy qpServer :: Server QueryParamApi qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAges :<|> qpRaw :<|> qpDeep where qpNames (_:name2:_) = return alice { name = name2 } qpNames _ = return alice qpCapitalize False = return alice qpCapitalize True = return alice { name = map toUpper (name alice) } qpAge Nothing = return alice qpAge (Just age') = return alice{ age = age'} qpAges ages = return alice{ age = sum ages} qpRaw q = return alice { name = maybe mempty C8.unpack $ join (lookup "name" q) , age = fromMaybe 0 (readMaybe . C8.unpack =<< join (lookup "age" q)) } qpDeep filter' = return alice { name = nameFilter filter' , age = ageFilter filter' } queryParamServer (Just name_) = return alice{name = name_} queryParamServer Nothing = return alice queryParamSpec :: Spec queryParamSpec = do let mkRequest params pinfo = Network.Wai.Test.request defaultRequest { rawQueryString = params , queryString = parseQuery params , pathInfo = pinfo } describe "Servant.API.QueryParam" $ do it "allows retrieving simple GET parameters" $ flip runSession (serve queryParamApi qpServer) $ do response1 <- mkRequest "?name=bob" [] liftIO $ decode' (simpleBody response1) `shouldBe` Just alice { name = "bob" } it "allows retrieving lists in GET parameters" $ flip runSession (serve queryParamApi qpServer) $ do response2 <- mkRequest "?names[]=bob&names[]=john" ["a"] liftIO $ decode' (simpleBody response2) `shouldBe` Just alice { name = "john" } it "parses a query parameter" $ flip runSession (serve queryParamApi qpServer) $ do response <- mkRequest "?age=55" ["param"] liftIO $ decode' (simpleBody response) `shouldBe` Just alice { age = 55 } it "generates an error on query parameter parse failure" $ flip runSession (serve queryParamApi qpServer) $ do response <- mkRequest "?age=foo" ["param"] liftIO $ statusCode (simpleStatus response) `shouldBe` 400 return () it "parses multiple query parameters" $ flip runSession (serve queryParamApi qpServer) $ do response <- mkRequest "?ages=10&ages=22" ["multiparam"] liftIO $ decode' (simpleBody response) `shouldBe` Just alice { age = 32 } it "generates an error on parse failures of multiple parameters" $ flip runSession (serve queryParamApi qpServer) $ do response <- mkRequest "?ages=2&ages=foo" ["multiparam"] liftIO $ statusCode (simpleStatus response) `shouldBe` 400 return () it "allows retrieving value-less GET parameters" $ flip runSession (serve queryParamApi qpServer) $ do response3 <- mkRequest "?capitalize" ["b"] liftIO $ decode' (simpleBody response3) `shouldBe` Just alice { name = "ALICE" } response3' <- mkRequest "?capitalize=" ["b"] liftIO $ decode' (simpleBody response3') `shouldBe` Just alice { name = "ALICE" } response3'' <- mkRequest "?unknown=" ["b"] liftIO $ decode' (simpleBody response3'') `shouldBe` Just alice { name = "Alice" } it "allows retrieving a full query string" $ flip runSession (serve queryParamApi qpServer) $ do response <- mkRequest "?age=32&name=john" ["raw-query-string"] liftIO $ decode' (simpleBody response) `shouldBe` Just alice { name = "john" , age = 32 } it "allows retrieving a query string deep object" $ flip runSession (serve queryParamApi qpServer) $ do response <- mkRequest "?filter[age]=32&filter[name]=john" ["deep-query"] liftIO $ decode' (simpleBody response) `shouldBe` Just alice { name = "john" , age = 32 } describe "Uses queryString instead of rawQueryString" $ do -- test query parameters rewriter let queryRewriter :: Middleware queryRewriter app req = app req { queryString = fmap rewrite $ queryString req } where rewrite :: QueryItem -> QueryItem rewrite (k, v) = (fromMaybe k (BS.stripPrefix "person_" k), v) let app = queryRewriter $ serve queryParamApi qpServer it "allows rewriting for simple GET/query parameters" $ flip runSession app $ do response1 <- mkRequest "?person_name=bob" [] liftIO $ decode' (simpleBody response1) `shouldBe` Just alice { name = "bob" } it "allows rewriting for lists in GET parameters" $ flip runSession app $ do response2 <- mkRequest "?person_names[]=bob&person_names[]=john" ["a"] liftIO $ decode' (simpleBody response2) `shouldBe` Just alice { name = "john" } it "allows rewriting when parsing multiple query parameters" $ flip runSession app $ do response <- mkRequest "?person_ages=10&person_ages=22" ["multiparam"] liftIO $ decode' (simpleBody response) `shouldBe` Just alice { age = 32 } it "allows retrieving value-less GET parameters" $ flip runSession app $ do response3 <- mkRequest "?person_capitalize" ["b"] liftIO $ decode' (simpleBody response3) `shouldBe` Just alice { name = "ALICE" } response3' <- mkRequest "?person_capitalize=" ["b"] liftIO $ decode' (simpleBody response3') `shouldBe` Just alice { name = "ALICE" } response3'' <- mkRequest "?person_unknown=" ["b"] liftIO $ decode' (simpleBody response3'') `shouldBe` Just alice { name = "Alice" } -- }}} ------------------------------------------------------------------------------ -- * fragmentSpec {{{ ------------------------------------------------------------------------------ type FragmentApi = "name" :> Fragment String :> Get '[JSON] Person :<|> "age" :> Fragment Integer :> Get '[JSON] Person fragmentApi :: Proxy FragmentApi fragmentApi = Proxy fragServer :: Server FragmentApi fragServer = fragmentServer :<|> fragAge where fragmentServer = return alice fragAge = return alice fragmentSpec :: Spec fragmentSpec = do let mkRequest params pinfo = Network.Wai.Test.request defaultRequest { rawQueryString = params , queryString = parseQuery params , pathInfo = pinfo } describe "Servant.API.Fragment" $ do it "ignores fragment even if it is present in query" $ do flip runSession (serve fragmentApi fragServer) $ do response1 <- mkRequest "#Alice" ["name"] liftIO $ decode' (simpleBody response1) `shouldBe` Just alice -- }}} ------------------------------------------------------------------------------ -- * reqBodySpec {{{ ------------------------------------------------------------------------------ type ReqBodyApi = ReqBody '[JSON] Person :> Post '[JSON] Person :<|> "blah" :> ReqBody '[JSON] Person :> Put '[JSON] Integer reqBodyApi :: Proxy ReqBodyApi reqBodyApi = Proxy reqBodySpec :: Spec reqBodySpec = describe "Servant.API.ReqBody" $ do let server :: Server ReqBodyApi server = return :<|> return . age mkReq method x = THW.request method x [(hContentType, "application/json;charset=utf-8")] with (return $ serve reqBodyApi server) $ do it "passes the argument to the handler" $ do response <- mkReq methodPost "" (encode alice) liftIO $ decode' (simpleBody response) `shouldBe` Just alice it "rejects invalid request bodies with status 400" $ do mkReq methodPut "/blah" "some invalid body" `shouldRespondWith` 400 it "responds with 415 if the request body media type is unsupported" $ do THW.request methodPost "/" [(hContentType, "application/nonsense")] "" `shouldRespondWith` 415 -- }}} ------------------------------------------------------------------------------ -- * headerSpec {{{ ------------------------------------------------------------------------------ type HeaderApi a = Header "MyHeader" a :> Delete '[JSON] NoContent headerApi :: Proxy a -> Proxy (HeaderApi a) headerApi _ = Proxy headerSpec :: Spec headerSpec = describe "Servant.API.Header" $ do let expectsInt :: Maybe Int -> Handler NoContent expectsInt (Just x) = do when (x /= 5) $ error "Expected 5" return NoContent expectsInt Nothing = error "Expected an int" let expectsString :: Maybe String -> Handler NoContent expectsString (Just x) = do when (x /= "more from you") $ error "Expected more from you" return NoContent expectsString Nothing = error "Expected a string" with (return (serve (headerApi (Proxy :: Proxy Int)) expectsInt)) $ do let delete' x = THW.request methodDelete x [("MyHeader", "5")] it "passes the header to the handler (Int)" $ delete' "/" "" `shouldRespondWith` 200 with (return (serve (headerApi (Proxy :: Proxy String)) expectsString)) $ do let delete' x = THW.request methodDelete x [("MyHeader", "more from you")] it "passes the header to the handler (String)" $ delete' "/" "" `shouldRespondWith` 200 with (return (serve (headerApi (Proxy :: Proxy Int)) expectsInt)) $ do let delete' x = THW.request methodDelete x [("MyHeader", "not a number")] it "checks for parse errors" $ delete' "/" "" `shouldRespondWith` 400 -- }}} ------------------------------------------------------------------------------ -- * rawSpec {{{ ------------------------------------------------------------------------------ type RawApi = "foo" :> Raw rawApi :: Proxy RawApi rawApi = Proxy rawApplication :: Show a => (Request -> a) -> Tagged m Application rawApplication f = Tagged $ \request_ sendResponse -> sendResponse $ responseLBS ok200 [] (cs $ show $ f request_) rawSpec :: Spec rawSpec = do describe "Servant.API.Raw" $ do it "runs applications" $ do flip runSession (serve rawApi (rawApplication (const (42 :: Integer)))) $ do response <- Network.Wai.Test.request defaultRequest{ pathInfo = ["foo"] } liftIO $ do simpleBody response `shouldBe` "42" it "gets the pathInfo modified" $ do flip runSession (serve rawApi (rawApplication pathInfo)) $ do response <- Network.Wai.Test.request defaultRequest{ pathInfo = ["foo", "bar"] } liftIO $ do simpleBody response `shouldBe` cs (show ["bar" :: String]) -- }}} ------------------------------------------------------------------------------ -- * alternativeSpec {{{ ------------------------------------------------------------------------------ type AlternativeApi = "foo" :> Get '[JSON] Person :<|> "bar" :> Get '[JSON] Animal :<|> "foo" :> Get '[PlainText] T.Text :<|> "bar" :> Post '[JSON] Animal :<|> "bar" :> Put '[JSON] Animal :<|> "bar" :> Delete '[JSON] NoContent alternativeApi :: Proxy AlternativeApi alternativeApi = Proxy alternativeServer :: Server AlternativeApi alternativeServer = return alice :<|> return jerry :<|> return "a string" :<|> return jerry :<|> return jerry :<|> return NoContent alternativeSpec :: Spec alternativeSpec = do describe "Servant.API.Alternative" $ do with (return $ serve alternativeApi alternativeServer) $ do it "unions endpoints" $ do response <- get "/foo" liftIO $ do decode' (simpleBody response) `shouldBe` Just alice response_ <- get "/bar" liftIO $ do decode' (simpleBody response_) `shouldBe` Just jerry it "checks all endpoints before returning 415" $ do get "/foo" `shouldRespondWith` 200 it "returns 404 if the path does not exist" $ do get "/nonexistent" `shouldRespondWith` 404 -- }}} ------------------------------------------------------------------------------ -- * responseHeaderSpec {{{ ------------------------------------------------------------------------------ type ResponseHeadersApi = Get '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String) :<|> Post '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String) :<|> Put '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String) :<|> Patch '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String) responseHeadersServer :: Server ResponseHeadersApi responseHeadersServer = let h = return $ addHeader 5 $ addHeader "kilroy" "hi" in h :<|> h :<|> h :<|> h responseHeadersSpec :: Spec responseHeadersSpec = describe "ResponseHeaders" $ do with (return $ serve (Proxy :: Proxy ResponseHeadersApi) responseHeadersServer) $ do let methods = [methodGet, methodPost, methodPut, methodPatch] it "includes the headers in the response" $ forM_ methods $ \method -> THW.request method "/" [] "" `shouldRespondWith` "\"hi\""{ matchHeaders = ["H1" <:> "5", "H2" <:> "kilroy"] , matchStatus = 200 } it "responds with not found for non-existent endpoints" $ forM_ methods $ \method -> THW.request method "blahblah" [] "" `shouldRespondWith` 404 it "returns 406 if the Accept header is not supported" $ forM_ methods $ \method -> THW.request method "" [(hAccept, "crazy/mime")] "" `shouldRespondWith` 406 -- }}} ------------------------------------------------------------------------------ -- * uverbResponseHeaderSpec {{{ ------------------------------------------------------------------------------ type UVerbHeaderResponse = '[ WithStatus 200 (Headers '[Header "H1" Int] String), WithStatus 404 String ] type UVerbResponseHeadersApi = Capture "ok" Bool :> UVerb 'GET '[JSON] UVerbHeaderResponse uverbResponseHeadersServer :: Server UVerbResponseHeadersApi uverbResponseHeadersServer True = respond . WithStatus @200 . addHeader @"H1" (5 :: Int) $ ("foo" :: String) uverbResponseHeadersServer False = respond . WithStatus @404 $ ("bar" :: String) uverbResponseHeadersSpec :: Spec uverbResponseHeadersSpec = describe "UVerbResponseHeaders" $ do with (return $ serve (Proxy :: Proxy UVerbResponseHeadersApi) uverbResponseHeadersServer) $ do it "includes the headers in the response" $ THW.request methodGet "/true" [] "" `shouldRespondWith` "\"foo\"" { matchHeaders = ["H1" <:> "5"] , matchStatus = 200 } -- }}} ------------------------------------------------------------------------------ -- * miscCombinatorSpec {{{ ------------------------------------------------------------------------------ type MiscCombinatorsAPI = "version" :> HttpVersion :> Get '[JSON] String :<|> "secure" :> IsSecure :> Get '[JSON] String :<|> "host" :> RemoteHost :> Get '[JSON] String :<|> "empty" :> EmptyAPI miscApi :: Proxy MiscCombinatorsAPI miscApi = Proxy miscServ :: Server MiscCombinatorsAPI miscServ = versionHandler :<|> secureHandler :<|> hostHandler :<|> emptyServer where versionHandler = return . show secureHandler Secure = return "secure" secureHandler NotSecure = return "not secure" hostHandler = return . show miscCombinatorSpec :: Spec miscCombinatorSpec = with (return $ serve miscApi miscServ) $ describe "Misc. combinators for request inspection" $ do it "Successfully gets the HTTP version specified in the request" $ go "/version" "\"HTTP/1.0\"" it "Checks that hspec-wai uses HTTP, not HTTPS" $ go "/secure" "\"not secure\"" it "Checks that hspec-wai issues request from 0.0.0.0" $ go "/host" "\"0.0.0.0:0\"" it "Doesn't serve anything from the empty API" $ Test.Hspec.Wai.get "empty" `shouldRespondWith` 404 where go path res = Test.Hspec.Wai.get path `shouldRespondWith` res -- }}} ------------------------------------------------------------------------------ -- * Basic Authentication {{{ ------------------------------------------------------------------------------ type BasicAuthAPI = BasicAuth "foo" () :> "basic" :> Get '[JSON] Animal :<|> Raw basicAuthApi :: Proxy BasicAuthAPI basicAuthApi = Proxy basicAuthServer :: Server BasicAuthAPI basicAuthServer = const (return jerry) :<|> (Tagged $ \ _ sendResponse -> sendResponse $ responseLBS imATeapot418 [] "") basicAuthContext :: Context '[ BasicAuthCheck () ] basicAuthContext = let basicHandler = BasicAuthCheck $ \(BasicAuthData usr pass) -> if usr == "servant" && pass == "server" then return (Authorized ()) else return Unauthorized in basicHandler :. EmptyContext basicAuthSpec :: Spec basicAuthSpec = do describe "Servant.API.BasicAuth" $ do with (return (serveWithContext basicAuthApi basicAuthContext basicAuthServer)) $ do context "Basic Authentication" $ do let basicAuthHeaders user password = [("Authorization", "Basic " <> Base64.encode (user <> ":" <> password))] it "returns 401 when no credentials given" $ do get "/basic" `shouldRespondWith` 401 it "returns 403 when invalid credentials given" $ do THW.request methodGet "/basic" (basicAuthHeaders "servant" "wrong") "" `shouldRespondWith` 403 it "returns 200 with the right password" $ do THW.request methodGet "/basic" (basicAuthHeaders "servant" "server") "" `shouldRespondWith` 200 it "plays nice with subsequent Raw endpoints" $ do get "/foo" `shouldRespondWith` 418 -- }}} ------------------------------------------------------------------------------ -- * General Authentication {{{ ------------------------------------------------------------------------------ type GenAuthAPI = AuthProtect "auth" :> "auth" :> Get '[JSON] Animal :<|> Raw genAuthApi :: Proxy GenAuthAPI genAuthApi = Proxy genAuthServer :: Server GenAuthAPI genAuthServer = const (return tweety) :<|> (Tagged $ \ _ sendResponse -> sendResponse $ responseLBS imATeapot418 [] "") type instance AuthServerData (AuthProtect "auth") = () genAuthContext :: Context '[AuthHandler Request ()] genAuthContext = let authHandler = \req -> case lookup "Auth" (requestHeaders req) of Just "secret" -> return () Just _ -> throwError err403 Nothing -> throwError err401 in mkAuthHandler authHandler :. EmptyContext genAuthSpec :: Spec genAuthSpec = do describe "Servant.API.Auth" $ do with (return (serveWithContext genAuthApi genAuthContext genAuthServer)) $ do context "Custom Auth Protection" $ do it "returns 401 when missing headers" $ do get "/auth" `shouldRespondWith` 401 it "returns 403 on wrong passwords" $ do THW.request methodGet "/auth" [("Auth","wrong")] "" `shouldRespondWith` 403 it "returns 200 with the right header" $ do THW.request methodGet "/auth" [("Auth","secret")] "" `shouldRespondWith` 200 it "plays nice with subsequent Raw endpoints" $ do get "/foo" `shouldRespondWith` 418 -- }}} ------------------------------------------------------------------------------ -- * UVerb {{{ ------------------------------------------------------------------------------ newtype PersonResponse = PersonResponse Person deriving Generic instance ToJSON PersonResponse instance HasStatus PersonResponse where type StatusOf PersonResponse = 200 newtype RedirectResponse = RedirectResponse String deriving Generic instance ToJSON RedirectResponse instance HasStatus RedirectResponse where type StatusOf RedirectResponse = 301 newtype AnimalResponse = AnimalResponse Animal deriving Generic instance ToJSON AnimalResponse instance HasStatus AnimalResponse where type StatusOf AnimalResponse = 203 type UVerbApi = "person" :> Capture "shouldRedirect" Bool :> UVerb 'GET '[JSON] '[PersonResponse, RedirectResponse] :<|> "animal" :> UVerb 'GET '[JSON] '[AnimalResponse] uverbSpec :: Spec uverbSpec = describe "Servant.API.UVerb " $ do let joe = Person "joe" 42 mouse = Animal "Mouse" 7 personHandler :: Bool -> Handler (Union '[PersonResponse ,RedirectResponse]) personHandler True = respond $ RedirectResponse "over there!" personHandler False = respond $ PersonResponse joe animalHandler = respond $ AnimalResponse mouse server :: Server UVerbApi server = personHandler :<|> animalHandler with (pure $ serve (Proxy :: Proxy UVerbApi) server) $ do context "A route returning either 301/String or 200/Person" $ do context "when requesting the person" $ do let theRequest = THW.get "/person/false" it "returns status 200" $ theRequest `shouldRespondWith` 200 it "returns a person" $ do response <- theRequest liftIO $ decode' (simpleBody response) `shouldBe` Just joe context "requesting the redirect" $ it "returns a message and status 301" $ THW.get "/person/true" `shouldRespondWith` "\"over there!\"" {matchStatus = 301} context "a route with a single response type" $ do let theRequest = THW.get "/animal" it "should return the defined status code" $ theRequest `shouldRespondWith` 203 it "should return the expected response" $ do response <- theRequest liftIO $ decode' (simpleBody response) `shouldBe` Just mouse -- }}} ------------------------------------------------------------------------------ -- * Test data types {{{ ------------------------------------------------------------------------------ data Person = Person { name :: String, age :: Integer } deriving (Eq, Show, Generic) instance ToJSON Person instance FromJSON Person alice :: Person alice = Person "Alice" 42 data Animal = Animal { species :: String, numberOfLegs :: Integer } deriving (Eq, Show, Generic) instance ToJSON Animal instance FromJSON Animal jerry :: Animal jerry = Animal "Mouse" 4 tweety :: Animal tweety = Animal "Bird" 2 -- weird animal with non-integer amount of ears chimera :: Animal chimera = Animal "Chimera" (-1) beholder :: Animal beholder = Animal "Beholder" 0 -- }}}