@@ -5,9 +5,13 @@
5{-# LANGUAGE TypeOperators #-}
5{-# LANGUAGE TypeOperators #-}
6{-# LANGUAGE RecordWildCards #-}
6{-# LANGUAGE RecordWildCards #-}
7{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE StandaloneDeriving #-}
8{-# OPTIONS_GHC -fcontext-stack=25 #-}
9{-# OPTIONS_GHC -fcontext-stack=25 #-}
10+{-# OPTIONS_GHC -fno-warn-orphans #-}
9module Servant.ClientSpec where
11module Servant.ClientSpec where
13+import Control.Applicative
14+import qualified Control.Arrow as Arrow
11import Control.Concurrent
15import Control.Concurrent
12import Control.Exception
16import Control.Exception
13import Control.Monad.Trans.Either
17import Control.Monad.Trans.Either
@@ -19,6 +23,7 @@ import Data.Monoid
19import Data.Proxy
23import Data.Proxy
20import qualified Data.Text as T
24import qualified Data.Text as T
21import GHC.Generics
25import GHC.Generics
26+import Network.HTTP.Client (HttpException(..))
22import Network.HTTP.Media
27import Network.HTTP.Media
23import Network.HTTP.Types
28import Network.HTTP.Types
24import Network.Socket
29import Network.Socket
@@ -29,7 +34,6 @@ import Test.Hspec.QuickCheck
29import Test.QuickCheck
34import Test.QuickCheck
31import Servant.API
36import Servant.API
32−import Servant.API.ContentTypes
33import Servant.Client
37import Servant.Client
34import Servant.Server
38import Servant.Server
@@ -58,6 +62,10 @@ instance FromFormUrlEncoded Person where
58 a <- lookupEither "age" xs
62 a <- lookupEither "age" xs
59 return $ Person (T.unpack n) (read $ T.unpack a)
63 return $ Person (T.unpack n) (read $ T.unpack a)
65+deriving instance Eq ServantError
67+instance Eq HttpException where
68+ a == b = show a == show b
62alice :: Person
70alice :: Person
63alice = Person "Alice" 42
71alice = Person "Alice" 42
@@ -110,21 +118,21 @@ server = serve api (
110withServer :: (BaseUrl -> IO a) -> IO a
118withServer :: (BaseUrl -> IO a) -> IO a
111withServer action = withWaiDaemon (return server) action
119withServer action = withWaiDaemon (return server) action
113−getGet :: BaseUrl -> EitherT String IO Person
121+getGet :: BaseUrl -> EitherT ServantError IO Person
114−getDelete :: BaseUrl -> EitherT String IO ()
122+getDelete :: BaseUrl -> EitherT ServantError IO ()
115−getCapture :: String -> BaseUrl -> EitherT String IO Person
123+getCapture :: String -> BaseUrl -> EitherT ServantError IO Person
116−getBody :: Person -> BaseUrl -> EitherT String IO Person
124+getBody :: Person -> BaseUrl -> EitherT ServantError IO Person
117−getQueryParam :: Maybe String -> BaseUrl -> EitherT String IO Person
125+getQueryParam :: Maybe String -> BaseUrl -> EitherT ServantError IO Person
118−getQueryParams :: [String] -> BaseUrl -> EitherT String IO [Person]
126+getQueryParams :: [String] -> BaseUrl -> EitherT ServantError IO [Person]
119−getQueryFlag :: Bool -> BaseUrl -> EitherT String IO Bool
127+getQueryFlag :: Bool -> BaseUrl -> EitherT ServantError IO Bool
120−getMatrixParam :: Maybe String -> BaseUrl -> EitherT String IO Person
128+getMatrixParam :: Maybe String -> BaseUrl -> EitherT ServantError IO Person
121−getMatrixParams :: [String] -> BaseUrl -> EitherT String IO [Person]
129+getMatrixParams :: [String] -> BaseUrl -> EitherT ServantError IO [Person]
122−getMatrixFlag :: Bool -> BaseUrl -> EitherT String IO Bool
130+getMatrixFlag :: Bool -> BaseUrl -> EitherT ServantError IO Bool
123−getRawSuccess :: Method -> BaseUrl -> EitherT String IO (Int, ByteString, MediaType)
131+getRawSuccess :: Method -> BaseUrl -> EitherT ServantError IO (Int, ByteString, MediaType)
124−getRawFailure :: Method -> BaseUrl -> EitherT String IO (Int, ByteString, MediaType)
132+getRawFailure :: Method -> BaseUrl -> EitherT ServantError IO (Int, ByteString, MediaType)
125getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
133getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
126 -> BaseUrl
134 -> BaseUrl
127− -> EitherT String IO (String, Maybe Int, Bool, [(String, [Rational])])
135+ -> EitherT ServantError IO (String, Maybe Int, Bool, [(String, [Rational])])
129 :<|> getDelete
137 :<|> getDelete
130 :<|> getCapture
138 :<|> getCapture
@@ -140,65 +148,82 @@ getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
140 :<|> getMultiple)
148 :<|> getMultiple)
141 = client api
149 = client api
153+ :<|> "capture" :> Capture "name" String :> Raw
155+failApi :: Proxy FailApi
158+failServer :: Application
159+failServer = serve failApi (
160+ (\ _request respond -> respond $ responseLBS ok200 [] "")
161+ :<|> (\ _capture _request respond -> respond $ responseLBS ok200 [("content-type", "application/json")] "")
162+ :<|> (\_request respond -> respond $ responseLBS ok200 [("content-type", "fooooo")] "")
165+withFailServer :: (BaseUrl -> IO a) -> IO a
166+withFailServer action = withWaiDaemon (return failServer) action
143spec :: Spec
168spec :: Spec
145 it "Servant.API.Get" $ withServer $ \ host -> do
170 it "Servant.API.Get" $ withServer $ \ host -> do
146− runEitherT (getGet host) `shouldReturn` Right alice
171+ (Arrow.left show <$> runEitherT (getGet host)) `shouldReturn` Right alice
148 it "Servant.API.Delete" $ withServer $ \ host -> do
173 it "Servant.API.Delete" $ withServer $ \ host -> do
149− runEitherT (getDelete host) `shouldReturn` Right ()
174+ (Arrow.left show <$> runEitherT (getDelete host)) `shouldReturn` Right ()
151 it "Servant.API.Capture" $ withServer $ \ host -> do
176 it "Servant.API.Capture" $ withServer $ \ host -> do
152− runEitherT (getCapture "Paula" host) `shouldReturn` Right (Person "Paula" 0)
177+ (Arrow.left show <$> runEitherT (getCapture "Paula" host)) `shouldReturn` Right (Person "Paula" 0)
154 it "Servant.API.ReqBody" $ withServer $ \ host -> do
179 it "Servant.API.ReqBody" $ withServer $ \ host -> do
155 let p = Person "Clara" 42
180 let p = Person "Clara" 42
156− runEitherT (getBody p host) `shouldReturn` Right p
181+ (Arrow.left show <$> runEitherT (getBody p host)) `shouldReturn` Right p
158 it "Servant.API.QueryParam" $ withServer $ \ host -> do
183 it "Servant.API.QueryParam" $ withServer $ \ host -> do
159− runEitherT (getQueryParam (Just "alice") host) `shouldReturn` Right alice
184+ Arrow.left show <$> runEitherT (getQueryParam (Just "alice") host) `shouldReturn` Right alice
160− Left result <- runEitherT (getQueryParam (Just "bob") host)
185+ Left FailureResponse{..} <- runEitherT (getQueryParam (Just "bob") host)
161− result `shouldContain` "bob not found"
186+ responseStatus `shouldBe` Status 400 "bob not found"
163 it "Servant.API.QueryParam.QueryParams" $ withServer $ \ host -> do
188 it "Servant.API.QueryParam.QueryParams" $ withServer $ \ host -> do
164− runEitherT (getQueryParams [] host) `shouldReturn` Right []
189+ (Arrow.left show <$> runEitherT (getQueryParams [] host)) `shouldReturn` Right []
165− runEitherT (getQueryParams ["alice", "bob"] host)
190+ (Arrow.left show <$> runEitherT (getQueryParams ["alice", "bob"] host))
166 `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
191 `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
168 context "Servant.API.QueryParam.QueryFlag" $
193 context "Servant.API.QueryParam.QueryFlag" $
169 forM_ [False, True] $ \ flag ->
194 forM_ [False, True] $ \ flag ->
170 it (show flag) $ withServer $ \ host -> do
195 it (show flag) $ withServer $ \ host -> do
171− runEitherT (getQueryFlag flag host) `shouldReturn` Right flag
196+ (Arrow.left show <$> runEitherT (getQueryFlag flag host)) `shouldReturn` Right flag
174 it "Servant.API.MatrixParam" $ withServer $ \ host -> do
198 it "Servant.API.MatrixParam" $ withServer $ \ host -> do
175− runEitherT (getMatrixParam (Just "alice") host) `shouldReturn` Right alice
199+ Arrow.left show <$> runEitherT (getMatrixParam (Just "alice") host) `shouldReturn` Right alice
176− Left result <- runEitherT (getMatrixParam (Just "bob") host)
200+ Left FailureResponse{..} <- runEitherT (getMatrixParam (Just "bob") host)
177− result `shouldContain` "bob not found"
201+ responseStatus `shouldBe` Status 400 "bob not found"
179 it "Servant.API.MatrixParam.MatrixParams" $ withServer $ \ host -> do
203 it "Servant.API.MatrixParam.MatrixParams" $ withServer $ \ host -> do
180− runEitherT (getMatrixParams [] host) `shouldReturn` Right []
204+ Arrow.left show <$> runEitherT (getMatrixParams [] host) `shouldReturn` Right []
181− runEitherT (getMatrixParams ["alice", "bob"] host)
205+ Arrow.left show <$> runEitherT (getMatrixParams ["alice", "bob"] host)
182 `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
206 `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
184 context "Servant.API.MatrixParam.MatrixFlag" $
208 context "Servant.API.MatrixParam.MatrixFlag" $
185 forM_ [False, True] $ \ flag ->
209 forM_ [False, True] $ \ flag ->
186 it (show flag) $ withServer $ \ host -> do
210 it (show flag) $ withServer $ \ host -> do
187− runEitherT (getMatrixFlag flag host) `shouldReturn` Right flag
211+ Arrow.left show <$> runEitherT (getMatrixFlag flag host) `shouldReturn` Right flag
190 it "Servant.API.Raw on success" $ withServer $ \ host -> do
213 it "Servant.API.Raw on success" $ withServer $ \ host -> do
214+ (Arrow.left show <$> runEitherT (getRawSuccess methodGet host))
191− runEitherT (getRawSuccess methodGet host) `shouldReturn` Right (200, "rawSuccess", "application"//"octet-stream")
215+ `shouldReturn` Right (200, "rawSuccess", "application"//"octet-stream")
193 it "Servant.API.Raw on failure" $ withServer $ \ host -> do
217 it "Servant.API.Raw on failure" $ withServer $ \ host -> do
218+ (Arrow.left show <$> runEitherT (getRawFailure methodGet host))
194− runEitherT (getRawFailure methodGet host) `shouldReturn` Right (400, "rawFailure", "application"//"octet-stream")
219+ `shouldReturn` Right (400, "rawFailure", "application"//"octet-stream")
196 modifyMaxSuccess (const 20) $ do
221 modifyMaxSuccess (const 20) $ do
197 it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $
222 it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $
198 property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->
223 property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->
199 ioProperty $ do
224 ioProperty $ do
200 withServer $ \ host -> do
225 withServer $ \ host -> do
201− result <- runEitherT (getMultiple cap num flag body host)
226+ result <- Arrow.left show <$> runEitherT (getMultiple cap num flag body host)
203 result === Right (cap, num, flag, body)
228 result === Right (cap, num, flag, body)
@@ -209,20 +234,52 @@ spec = do
209 it desc $
234 it desc $
210 withWaiDaemon (return (serve api (left (500, "error message")))) $
235 withWaiDaemon (return (serve api (left (500, "error message")))) $
211 \ host -> do
236 \ host -> do
212− let getResponse :: BaseUrl -> EitherT String IO ()
237+ let getResponse :: BaseUrl -> EitherT ServantError IO ()
213 getResponse = client api
238 getResponse = client api
214− Left result <- runEitherT (getResponse host)
239+ Left FailureResponse{..} <- runEitherT (getResponse host)
215− result `shouldContain` "error message"
240+ responseStatus `shouldBe` (Status 500 "error message")
216 mapM_ test $
241 mapM_ test $
217 (WrappedApi (Proxy :: Proxy Delete), "Delete") :
242 (WrappedApi (Proxy :: Proxy Delete), "Delete") :
218− (WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Delete") :
243+ (WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Get") :
219− (WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Delete") :
244+ (WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Post") :
220− (WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Delete") :
245+ (WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Put") :
248+ context "client returns errors appropriately" $ do
249+ it "reports FailureResponse" $ withFailServer $ \ host -> do
250+ Left res <- runEitherT (getDelete host)
252+ FailureResponse (Status 404 "Not Found") _ _ -> return ()
253+ _ -> fail $ "expected 404 response, but got " <> show res
255+ it "reports DecodeFailure" $ withFailServer $ \ host -> do
256+ Left res <- runEitherT (getCapture "foo" host)
258+ DecodeFailure _ ("application/json") _ -> return ()
259+ _ -> fail $ "expected DecodeFailure, but got " <> show res
261+ it "reports ConnectionError" $ do
262+ Right host <- return $ parseBaseUrl "127.0.0.1:987654"
263+ Left res <- runEitherT (getGet host)
265+ ConnectionError (FailedConnectionException2 "127.0.0.1" 987654 False _) -> return ()
266+ _ -> fail $ "expected ConnectionError, but got " <> show res
268+ it "reports UnsupportedContentType" $ withFailServer $ \ host -> do
269+ Left res <- runEitherT (getGet host)
271+ UnsupportedContentType ("application/octet-stream") _ -> return ()
272+ _ -> fail $ "expected UnsupportedContentType, but got " <> show res
274+ it "reports InvalidContentTypeHeader" $ withFailServer $ \ host -> do
275+ Left res <- runEitherT (getBody alice host)
277+ InvalidContentTypeHeader "fooooo" _ -> return ()
278+ _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res
223data WrappedApi where
280data WrappedApi where
224 WrappedApi :: (HasServer api, Server api ~ EitherT (Int, String) IO a,
281 WrappedApi :: (HasServer api, Server api ~ EitherT (Int, String) IO a,
225− HasClient api, Client api ~ (BaseUrl -> EitherT String IO ())) =>
282+ HasClient api, Client api ~ (BaseUrl -> EitherT ServantError IO ())) =>
226 Proxy api -> WrappedApi
283 Proxy api -> WrappedApi