@@ -26,14 +26,13 @@ import Prelude ()
26import Prelude.Compat
26import Prelude.Compat
28import Control.Arrow (left)
28import Control.Arrow (left)
29−import Control.Monad.Trans.Except (runExceptT, throwE)
29+import Control.Monad.Trans.Except (throwE )
30import Data.Aeson
30import Data.Aeson
31import qualified Data.ByteString.Lazy as BS
31import qualified Data.ByteString.Lazy as BS
32import Data.Char (chr, isPrint)
32import Data.Char (chr, isPrint)
33import Data.Foldable (forM_)
33import Data.Foldable (forM_)
34import Data.Monoid hiding (getLast)
34import Data.Monoid hiding (getLast)
35import Data.Proxy
35import Data.Proxy
36−import qualified Data.Text as T
37import GHC.Generics (Generic)
36import GHC.Generics (Generic)
38import qualified Network.HTTP.Client as C
37import qualified Network.HTTP.Client as C
39import Network.HTTP.Media
38import Network.HTTP.Media
@@ -46,10 +45,12 @@ import Test.HUnit
46import Test.Hspec
45import Test.Hspec
47import Test.Hspec.QuickCheck
46import Test.Hspec.QuickCheck
48import Test.QuickCheck
47import Test.QuickCheck
48+import Web.FormUrlEncoded (FromForm, ToForm)
50import Servant.API
50import Servant.API
51import Servant.Client
51import Servant.Client
52import Servant.Client.TestServer
52import Servant.Client.TestServer
53+import Servant.Client.TestServer.GHC
53import qualified Servant.Common.Req as SCR
54import qualified Servant.Common.Req as SCR
54import Servant.Server
55import Servant.Server
55import Servant.Server.Experimental.Auth
56import Servant.Server.Experimental.Auth
@@ -117,19 +118,8 @@ data Person = Person {
117instance ToJSON Person
118instance ToJSON Person
118instance FromJSON Person
119instance FromJSON Person
120−instance ToFormUrlEncoded Person where
121+instance ToForm Person where
121− toFormUrlEncoded Person{..} =
122− [("name", T.pack name), ("age", T.pack (show age))]
124−lookupEither :: (Show a, Eq a) => a -> [(a,b)] -> Either String b
125−lookupEither x xs = do
126− maybe (Left $ "could not find key " <> show x) return $ lookup x xs
128−instance FromFormUrlEncoded Person where
122+instance FromForm Person where
129− fromFormUrlEncoded xs = do
130− n <- lookupEither "name" xs
131− a <- lookupEither "age" xs
132− return $ Person (T.unpack n) (read $ T.unpack a)
134alice :: Person
124alice :: Person
135alice = Person "Alice" 42
125alice = Person "Alice" 42
@@ -140,6 +130,7 @@ type Api =
140 "get" :> Get '[JSON] Person
130 "get" :> Get '[JSON] Person
141 :<|> "deleteEmpty" :> DeleteNoContent '[JSON] NoContent
131 :<|> "deleteEmpty" :> DeleteNoContent '[JSON] NoContent
142 :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person
132 :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person
133+ :<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person]
143 :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
134 :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
144 :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person
135 :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person
145 :<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]
136 :<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]
@@ -157,24 +148,26 @@ type Api =
157api :: Proxy Api
148api :: Proxy Api
158api = Proxy
149api = Proxy
160−getGet :: C.Manager -> BaseUrl -> SCR.ClientM Person
151+getGet :: SCR.ClientM Person
161−getDeleteEmpty :: C.Manager -> BaseUrl -> SCR.ClientM NoContent
152+getDeleteEmpty :: SCR.ClientM NoContent
162−getCapture :: String -> C.Manager -> BaseUrl -> SCR.ClientM Person
153+getCapture :: String -> SCR.ClientM Person
154+getCaptureAll :: [String] -> SCR.ClientM [Person]
163−getBody :: Person -> C.Manager -> BaseUrl -> SCR.ClientM Person
155+getBody :: Person -> SCR.ClientM Person
164−getQueryParam :: Maybe String -> C.Manager -> BaseUrl -> SCR.ClientM Person
156+getQueryParam :: Maybe String -> SCR.ClientM Person
165−getQueryParams :: [String] -> C.Manager -> BaseUrl -> SCR.ClientM [Person]
157+getQueryParams :: [String] -> SCR.ClientM [Person]
166−getQueryFlag :: Bool -> C.Manager -> BaseUrl -> SCR.ClientM Bool
158+getQueryFlag :: Bool -> SCR.ClientM Bool
167−getRawSuccess :: HTTP.Method -> C.Manager -> BaseUrl
159+getRawSuccess :: HTTP.Method
168 -> SCR.ClientM (Int, BS.ByteString, MediaType, [HTTP.Header], C.Response BS.ByteString)
160 -> SCR.ClientM (Int, BS.ByteString, MediaType, [HTTP.Header], C.Response BS.ByteString)
169−getRawFailure :: HTTP.Method -> C.Manager -> BaseUrl
161+getRawFailure :: HTTP.Method
170 -> SCR.ClientM (Int, BS.ByteString, MediaType, [HTTP.Header], C.Response BS.ByteString)
162 -> SCR.ClientM (Int, BS.ByteString, MediaType, [HTTP.Header], C.Response BS.ByteString)
171−getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])] -> C.Manager -> BaseUrl
163+getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
172 -> SCR.ClientM (String, Maybe Int, Bool, [(String, [Rational])])
164 -> SCR.ClientM (String, Maybe Int, Bool, [(String, [Rational])])
173−getRespHeaders :: C.Manager -> BaseUrl -> SCR.ClientM (Headers TestHeaders Bool)
165+getRespHeaders :: SCR.ClientM (Headers TestHeaders Bool)
174−getDeleteContentType :: C.Manager -> BaseUrl -> SCR.ClientM NoContent
166+getDeleteContentType :: SCR.ClientM NoContent
176 :<|> getDeleteEmpty
168 :<|> getDeleteEmpty
177 :<|> getCapture
169 :<|> getCapture
178 :<|> getBody
171 :<|> getBody
179 :<|> getQueryParam
172 :<|> getQueryParam
180 :<|> getQueryParams
173 :<|> getQueryParams
@@ -190,6 +183,7 @@ server = TestServer "server" $ serve api (
190 return alice
183 return alice
191 :<|> return NoContent
184 :<|> return NoContent
192 :<|> (\ name -> return $ Person name 0)
185 :<|> (\ name -> return $ Person name 0)
186+ :<|> (\ names -> return (zipWith Person names [0..]))
193 :<|> return
187 :<|> return
194 :<|> (\ name -> case name of
188 :<|> (\ name -> case name of
195 Just "alice" -> return alice
189 Just "alice" -> return alice
@@ -275,38 +269,42 @@ sucessSpec :: Spec
275sucessSpec = around (withTestServer "server") $ do
269sucessSpec = around (withTestServer "server") $ do
277 it "Servant.API.Get" $ \baseUrl -> do
271 it "Servant.API.Get" $ \baseUrl -> do
278− (left show <$> runExceptT (getGet manager baseUrl)) `shouldReturn` Right alice
272+ (left show <$> (runClientM getGet (ClientEnv manager baseUrl))) `shouldReturn` Right alice
280 describe "Servant.API.Delete" $ do
274 describe "Servant.API.Delete" $ do
281 it "allows empty content type" $ \baseUrl -> do
275 it "allows empty content type" $ \baseUrl -> do
282− (left show <$> runExceptT (getDeleteEmpty manager baseUrl)) `shouldReturn` Right NoContent
276+ (left show <$> (runClientM getDeleteEmpty (ClientEnv manager baseUrl))) `shouldReturn` Right NoContent
284 it "allows content type" $ \baseUrl -> do
278 it "allows content type" $ \baseUrl -> do
285− (left show <$> runExceptT (getDeleteContentType manager baseUrl)) `shouldReturn` Right NoContent
279+ (left show <$> (runClientM getDeleteContentType (ClientEnv manager baseUrl))) `shouldReturn` Right NoContent
287 it "Servant.API.Capture" $ \baseUrl -> do
281 it "Servant.API.Capture" $ \baseUrl -> do
288− (left show <$> runExceptT (getCapture "Paula" manager baseUrl)) `shouldReturn` Right (Person "Paula" 0)
282+ (left show <$> (runClientM (getCapture "Paula") (ClientEnv manager baseUrl))) `shouldReturn` Right (Person "Paula" 0)
284+ it "Servant.API.CaptureAll" $ \baseUrl -> do
285+ let expected = [(Person "Paula" 0), (Person "Peta" 1)]
286+ (left show <$> (runClientM (getCaptureAll ["Paula", "Peta"]) (ClientEnv manager baseUrl))) `shouldReturn` Right expected
290 it "Servant.API.ReqBody" $ \baseUrl -> do
288 it "Servant.API.ReqBody" $ \baseUrl -> do
291 let p = Person "Clara" 42
289 let p = Person "Clara" 42
292− (left show <$> runExceptT (getBody p manager baseUrl)) `shouldReturn` Right p
290+ (left show <$> runClientM (getBody p) (ClientEnv manager baseUrl)) `shouldReturn` Right p
294 it "Servant.API.QueryParam" $ \baseUrl -> do
292 it "Servant.API.QueryParam" $ \baseUrl -> do
295− left show <$> runExceptT (getQueryParam (Just "alice") manager baseUrl) `shouldReturn` Right alice
293+ left show <$> runClientM (getQueryParam (Just "alice")) (ClientEnv manager baseUrl) `shouldReturn` Right alice
296− Left FailureResponse{..} <- runExceptT (getQueryParam (Just "bob") manager baseUrl)
294+ Left FailureResponse{..} <- runClientM (getQueryParam (Just "bob")) (ClientEnv manager baseUrl)
297 responseStatus `shouldBe` HTTP.Status 400 "bob not found"
295 responseStatus `shouldBe` HTTP.Status 400 "bob not found"
299 it "Servant.API.QueryParam.QueryParams" $ \baseUrl -> do
297 it "Servant.API.QueryParam.QueryParams" $ \baseUrl -> do
300− (left show <$> runExceptT (getQueryParams [] manager baseUrl)) `shouldReturn` Right []
298+ (left show <$> runClientM (getQueryParams []) (ClientEnv manager baseUrl)) `shouldReturn` Right []
301− (left show <$> runExceptT (getQueryParams ["alice", "bob"] manager baseUrl))
299+ (left show <$> runClientM (getQueryParams ["alice", "bob"]) (ClientEnv manager baseUrl))
302 `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
300 `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
304 context "Servant.API.QueryParam.QueryFlag" $
302 context "Servant.API.QueryParam.QueryFlag" $
305 forM_ [False, True] $ \ flag -> it (show flag) $ \baseUrl -> do
303 forM_ [False, True] $ \ flag -> it (show flag) $ \baseUrl -> do
306− (left show <$> runExceptT (getQueryFlag flag manager baseUrl)) `shouldReturn` Right flag
304+ (left show <$> runClientM (getQueryFlag flag) (ClientEnv manager baseUrl)) `shouldReturn` Right flag
308 it "Servant.API.Raw on success" $ \baseUrl -> do
306 it "Servant.API.Raw on success" $ \baseUrl -> do
309− res <- runExceptT (getRawSuccess HTTP.methodGet manager baseUrl)
307+ res <- runClientM (getRawSuccess HTTP.methodGet) (ClientEnv manager baseUrl)
310 case res of
308 case res of
311 Left e -> assertFailure $ show e
309 Left e -> assertFailure $ show e
312 Right (code, body, ct, _, response) -> do
310 Right (code, body, ct, _, response) -> do
@@ -315,7 +313,7 @@ sucessSpec = around (withTestServer "server") $ do
315 C.responseStatus response `shouldBe` HTTP.ok200
313 C.responseStatus response `shouldBe` HTTP.ok200
317 it "Servant.API.Raw should return a Left in case of failure" $ \baseUrl -> do
315 it "Servant.API.Raw should return a Left in case of failure" $ \baseUrl -> do
318− res <- runExceptT (getRawFailure HTTP.methodGet manager baseUrl)
316+ res <- runClientM (getRawFailure HTTP.methodGet) (ClientEnv manager baseUrl)
319 case res of
317 case res of
320 Right _ -> assertFailure "expected Left, but got Right"
318 Right _ -> assertFailure "expected Left, but got Right"
321 Left e -> do
319 Left e -> do
@@ -323,7 +321,7 @@ sucessSpec = around (withTestServer "server") $ do
323 Servant.Client.responseBody e `shouldBe` "rawFailure"
321 Servant.Client.responseBody e `shouldBe` "rawFailure"
325 it "Returns headers appropriately" $ \baseUrl -> do
323 it "Returns headers appropriately" $ \baseUrl -> do
326− res <- runExceptT (getRespHeaders manager baseUrl)
324+ res <- runClientM getRespHeaders (ClientEnv manager baseUrl)
327 case res of
325 case res of
328 Left e -> assertFailure $ show e
326 Left e -> assertFailure $ show e
329 Right val -> getHeaders val `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]
327 Right val -> getHeaders val `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]
@@ -332,7 +330,7 @@ sucessSpec = around (withTestServer "server") $ do
332 it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $ \baseUrl ->
330 it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $ \baseUrl ->
333 property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->
331 property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->
334 ioProperty $ do
332 ioProperty $ do
335− result <- left show <$> runExceptT (getMultiple cap num flag body manager baseUrl)
333+ result <- left show <$> runClientM (getMultiple cap num flag body) (ClientEnv manager baseUrl)
337 result === Right (cap, num, flag, body)
335 result === Right (cap, num, flag, body)
@@ -358,9 +356,9 @@ errorSpec =
358 it "reports error statuses correctly" $ \baseUrl -> do
356 it "reports error statuses correctly" $ \baseUrl -> do
359 let delete :<|> get :<|> post :<|> put =
357 let delete :<|> get :<|> post :<|> put =
360 client errorApi
358 client errorApi
361− actions = map (\ f -> f manager baseUrl) [delete, get, post, put]
359+ actions = [delete, get, post, put]
362 forM_ actions $ \ clientAction -> do
360 forM_ actions $ \ clientAction -> do
363− Left FailureResponse{..} <- runExceptT clientAction
361+ Left FailureResponse{..} <- runClientM clientAction (ClientEnv manager baseUrl)
364 responseStatus `shouldBe` HTTP.Status 500 "error message"
362 responseStatus `shouldBe` HTTP.Status 500 "error message"
366basicAuthSpec :: Spec
364basicAuthSpec :: Spec
@@ -370,14 +368,14 @@ basicAuthSpec = around (withTestServer "basicAuthServer") $ do
370 it "Authenticates a BasicAuth protected server appropriately" $ \baseUrl -> do
368 it "Authenticates a BasicAuth protected server appropriately" $ \baseUrl -> do
371 let getBasic = client basicAuthAPI
369 let getBasic = client basicAuthAPI
372 let basicAuthData = BasicAuthData "servant" "server"
370 let basicAuthData = BasicAuthData "servant" "server"
373− (left show <$> runExceptT (getBasic basicAuthData manager baseUrl)) `shouldReturn` Right alice
371+ (left show <$> runClientM (getBasic basicAuthData) (ClientEnv manager baseUrl)) `shouldReturn` Right alice
375 context "Authentication is rejected when requests are not authenticated properly" $ do
373 context "Authentication is rejected when requests are not authenticated properly" $ do
377 it "Authenticates a BasicAuth protected server appropriately" $ \baseUrl -> do
375 it "Authenticates a BasicAuth protected server appropriately" $ \baseUrl -> do
378 let getBasic = client basicAuthAPI
376 let getBasic = client basicAuthAPI
379 let basicAuthData = BasicAuthData "not" "password"
377 let basicAuthData = BasicAuthData "not" "password"
380− Left FailureResponse{..} <- runExceptT (getBasic basicAuthData manager baseUrl)
378+ Left FailureResponse{..} <- runClientM (getBasic basicAuthData) (ClientEnv manager baseUrl)
381 responseStatus `shouldBe` HTTP.Status 403 "Forbidden"
379 responseStatus `shouldBe` HTTP.Status 403 "Forbidden"
383genAuthSpec :: Spec
381genAuthSpec :: Spec
@@ -387,14 +385,14 @@ genAuthSpec = around (withTestServer "genAuthServer") $ do
387 it "Authenticates a AuthProtect protected server appropriately" $ \baseUrl -> do
385 it "Authenticates a AuthProtect protected server appropriately" $ \baseUrl -> do
388 let getProtected = client genAuthAPI
386 let getProtected = client genAuthAPI
389 let authRequest = mkAuthenticateReq () (\_ req -> SCR.addHeader "AuthHeader" ("cool" :: String) req)
387 let authRequest = mkAuthenticateReq () (\_ req -> SCR.addHeader "AuthHeader" ("cool" :: String) req)
390− (left show <$> runExceptT (getProtected authRequest manager baseUrl)) `shouldReturn` Right alice
388+ (left show <$> runClientM (getProtected authRequest) (ClientEnv manager baseUrl)) `shouldReturn` Right alice
392 context "Authentication is rejected when requests are not authenticated properly" $ do
390 context "Authentication is rejected when requests are not authenticated properly" $ do
394 it "Authenticates a AuthProtect protected server appropriately" $ \baseUrl -> do
392 it "Authenticates a AuthProtect protected server appropriately" $ \baseUrl -> do
395 let getProtected = client genAuthAPI
393 let getProtected = client genAuthAPI
396 let authRequest = mkAuthenticateReq () (\_ req -> SCR.addHeader "Wrong" ("header" :: String) req)
394 let authRequest = mkAuthenticateReq () (\_ req -> SCR.addHeader "Wrong" ("header" :: String) req)
397− Left FailureResponse{..} <- runExceptT (getProtected authRequest manager baseUrl)
395+ Left FailureResponse{..} <- runClientM (getProtected authRequest) (ClientEnv manager baseUrl)
398 responseStatus `shouldBe` (HTTP.Status 401 "Unauthorized")
396 responseStatus `shouldBe` (HTTP.Status 401 "Unauthorized")
400failSpec :: Spec
398failSpec :: Spec
@@ -403,35 +401,36 @@ failSpec = around (withTestServer "failServer") $ do
403 context "client returns errors appropriately" $ do
401 context "client returns errors appropriately" $ do
404 it "reports FailureResponse" $ \baseUrl -> do
402 it "reports FailureResponse" $ \baseUrl -> do
405 let (_ :<|> getDeleteEmpty :<|> _) = client api
403 let (_ :<|> getDeleteEmpty :<|> _) = client api
406− Left res <- runExceptT (getDeleteEmpty manager baseUrl)
404+ Left res <- runClientM getDeleteEmpty (ClientEnv manager baseUrl)
407 case res of
405 case res of
408 FailureResponse (HTTP.Status 404 "Not Found") _ _ -> return ()
406 FailureResponse (HTTP.Status 404 "Not Found") _ _ -> return ()
409 _ -> fail $ "expected 404 response, but got " <> show res
407 _ -> fail $ "expected 404 response, but got " <> show res
411 it "reports DecodeFailure" $ \baseUrl -> do
409 it "reports DecodeFailure" $ \baseUrl -> do
412 let (_ :<|> _ :<|> getCapture :<|> _) = client api
410 let (_ :<|> _ :<|> getCapture :<|> _) = client api
413− Left res <- runExceptT (getCapture "foo" manager baseUrl)
411+ Left res <- runClientM (getCapture "foo") (ClientEnv manager baseUrl)
414 case res of
412 case res of
415 DecodeFailure _ ("application/json") _ -> return ()
413 DecodeFailure _ ("application/json") _ -> return ()
416 _ -> fail $ "expected DecodeFailure, but got " <> show res
414 _ -> fail $ "expected DecodeFailure, but got " <> show res
418 it "reports ConnectionError" $ \_ -> do
416 it "reports ConnectionError" $ \_ -> do
419 let (getGetWrongHost :<|> _) = client api
417 let (getGetWrongHost :<|> _) = client api
420− Left res <- runExceptT (getGetWrongHost manager (BaseUrl Http "127.0.0.1" 19872 ""))
418+ Left res <- runClientM getGetWrongHost (ClientEnv manager (BaseUrl Http "127.0.0.1" 19872 ""))
421 case res of
419 case res of
422 ConnectionError _ -> return ()
420 ConnectionError _ -> return ()
423 _ -> fail $ "expected ConnectionError, but got " <> show res
421 _ -> fail $ "expected ConnectionError, but got " <> show res
425 it "reports UnsupportedContentType" $ \baseUrl -> do
423 it "reports UnsupportedContentType" $ \baseUrl -> do
426 let (getGet :<|> _ ) = client api
424 let (getGet :<|> _ ) = client api
427− Left res <- runExceptT (getGet manager baseUrl)
425+ Left res <- runClientM getGet (ClientEnv manager baseUrl)
428 case res of
426 case res of
429 UnsupportedContentType ("application/octet-stream") _ -> return ()
427 UnsupportedContentType ("application/octet-stream") _ -> return ()
430 _ -> fail $ "expected UnsupportedContentType, but got " <> show res
428 _ -> fail $ "expected UnsupportedContentType, but got " <> show res
432 it "reports InvalidContentTypeHeader" $ \baseUrl -> do
430 it "reports InvalidContentTypeHeader" $ \baseUrl -> do
433− let (_ :<|> _ :<|> _ :<|> getBody :<|> _) = client api
431+ let (_ :<|> _ :<|> _ :<|> _ :<|> getBody :<|> _) = client api
434− Left res <- runExceptT (getBody alice manager baseUrl)
432+ Left res <- runClientM (getBody alice) (ClientEnv manager baseUrl)
435 case res of
434 case res of
436 InvalidContentTypeHeader "fooooo" _ -> return ()
435 InvalidContentTypeHeader "fooooo" _ -> return ()
437 _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res
436 _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res