1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE QuasiQuotes #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE TypeOperators #-}
8module Servant.ServerSpec where
11import Control.Monad (when)
12import Control.Monad.Trans.Either (EitherT, left)
13import Data.Aeson (ToJSON, FromJSON, encode, decode')
14import Data.Char (toUpper)
15import Data.Monoid ((<>))
16import Data.Proxy (Proxy(Proxy))
17import Data.String (fromString)
18import Data.String.Conversions (cs)
19import GHC.Generics (Generic)
20import Network.HTTP.Types ( parseQuery, ok200, status409, methodPost
21 , methodDelete, hContentType)
22import Network.Wai ( Application, Request, responseLBS, pathInfo
23 , queryString, rawQueryString )
24import Network.Wai.Test (runSession, defaultRequest, simpleBody, request)
25import Test.Hspec (Spec, describe, it, shouldBe)
26import Test.Hspec.Wai ( liftIO, with, get, post, shouldRespondWith
27 , matchStatus, request )
29import Servant.API (JSON, Capture, Get, ReqBody, Post, QueryParam
30 , QueryParams, QueryFlag, MatrixParam, MatrixParams
31 , MatrixFlag, Raw, (:>), (:<|>)(..), Header, Delete )
32import Servant.Server (Server, serve)
33import Servant.Server.Internal (RouteMismatch(..))
42 deriving (Eq, Show, Generic)
45instance FromJSON Person
48alice = Person "Alice" 42
52 numberOfLegs :: Integer
54 deriving (Eq, Show, Generic)
57instance FromJSON Animal
60jerry = Animal "Mouse" 4
63tweety = Animal "Bird" 2
81type CaptureApi = Capture "legs" Integer :> Get '[JSON] Animal
82captureApi :: Proxy CaptureApi
84captureServer :: Integer -> EitherT (Int, String) IO Animal
85captureServer legs = case legs of
88 _ -> left (404, "not found")
92 describe "Servant.API.Capture" $ do
93 with (return (serve captureApi captureServer)) $ do
94 it "can capture parts of the 'pathInfo'" $ do
97 decode' (simpleBody response) `shouldBe` Just tweety
100 (Proxy :: Proxy (Capture "captured" String :> Raw))
101 (\ "captured" request_ respond ->
102 respond $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do
103 it "strips the captured path snippet from pathInfo" $ do
104 get "/captured/foo" `shouldRespondWith` (fromString (show ["foo" :: String]))
107type GetApi = Get '[JSON] Person
108getApi :: Proxy GetApi
113 describe "Servant.API.Get" $ do
114 with (return (serve getApi (return alice))) $ do
115 it "allows to GET a Person" $ do
117 return response `shouldRespondWith` 200
119 decode' (simpleBody response) `shouldBe` Just alice
121 it "throws 405 (wrong method) on POSTs" $ do
122 post "/" "" `shouldRespondWith` 405
125type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
126 :<|> "a" :> QueryParams "names" String :> Get '[JSON] Person
127 :<|> "b" :> QueryFlag "capitalize" :> Get '[JSON] Person
129queryParamApi :: Proxy QueryParamApi
132qpServer :: Server QueryParamApi
133qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize
135 where qpNames (_:name2:_) = return alice { name = name2 }
136 qpNames _ = return alice
138 qpCapitalize False = return alice
139 qpCapitalize True = return alice { name = map toUpper (name alice) }
141 queryParamServer (Just name_) = return alice{name = name_}
142 queryParamServer Nothing = return alice
144queryParamSpec :: Spec
146 describe "Servant.API.QueryParam" $ do
147 it "allows to retrieve simple GET parameters" $
148 (flip runSession) (serve queryParamApi qpServer) $ do
149 let params1 = "?name=bob"
150 response1 <- Network.Wai.Test.request defaultRequest{
151 rawQueryString = params1,
152 queryString = parseQuery params1
155 decode' (simpleBody response1) `shouldBe` Just alice{
159 it "allows to retrieve lists in GET parameters" $
160 (flip runSession) (serve queryParamApi qpServer) $ do
161 let params2 = "?names[]=bob&names[]=john"
162 response2 <- Network.Wai.Test.request defaultRequest{
163 rawQueryString = params2,
164 queryString = parseQuery params2,
168 decode' (simpleBody response2) `shouldBe` Just alice{
173 it "allows to retrieve value-less GET parameters" $
174 (flip runSession) (serve queryParamApi qpServer) $ do
175 let params3 = "?capitalize"
176 response3 <- Network.Wai.Test.request defaultRequest{
177 rawQueryString = params3,
178 queryString = parseQuery params3,
182 decode' (simpleBody response3) `shouldBe` Just alice{
186 let params3' = "?capitalize="
187 response3' <- Network.Wai.Test.request defaultRequest{
188 rawQueryString = params3',
189 queryString = parseQuery params3',
193 decode' (simpleBody response3') `shouldBe` Just alice{
197 let params3'' = "?unknown="
198 response3' <- Network.Wai.Test.request defaultRequest{
199 rawQueryString = params3'',
200 queryString = parseQuery params3'',
204 decode' (simpleBody response3') `shouldBe` Just alice{
208type MatrixParamApi = "a" :> MatrixParam "name" String :> Get '[JSON] Person
209 :<|> "b" :> MatrixParams "names" String :> "bsub" :> MatrixParams "names" String :> Get '[JSON] Person
210 :<|> "c" :> MatrixFlag "capitalize" :> Get '[JSON] Person
211 :<|> "d" :> Capture "foo" Integer :> MatrixParam "name" String :> MatrixFlag "capitalize" :> "dsub" :> Get '[JSON] Person
213matrixParamApi :: Proxy MatrixParamApi
214matrixParamApi = Proxy
216mpServer :: Server MatrixParamApi
217mpServer = matrixParamServer :<|> mpNames :<|> mpCapitalize alice :<|> mpComplex
218 where mpNames (_:name2:_) _ = return alice { name = name2 }
219 mpNames _ _ = return alice
221 mpCapitalize p False = return p
222 mpCapitalize p True = return p { name = map toUpper (name p) }
224 matrixParamServer (Just name) = return alice{name = name}
225 matrixParamServer Nothing = return alice
227 mpAge age p = return p { age = age }
228 mpComplex capture name cap = matrixParamServer name >>= flip mpCapitalize cap >>= mpAge capture
230matrixParamSpec :: Spec
232 describe "Servant.API.MatrixParam" $ do
233 it "allows to retrieve simple matrix parameters" $
234 (flip runSession) (serve matrixParamApi mpServer) $ do
235 response1 <- Network.Wai.Test.request defaultRequest{
236 pathInfo = ["a;name=bob"]
239 decode' (simpleBody response1) `shouldBe` Just alice{
243 it "allows to retrieve lists in matrix parameters" $
244 (flip runSession) (serve matrixParamApi mpServer) $ do
245 response2 <- Network.Wai.Test.request defaultRequest{
246 pathInfo = ["b;names=bob;names=john", "bsub;names=anna;names=sarah"]
249 decode' (simpleBody response2) `shouldBe` Just alice{
253 it "allows to retrieve value-less matrix parameters" $
254 (flip runSession) (serve matrixParamApi mpServer) $ do
255 response3 <- Network.Wai.Test.request defaultRequest{
256 pathInfo = ["c;capitalize"]
259 decode' (simpleBody response3) `shouldBe` Just alice{
263 response3' <- Network.Wai.Test.request defaultRequest{
264 pathInfo = ["c;capitalize="]
267 decode' (simpleBody response3') `shouldBe` Just alice{
271 it "allows to retrieve matrix parameters on captured segments" $
272 (flip runSession) (serve matrixParamApi mpServer) $ do
273 response4 <- Network.Wai.Test.request defaultRequest{
274 pathInfo = ["d", "12;name=stephen;capitalize", "dsub"]
277 decode' (simpleBody response4) `shouldBe` Just alice{
282 response4' <- Network.Wai.Test.request defaultRequest{
283 pathInfo = ["d;ignored=1", "5", "dsub"]
286 decode' (simpleBody response4') `shouldBe` Just alice{
292 ReqBody '[JSON] Person :> Post '[JSON] Integer
293 :<|> "bla" :> ReqBody '[JSON] Person :> Post '[JSON] Integer
294postApi :: Proxy PostApi
299 describe "Servant.API.Post and .ReqBody" $ do
300 with (return (serve postApi (return . age :<|> return . age))) $ do
301 let post' x = Test.Hspec.Wai.request methodPost x [(hContentType
302 , "application/json;charset=utf-8")]
304 it "allows to POST a Person" $ do
305 post' "/" (encode alice) `shouldRespondWith` "42"{
309 it "allows alternative routes if all have request bodies" $ do
310 post' "/bla" (encode alice) `shouldRespondWith` "42"{
314 it "handles trailing '/' gracefully" $ do
315 post' "/bla/" (encode alice) `shouldRespondWith` "42"{
319 it "correctly rejects invalid request bodies with status 400" $ do
320 post' "/" "some invalid body" `shouldRespondWith` 400
322 it "responds with 415 if the requested media type is unsupported" $ do
323 let post'' x = Test.Hspec.Wai.request methodPost x [(hContentType
324 , "application/nonsense")]
325 post'' "/" "anything at all" `shouldRespondWith` 415
327type HeaderApi a = Header "MyHeader" a :> Delete
328headerApi :: Proxy (HeaderApi a)
332headerSpec = describe "Servant.API.Header" $ do
334 let expectsInt :: Maybe Int -> EitherT (Int,String) IO ()
335 expectsInt (Just x) = when (x /= 5) $ error "Expected 5"
336 expectsInt Nothing = error "Expected an int"
338 let expectsString :: Maybe String -> EitherT (Int,String) IO ()
339 expectsString (Just x) = when (x /= "more from you") $ error "Expected more from you"
340 expectsString Nothing = error "Expected a string"
342 with (return (serve headerApi expectsInt)) $ do
343 let delete' x = Test.Hspec.Wai.request methodDelete x [("MyHeader" ,"5")]
345 it "passes the header to the handler (Int)" $
346 delete' "/" "" `shouldRespondWith` 204
348 with (return (serve headerApi expectsString)) $ do
349 let delete' x = Test.Hspec.Wai.request methodDelete x [("MyHeader" ,"more from you")]
351 it "passes the header to the handler (String)" $
352 delete' "/" "" `shouldRespondWith` 204
355type RawApi = "foo" :> Raw
356rawApi :: Proxy RawApi
358rawApplication :: Show a => (Request -> a) -> Application
359rawApplication f request_ respond = respond $ responseLBS ok200 [] (cs $ show $ f request_)
363 describe "Servant.API.Raw" $ do
364 it "runs applications" $ do
365 (flip runSession) (serve rawApi (rawApplication (const (42 :: Integer)))) $ do
366 response <- Network.Wai.Test.request defaultRequest{
370 simpleBody response `shouldBe` "42"
372 it "gets the pathInfo modified" $ do
373 (flip runSession) (serve rawApi (rawApplication pathInfo)) $ do
374 response <- Network.Wai.Test.request defaultRequest{
375 pathInfo = ["foo", "bar"]
378 simpleBody response `shouldBe` cs (show ["bar" :: String])
382 "foo" :> Get '[JSON] Person
383 :<|> "bar" :> Get '[JSON] Animal
384unionApi :: Proxy AlternativeApi
387unionServer :: Server AlternativeApi
394 describe "Servant.API.Alternative" $ do
395 with (return $ serve unionApi unionServer) $ do
396 it "unions endpoints" $ do
397 response <- get "/foo"
399 decode' (simpleBody response) `shouldBe`
401 response_ <- get "/bar"
403 decode' (simpleBody response_) `shouldBe`
406-- | Test server error functionality.
409 let he = HttpError status409 (Just "A custom error")
410 let ib = InvalidBody "The body is invalid"
414 describe "Servant.Server.Internal.RouteMismatch" $ do
415 it "HttpError > *" $ do
416 ib <> he `shouldBe` he
417 wm <> he `shouldBe` he
418 nf <> he `shouldBe` he
420 he <> ib `shouldBe` he
421 he <> wm `shouldBe` he
422 he <> nf `shouldBe` he
424 it "HE > InvalidBody > (WM,NF)" $ do
425 he <> ib `shouldBe` he
426 wm <> ib `shouldBe` ib
427 nf <> ib `shouldBe` ib
429 ib <> he `shouldBe` he
430 ib <> wm `shouldBe` ib
431 ib <> nf `shouldBe` ib
433 it "HE > IB > WrongMethod > NF" $ do
434 he <> wm `shouldBe` he
435 ib <> wm `shouldBe` ib
436 nf <> wm `shouldBe` wm
438 wm <> he `shouldBe` he
439 wm <> ib `shouldBe` ib
440 wm <> nf `shouldBe` wm
442 it "* > NotFound" $ do
443 he <> nf `shouldBe` he
444 ib <> nf `shouldBe` ib
445 wm <> nf `shouldBe` wm
447 nf <> he `shouldBe` he
448 nf <> ib `shouldBe` ib
449 nf <> wm `shouldBe` wm
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list