packages feed

servant-client 0.16.0.1 → 0.20.3.0

raw patch · 21 files changed

Files

CHANGELOG.md view
@@ -1,6 +1,174 @@ [The latest version of this document is on GitHub.](https://github.com/haskell-servant/servant/blob/master/servant-client/CHANGELOG.md) [Changelog for `servant` package contains significant entries for all core packages.](https://github.com/haskell-servant/servant/blob/master/servant/CHANGELOG.md) +Package versions follow the [Package Versioning Policy](https://pvp.haskell.org/): in A.B.C, bumps to either A or B represent major versions.++0.20.3.0+--------++- Server-sent events (SSE) for client-side [#1811](https://github.com/haskell-servant/servant/issues/1811)++  Implement Server-sent events (SSE) for the Servant client using a new+  combinator "ServerSentEvents". The raw event messages, accumulated events and+  JSON-processed events can be exposed.++- Integrate MultiVerb [#1766](https://github.com/haskell-servant/servant/pull/1766) [#1804](https://github.com/haskell-servant/servant/pull/1804)++  Expose MultiVerb, a more ergonomic way of defining endpoints that return+  many kinds of responses. Read the cookbook https://docs.servant.dev/en/master/cookbook/multiverb/MultiVerb.html++- Add public re-export of renderCurlBasePath lens [#1706](https://github.com/haskell-servant/servant/pull/1706)+- Remove GHC <= 8.10.7 from the support window [#1778](https://github.com/haskell-servant/servant/pull/1778)+- Add Servant.API.Range type [#1805](https://github.com/haskell-servant/servant/pull/1805)+- Add missing HasLink instance for DeepQuery [#1784](https://github.com/haskell-servant/servant/issues/1784) [#1814](https://github.com/haskell-servant/servant/pull/1814)+- Add Host API combinator [#1800](https://github.com/haskell-servant/servant/pull/1800)++  Adding a Host combinator allows servant users to select APIs according+  to the Host header provided by clients.++0.20.2+------++- Client Middleware [#1720](https://github.com/haskell-servant/servant/pull/1720)++  Clients now support real middleware of type `(Request -> ClientM Response) -> Request -> ClientM Response` which can be configured in `ClientEnv`.+  This allows access to raw request and response data. It can also be used to control how/when/if actual requests are performed.+  Middleware can be chained with function composition `mid1 . mid2 . mid3`.++0.20+----++- Escape special chars in QueryParams. [#1584](https://github.com/haskell-servant/servant/issues/1584) [#1597](https://github.com/haskell-servant/servant/pull/1597)++  Escape special chars in QueryParam (`:@&=+$`) in servant-client. Note that this+  mean binary data will not work as is, and so reverts the functionality in+  [#1432](https://github.com/haskell-servant/servant/pull/1432).++- Handle Cookies correctly for RunStreamingClient [#1605](https://github.com/haskell-servant/servant/issues/1605) [#1606](https://github.com/haskell-servant/servant/pull/1606)++  Makes `performWithStreamingRequest` take into consideration the+  CookieJar, which it previously didn't.++- Fix the handling of multiple headers with the same name. [#1666](https://github.com/haskell-servant/servant/pull/1666)++  servant-client no longer concatenates the values of response headers with the same name.+  This fixes an issue with parsing multiple `Set-Cookie` headers.++0.19+----++### Significant changes++- Drop support for GHC < 8.6.+- Support GHC 9.0 (GHC 9.2 should work as well, but isn't fully tested yet).+- Support Aeson 2 ([#1475](https://github.com/haskell-servant/servant/pull/1475)),+  which fixes a [DOS vulnerability](https://github.com/haskell/aeson/issues/864)+  related to hash collisions.+- Add `NamedRoutes` combinator, making support for records first-class in Servant+  ([#1388](https://github.com/haskell-servant/servant/pull/1388)).+- Add custom type errors for partially applied combinators+  ([#1289](https://github.com/haskell-servant/servant/pull/1289),+  [#1486](https://github.com/haskell-servant/servant/pull/1486)).+- *servant-client* / *servant-client-core* / *servant-http-streams*: Fix+  erroneous behavior, where only 2XX status codes would be considered+  successful, irrelevant of the status parameter specified by the verb+  combinator. ([#1469](https://github.com/haskell-servant/servant/pull/1469))+- *servant-client* / *servant-client-core*: Fix `Show` instance for+  `Servant.Client.Core.Request`.+- *servant-client* /  *servant-client-core*: Allow passing arbitrary binary data+  in Query parameters.+  ([#1432](https://github.com/haskell-servant/servant/pull/1432)).++### Other changes++- Various version bumps.++0.18.3+------++### Significant changes++- Add response header support to UVerb (#1420)++### Other changes++- Support GHC-9.0.1.+- Bump `bytestring`, `hspec`, `http-client` and `QuickCheck` dependencies.++0.18.2+------++### Significant changes++- Support `Fragment` combinator.++0.18.1+------++### Significant changes++- Union verbs++### Other changes++- Bump "tested-with" ghc versions++0.18+----++### Significant changes++- Support for ghc8.8 (#1318, #1326, #1327)+++0.17+----++### Significant changes++- Add NoContentVerb [#1028](https://github.com/haskell-servant/servant/issues/1028) [#1219](https://github.com/haskell-servant/servant/pull/1219) [#1228](https://github.com/haskell-servant/servant/pull/1228)++  The `NoContent` API endpoints should now use `NoContentVerb` combinator.+  The API type changes are usually of the kind++  ```diff+  - :<|> PostNoContent '[JSON] NoContent+  + :<|> PostNoContent+  ```++  i.e. one doesn't need to specify the content-type anymore. There is no content.++- `Capture` can be `Lenient` [#1155](https://github.com/haskell-servant/servant/issues/1155) [#1156](https://github.com/haskell-servant/servant/pull/1156)++  You can specify a lenient capture as++  ```haskell+  :<|> "capture-lenient"  :> Capture' '[Lenient] "foo" Int :> GET+  ```++  which will make the capture always succeed. Handlers will be of the+  type `Either String CapturedType`, where `Left err` represents+  the possible parse failure.++- *servant-client* Added a function to create Client.Request in ClientEnv [#1213](https://github.com/haskell-servant/servant/pull/1213) [#1255](https://github.com/haskell-servant/servant/pull/1255)++  The new member `makeClientRequest` of `ClientEnv` is used to create+  `http-client` `Request` from `servant-client-core` `Request`.+  This functionality can be used for example to set+  dynamic timeouts for each request.++### Other changes++- *servant-client* *servant-client-core* *servant-http-streams* Fix Verb with headers checking content type differently [#1200](https://github.com/haskell-servant/servant/issues/1200) [#1204](https://github.com/haskell-servant/servant/pull/1204)++  For `Verb`s with response `Headers`, the implementation didn't check+  for the content-type of the response. Now it does.++- *servant-client* *servant-http-streams* `HasClient` instance for `Stream` with `Headers` [#1170](https://github.com/haskell-servant/servant/issues/1170) [#1197](https://github.com/haskell-servant/servant/pull/1197)+- *servant-client* Redact the authorization header in Show and exceptions [#1238](https://github.com/haskell-servant/servant/pull/1238)+++ 0.16.0.1 -------- @@ -150,7 +318,7 @@  - *servant-client-core* Add `hoistClient` to `HasClient`.   Just like `hoistServer` allows us to change the monad in which request handlers-  of a web application live in, we also have `hoistClient` for changing the monad+  of a web application live, we also have `hoistClient` for changing the monad   in which *client functions* live.   Read [tutorial section for more information](https://docs.servant.dev/en/release-0.14/tutorial/Client.html#changing-the-monad-the-client-functions-live-in).   ([#936](https://github.com/haskell-servant/servant/pull/936))
README.lhs view
@@ -11,7 +11,7 @@ {-# LANGUAGE TypeOperators #-}  import Data.Proxy-import Data.Text+import Data.Text (Text) import Network.HTTP.Client (newManager, defaultManagerSettings) import Servant.API import Servant.Client
README.md view
@@ -11,7 +11,7 @@ {-# LANGUAGE TypeOperators #-}  import Data.Proxy-import Data.Text+import Data.Text (Text) import Network.HTTP.Client (newManager, defaultManagerSettings) import Servant.API import Servant.Client
servant-client.cabal view
@@ -1,9 +1,8 @@-cabal-version:       >=1.10-name:                servant-client-version:             0.16.0.1--synopsis:            Automatic derivation of querying functions for servant-category:            Servant, Web+cabal-version:      3.0+name:               servant-client+version:            0.20.3.0+synopsis:           Automatic derivation of querying functions for servant+category:           Servant, Web description:   This library lets you derive automatically Haskell functions that   let you query each endpoint of a <http://hackage.haskell.org/package/servant servant> webservice.@@ -12,125 +11,190 @@   .   <https://github.com/haskell-servant/servant/blob/master/servant-client/CHANGELOG.md CHANGELOG> -homepage:            http://docs.servant.dev/-bug-reports:         http://github.com/haskell-servant/servant/issues-license:             BSD3-license-file:        LICENSE-author:              Servant Contributors-maintainer:          haskell-servant-maintainers@googlegroups.com-copyright:           2014-2016 Zalora South East Asia Pte Ltd, 2016-2019 Servant Contributors-build-type:          Simple-tested-with:-  GHC ==8.0.2-   || ==8.2.2-   || ==8.4.4-   || ==8.6.5-   || ==8.8.1+homepage:           http://docs.servant.dev/+bug-reports:        http://github.com/haskell-servant/servant/issues+license:            BSD-3-Clause+license-file:       LICENSE+author:             Servant Contributors+maintainer:         haskell-servant-maintainers@googlegroups.com+copyright:+  2014-2016 Zalora South East Asia Pte Ltd, 2016-2019 Servant Contributors +build-type:         Simple+tested-with:        GHC ==9.2.8 || ==9.4.8 || ==9.6.6 || ==9.8.4 || ==9.10.1 || ==9.12.1+ extra-source-files:   CHANGELOG.md   README.md  source-repository head-  type: git+  type:     git   location: http://github.com/haskell-servant/servant.git +common extensions+  default-extensions:+    AllowAmbiguousTypes+    ConstraintKinds+    DataKinds+    DeriveAnyClass+    DeriveDataTypeable+    DeriveFunctor+    DeriveGeneric+    DerivingStrategies+    DerivingVia+    DuplicateRecordFields+    ExplicitNamespaces+    FlexibleContexts+    FlexibleInstances+    FunctionalDependencies+    GADTs+    GeneralizedNewtypeDeriving+    InstanceSigs+    KindSignatures+    LambdaCase+    MultiParamTypeClasses+    NamedFieldPuns+    NoStarIsType+    OverloadedLabels+    OverloadedStrings+    PackageImports+    PolyKinds+    QuantifiedConstraints+    RankNTypes+    RecordWildCards+    ScopedTypeVariables+    StrictData+    TupleSections+    TypeApplications+    TypeFamilies+    TypeOperators+    UndecidableInstances+    ViewPatterns++  default-language:   Haskell2010++common ghc-options+  ghc-options:+    -Wall -Wcompat -Widentities -Wincomplete-record-updates+    -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints+    -fhide-source-paths -Wno-unused-do-bind -fdicts-strict+    -Wno-unticked-promoted-constructors -Werror=unused-imports+    -Wunused-packages+ library+  import:          extensions+  import:          ghc-options   exposed-modules:     Servant.Client-    Servant.Client.Streaming     Servant.Client.Internal.HttpClient     Servant.Client.Internal.HttpClient.Streaming+    Servant.Client.Streaming    -- Bundled with GHC: Lower bound to not force re-installs   -- text and mtl are bundled starting with GHC-8.4   build-depends:-      base                  >= 4.9      && < 4.14-    , bytestring            >= 0.10.8.1 && < 0.11-    , containers            >= 0.5.7.1  && < 0.7-    , deepseq               >= 1.4.2.0  && < 1.5-    , mtl                   >= 2.2.2    && < 2.3-    , stm                   >= 2.4.5.1  && < 2.6-    , text                  >= 1.2.3.0  && < 1.3-    , time                  >= 1.6.0.1  && < 1.10-    , transformers          >= 0.5.2.0  && < 0.6--  if !impl(ghc >= 8.2)-    build-depends:-      bifunctors >= 5.5.3 && < 5.6+    , base          >= 4.16.4.0 && < 4.22+    , bytestring    >=0.11 && <0.13+    , containers    >=0.6.5.1  && <0.9+    , deepseq       >=1.4.2.0  && <1.6+    , mtl           ^>=2.2.2   || ^>=2.3.1+    , stm           >=2.4.5.1  && <2.6+    , time          >=1.6.0.1  && <1.15+    , transformers  >=0.5.2.0  && <0.7    -- Servant dependencies.   -- Strict dependency on `servant-client-core` as we re-export things.   build-depends:-      servant               == 0.16.*-    , servant-client-core   >= 0.16 && <0.16.1+    , servant              >=0.20.2 && <0.21+    , servant-client-core  >=0.20.2 && <0.21    -- Other dependencies: Lower bound around what is in the latest Stackage LTS.   -- Here can be exceptions if we really need features from the newer versions.   build-depends:-      base-compat           >= 0.10.5   && < 0.12-    , http-client           >= 0.5.13.1 && < 0.7-    , http-media            >= 0.7.1.3  && < 0.9-    , http-types            >= 0.12.2   && < 0.13-    , exceptions            >= 0.10.0   && < 0.11-    , kan-extensions        >= 5.2      && < 5.3-    , monad-control         >= 1.0.2.3  && < 1.1-    , semigroupoids         >= 5.3.1    && < 5.4-    , transformers-base     >= 0.4.5.2  && < 0.5-    , transformers-compat   >= 0.6.2    && < 0.7+    , base-compat          >=0.10.5   && <0.15+    , exceptions           >=0.10.0   && <0.11+    , http-client          >=0.5.13.1 && <0.8+    , http-media           >=0.7.1.3  && <0.9+    , http-types           >=0.12.2   && <0.13+    , kan-extensions       >=5.2      && <5.3+    , monad-control        >=1.0.2.3  && <1.1+    , semigroupoids        >=5.3.1    && <6.1+    , transformers-base    >=0.4.5.2  && <0.5 -  hs-source-dirs: src-  default-language: Haskell2010-  ghc-options: -Wall -Wno-redundant-constraints+  hs-source-dirs:  src  test-suite spec-  type: exitcode-stdio-1.0-  ghc-options: -Wall -rtsopts -threaded "-with-rtsopts=-T -N2"-  default-language: Haskell2010-  hs-source-dirs: test-  main-is: Spec.hs+  import:             extensions+  import:             ghc-options+  type:               exitcode-stdio-1.0+  ghc-options:        -Wall -rtsopts -threaded "-with-rtsopts=-T -N2"++  if impl(ghcjs)+    buildable: False++  hs-source-dirs:     test+  main-is:            Spec.hs   other-modules:-      Servant.ClientSpec-      Servant.StreamSpec+    Servant.BasicAuthSpec+    Servant.BrokenSpec+    Servant.ClientTestUtils+    Servant.ConnectionErrorSpec+    Servant.FailSpec+    Servant.GenAuthSpec+    Servant.GenericSpec+    Servant.HoistClientSpec+    Servant.MiddlewareSpec+    Servant.StreamSpec+    Servant.SuccessSpec+    Servant.WrappedApiSpec    -- Dependencies inherited from the library. No need to specify bounds.   build-depends:-      base     , aeson+    , base     , base-compat     , bytestring     , http-api-data     , http-client     , http-types     , mtl-    , kan-extensions     , servant-client     , servant-client-core+    , sop-core+    , generics-sop     , stm     , text     , transformers-    , transformers-compat     , wai     , warp -  -- Additonal dependencies+  -- Additional dependencies   build-depends:-      entropy           >= 0.4.1.3  && < 0.5-    , hspec             >= 2.6.0    && < 2.8-    , HUnit             >= 1.6.0.0  && < 1.7-    , network           >= 2.8.0.0  && < 3.2-    , QuickCheck        >= 2.12.6.1 && < 2.14-    , servant           == 0.16.*-    , servant-server    == 0.16.*-    , tdigest           >= 0.2     && < 0.3+    , entropy         >=0.4.1.3  && <0.5+    , hspec           >=2.6.0    && <2.12+    , HUnit           >=1.6.0.0  && <1.7+    , network         >=2.8.0.0  && <3.3+    , QuickCheck      >=2.12.6.1 && <2.16+    , servant         >=0.20.2   && <0.21+    , servant-server  >=0.20.2   && <0.21 -  build-tool-depends:-    hspec-discover:hspec-discover >= 2.6.0 && < 2.8+  build-tool-depends: hspec-discover:hspec-discover >=2.6.0 && <2.12  test-suite readme-  type:           exitcode-stdio-1.0-  main-is:        README.lhs-  build-depends:  base, servant, http-client, text, servant-client, markdown-unlit+  import:             extensions+  import:             ghc-options+  type:               exitcode-stdio-1.0+  main-is:            README.lhs+  build-depends:+    , base+    , http-client+    , markdown-unlit+    , servant+    , servant-client+    , text+   build-tool-depends: markdown-unlit:markdown-unlit-  ghc-options:    -pgmL markdown-unlit-  default-language: Haskell2010+  ghc-options:        -pgmL markdown-unlit++  if impl(ghcjs)+    buildable: False
src/Servant/Client.hs view
@@ -9,6 +9,7 @@   , runClientM   , ClientEnv(..)   , mkClientEnv+  , defaultMakeClientRequest   , hoistClient   , module Servant.Client.Core.Reexport   ) where
src/Servant/Client/Internal/HttpClient.hs view
@@ -1,14 +1,4 @@ {-# LANGUAGE CPP                        #-}-{-# LANGUAGE DeriveDataTypeable         #-}-{-# LANGUAGE DeriveGeneric              #-}-{-# LANGUAGE FlexibleContexts           #-}-{-# LANGUAGE FlexibleInstances          #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses      #-}-{-# LANGUAGE OverloadedStrings          #-}-{-# LANGUAGE RankNTypes                 #-}-{-# LANGUAGE ScopedTypeVariables        #-}-{-# LANGUAGE TypeFamilies               #-} module Servant.Client.Internal.HttpClient where  import           Prelude ()@@ -24,7 +14,7 @@ import           Control.Monad.Base                  (MonadBase (..)) import           Control.Monad.Catch-                 (MonadCatch, MonadThrow)+                 (MonadCatch, MonadThrow, MonadMask) import           Control.Monad.Error.Class                  (MonadError (..)) import           Control.Monad.IO.Class@@ -43,18 +33,15 @@ import           Data.ByteString.Builder                  (toLazyByteString) import qualified Data.ByteString.Lazy        as BSL-import           Data.Either-                 (either)+import qualified Data.List as List import           Data.Foldable                  (toList) import           Data.Functor.Alt                  (Alt (..)) import           Data.Maybe-                 (maybe, maybeToList)+                 (maybeToList) import           Data.Proxy                  (Proxy (..))-import           Data.Semigroup-                 ((<>)) import           Data.Sequence                  (fromList) import           Data.String@@ -65,23 +52,45 @@ import           Network.HTTP.Media                  (renderHeader) import           Network.HTTP.Types-                 (hContentType, renderQuery, statusCode)+                 (hContentType, statusIsSuccessful, urlEncode, Status) import           Servant.Client.Core  import qualified Network.HTTP.Client         as Client import qualified Servant.Types.SourceT       as S  -- | The environment in which a request is run.+--   The 'baseUrl' and 'makeClientRequest' function are used to create a @http-client@ request.+--   Cookies are then added to that request if a 'CookieJar' is set on the environment.+--   Finally the request is executed with the 'manager'.+--   The 'makeClientRequest' function can be used to modify the request to execute and set values which+--   are not specified on a @servant@ 'Request' like 'responseTimeout' or 'redirectCount' data ClientEnv   = ClientEnv   { manager :: Client.Manager   , baseUrl :: BaseUrl   , cookieJar :: Maybe (TVar Client.CookieJar)+  , makeClientRequest :: BaseUrl -> Request -> IO Client.Request+  -- ^ this function can be used to customize the creation of @http-client@ requests from @servant@ requests. Default value: 'defaultMakeClientRequest'+  --   Note that:+  --      1. 'makeClientRequest' exists to allow overriding operational semantics e.g. 'responseTimeout' per request,+  --          If you need global modifications, you should use 'managerModifyRequest'+  --      2. the 'cookieJar', if defined, is being applied after 'makeClientRequest' is called.+  , middleware :: ClientMiddleware    } +type ClientApplication = Request -> ClientM Response++type ClientMiddleware = ClientApplication -> ClientApplication+ -- | 'ClientEnv' smart constructor. mkClientEnv :: Client.Manager -> BaseUrl -> ClientEnv-mkClientEnv mgr burl = ClientEnv mgr burl Nothing+mkClientEnv manager baseUrl = ClientEnv +  { manager+  , baseUrl+  , cookieJar = Nothing+  , makeClientRequest = defaultMakeClientRequest+  , middleware = id+  }  -- | Generates a set of client functions for an API. --@@ -125,9 +134,9 @@ -- 'Client.Manager' and 'BaseUrl' used for requests in the reader environment. newtype ClientM a = ClientM   { unClientM :: ReaderT ClientEnv (ExceptT ClientError IO) a }-  deriving ( Functor, Applicative, Monad, MonadIO, Generic+  deriving newtype ( Functor, Applicative, Monad, MonadIO, Generic            , MonadReader ClientEnv, MonadError ClientError, MonadThrow-           , MonadCatch)+           , MonadCatch, MonadMask)  instance MonadBase IO ClientM where   liftBase = ClientM . liftBase@@ -144,16 +153,19 @@   a <!> b = a `catchError` \_ -> b  instance RunClient ClientM where-  runRequest = performRequest+  runRequestAcceptStatus statuses req = do+    ClientEnv {middleware} <- ask+    let oldApp = performRequest statuses+    middleware oldApp req   throwClientError = throwError  runClientM :: ClientM a -> ClientEnv -> IO (Either ClientError a) runClientM cm env = runExceptT $ flip runReaderT env $ unClientM cm -performRequest :: Request -> ClientM Response-performRequest req = do-  ClientEnv m burl cookieJar' <- ask-  let clientRequest = requestToClientRequest burl req+performRequest :: Maybe [Status] -> Request -> ClientM Response+performRequest acceptStatus req = do+  ClientEnv m burl cookieJar' createClientRequest _ <- ask+  clientRequest <- liftIO $ createClientRequest burl req   request <- case cookieJar' of     Nothing -> pure clientRequest     Just cj -> liftIO $ do@@ -162,7 +174,7 @@         oldCookieJar <- readTVar cj         let (newRequest, newCookieJar) =               Client.insertCookiesIntoRequest-                (requestToClientRequest burl req)+                clientRequest                 oldCookieJar                 now         writeTVar cj newCookieJar@@ -170,10 +182,12 @@    response <- maybe (requestWithoutCookieJar m request) (requestWithCookieJar m request) cookieJar'   let status = Client.responseStatus response-      status_code = statusCode status       ourResponse = clientResponseToResponse id response-  unless (status_code >= 200 && status_code < 300) $-      throwError $ mkFailureResponse burl req ourResponse+      goodStatus = case acceptStatus of+        Nothing -> statusIsSuccessful status+        Just good -> status `elem` good+  unless goodStatus $ do+    throwError $ mkFailureResponse burl req ourResponse   return ourResponse   where     requestWithoutCookieJar :: Client.Manager -> Client.Request -> ClientM (Client.Response BSL.ByteString)@@ -215,15 +229,25 @@     , responseHttpVersion = Client.responseVersion r     } -requestToClientRequest :: BaseUrl -> Request -> Client.Request-requestToClientRequest burl r = Client.defaultRequest+-- | Create a @http-client@ 'Client.Request' from a @servant@ 'Request'+--    The 'Client.host', 'Client.path' and 'Client.port' fields are extracted from the 'BaseUrl'+--    otherwise the body, headers and query string are derived from the @servant@ 'Request'+--+-- Note that @Applicative@ dependency is not really needed for this function+-- implementation. But in the past the return type was wrapped into @IO@+-- without a necessity breaking the API backward-compatibility. In order to not+-- break the API again it was changed to @Applicative@ so that you can just use+-- something like @Data.Functor.Identity@ without a need to involve @IO@ but+-- still keeping it compatible with the code written when it was typed as @IO@.+defaultMakeClientRequest :: Applicative f => BaseUrl -> Request -> f Client.Request+defaultMakeClientRequest burl r = pure Client.defaultRequest     { Client.method = requestMethod r     , Client.host = fromString $ baseUrlHost burl     , Client.port = baseUrlPort burl     , Client.path = BSL.toStrict                   $ fromString (baseUrlPath burl)                  <> toLazyByteString (requestPath r)-    , Client.queryString = renderQuery True . toList $ requestQueryString r+    , Client.queryString = buildQueryString . toList $ requestQueryString r     , Client.requestHeaders =       maybeToList acceptHdr ++ maybeToList contentTypeHdr ++ headers     , Client.requestBody = body@@ -232,7 +256,7 @@   where     -- Content-Type and Accept are specified by requestBody and requestAccept     headers = filter (\(h, _) -> h /= "Accept" && h /= "Content-Type") $-        toList $requestHeaders r+        toList $ requestHeaders r      acceptHdr         | null hs   = Nothing@@ -273,6 +297,14 @@     isSecure = case baseUrlScheme burl of         Http -> False         Https -> True++    -- Query string builder which does not do any encoding+    buildQueryString [] = mempty+    buildQueryString qps = "?" <> List.foldl' addQueryParam mempty qps++    addQueryParam qs (k, v) =+          qs <> (if BS.null qs then mempty else "&") <> urlEncode True k <> foldMap ("=" <>) v+  catchConnectionError :: IO a -> IO (Either ClientError a) catchConnectionError action =
src/Servant/Client/Internal/HttpClient/Streaming.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE CPP                        #-}-{-# LANGUAGE DeriveGeneric              #-} {-# LANGUAGE FlexibleContexts           #-} {-# LANGUAGE FlexibleInstances          #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -12,7 +11,7 @@     ClientEnv (..),     mkClientEnv,     clientResponseToResponse,-    requestToClientRequest,+    defaultMakeClientRequest,     catchConnectionError,     ) where @@ -24,14 +23,16 @@                  (NFData, force) import           Control.Exception                  (evaluate, throwIO)-import           Control.Monad ()+import           Control.Monad+                 (unless) import           Control.Monad.Base                  (MonadBase (..)) import           Control.Monad.Codensity                  (Codensity (..)) import           Control.Monad.Error.Class                  (MonadError (..))-import           Control.Monad.Reader+import           Control.Monad.IO.Class (MonadIO(..))+import           Control.Monad.Reader (MonadReader(..), ReaderT(..)) import           Control.Monad.STM                  (atomically) import           Control.Monad.Trans.Except@@ -47,7 +48,7 @@                  (getCurrentTime) import           GHC.Generics import           Network.HTTP.Types-                 (statusCode)+                 (Status, statusIsSuccessful)  import qualified Network.HTTP.Client                as Client @@ -55,8 +56,9 @@ import           Servant.Client.Internal.HttpClient                  (ClientEnv (..), catchConnectionError,                  clientResponseToResponse, mkClientEnv, mkFailureResponse,-                 requestToClientRequest)+                 defaultMakeClientRequest) import qualified Servant.Types.SourceT              as S+import Control.Monad.Trans.Class (MonadTrans(..))   -- | Generates a set of client functions for an API.@@ -101,7 +103,7 @@ -- 'Client.Manager' and 'BaseUrl' used for requests in the reader environment. newtype ClientM a = ClientM   { unClientM :: ReaderT ClientEnv (ExceptT ClientError (Codensity IO)) a }-  deriving ( Functor, Applicative, Monad, MonadIO, Generic+  deriving newtype ( Functor, Applicative, Monad, MonadIO, Generic            , MonadReader ClientEnv, MonadError ClientError)  instance MonadBase IO ClientM where@@ -112,7 +114,7 @@   a <!> b = a `catchError` \_ -> b  instance RunClient ClientM where-  runRequest = performRequest+  runRequestAcceptStatus = performRequest   throwClientError = throwError  instance RunStreamingClient ClientM where@@ -130,17 +132,17 @@ -- streaming response types ('SourceT', 'Conduit', pipes 'Proxy' or 'Machine'). -- For those you have to use 'withClientM'. ----- /Note:/ we 'force' the result, so the likehood of accidentally leaking a+-- /Note:/ we 'force' the result, so the likelihood of accidentally leaking a -- connection is smaller. Use with care. -- runClientM :: NFData a => ClientM a -> ClientEnv -> IO (Either ClientError a) runClientM cm env = withClientM cm env (evaluate . force) -performRequest :: Request -> ClientM Response-performRequest req = do+performRequest :: Maybe [Status] -> Request -> ClientM Response+performRequest acceptStatus req = do     -- TODO: should use Client.withResponse here too-  ClientEnv m burl cookieJar' <- ask-  let clientRequest = requestToClientRequest burl req+  ClientEnv m burl cookieJar' createClientRequest _ <- ask+  clientRequest <- liftIO $ createClientRequest burl req   request <- case cookieJar' of     Nothing -> pure clientRequest     Just cj -> liftIO $ do@@ -149,7 +151,7 @@         oldCookieJar <- readTVar cj         let (newRequest, newCookieJar) =               Client.insertCookiesIntoRequest-                (requestToClientRequest burl req)+                clientRequest                 oldCookieJar                 now         writeTVar cj newCookieJar@@ -163,24 +165,38 @@         now' <- getCurrentTime         atomically $ modifyTVar' cj (fst . Client.updateCookieJar response request now')       let status = Client.responseStatus response-          status_code = statusCode status           ourResponse = clientResponseToResponse id response-      unless (status_code >= 200 && status_code < 300) $+          goodStatus = case acceptStatus of+            Nothing -> statusIsSuccessful status+            Just good -> status `elem` good+      unless goodStatus $ do         throwError $ mkFailureResponse burl req ourResponse       return ourResponse +-- | TODO: support UVerb ('acceptStatus' argument, like in 'performRequest' above). performWithStreamingRequest :: Request -> (StreamingResponse -> IO a) -> ClientM a performWithStreamingRequest req k = do-  m <- asks manager-  burl <- asks baseUrl-  let request = requestToClientRequest burl req+  ClientEnv m burl cookieJar' createClientRequest _ <- ask+  clientRequest <- liftIO $ createClientRequest burl req+  request <- case cookieJar' of+    Nothing -> pure clientRequest+    Just cj -> liftIO $ do+      now <- getCurrentTime+      atomically $ do+        oldCookieJar <- readTVar cj+        let (newRequest, newCookieJar) =+              Client.insertCookiesIntoRequest+                clientRequest+                oldCookieJar+                now+        writeTVar cj newCookieJar+        pure newRequest   ClientM $ lift $ lift $ Codensity $ \k1 ->       Client.withResponse request m $ \res -> do           let status = Client.responseStatus res-              status_code = statusCode status            -- we throw FailureResponse in IO :(-          unless (status_code >= 200 && status_code < 300) $ do+          unless (statusIsSuccessful status) $ do               b <- BSL.fromChunks <$> Client.brConsume (Client.responseBody res)               throwIO $ mkFailureResponse burl req (clientResponseToResponse (const b) res) 
src/Servant/Client/Streaming.hs view
@@ -10,6 +10,7 @@     , runClientM     , ClientEnv(..)     , mkClientEnv+    , defaultMakeClientRequest     , hoistClient     , module Servant.Client.Core.Reexport     ) where
+ test/Servant/BasicAuthSpec.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.BasicAuthSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Control.Arrow+                 (left)+import           Data.Monoid ()+import qualified Network.HTTP.Types                   as HTTP+import           Test.Hspec++import           Servant.API+                 (BasicAuthData (..))+import           Servant.Client+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.BasicAuthSpec" $ do+    basicAuthSpec++basicAuthSpec :: Spec+basicAuthSpec = beforeAll (startWaiApp basicAuthServer) $ afterAll endWaiApp $ do+  context "Authentication works when requests are properly authenticated" $ do++    it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do+      let getBasic = client basicAuthAPI+      let basicAuthData = BasicAuthData "servant" "server"+      left show <$> runClient (getBasic basicAuthData) baseUrl `shouldReturn` Right alice++  context "Authentication is rejected when requests are not authenticated properly" $ do++    it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do+      let getBasic = client basicAuthAPI+      let basicAuthData = BasicAuthData "not" "password"+      Left (FailureResponse _ r) <- runClient (getBasic basicAuthData) baseUrl+      responseStatusCode r `shouldBe` HTTP.Status 403 "Forbidden"
+ test/Servant/BrokenSpec.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE DataKinds                   #-}+{-# LANGUAGE TypeOperators               #-}+{-# OPTIONS_GHC -freduction-depth=100    #-}+{-# OPTIONS_GHC -fno-warn-orphans        #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.BrokenSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Data.Monoid ()+import           Data.Proxy+import qualified Network.HTTP.Types as HTTP+import           Test.Hspec++import           Servant.API+                 ((:<|>) ((:<|>)), (:>), JSON, Verb, Get, StdMethod (GET))+import           Servant.Client+import           Servant.ClientTestUtils+import           Servant.Server++-- * api for testing inconsistencies between client and server++type Get201 = Verb 'GET 201+type Get301 = Verb 'GET 301++type BrokenAPI =+  -- the server should respond with 200, but returns 201+       "get200" :> Get201 '[JSON] ()+  -- the server should respond with 307, but returns 301+  :<|> "get307" :> Get301 '[JSON] ()++brokenApi :: Proxy BrokenAPI+brokenApi = Proxy++brokenServer :: Application+brokenServer = serve brokenApi (pure () :<|> pure ())++type PublicAPI =+  -- the client expects 200+       "get200" :> Get '[JSON] ()+  -- the client expects 307+  :<|> "get307" :> Get307 '[JSON] ()++publicApi :: Proxy PublicAPI+publicApi = Proxy++get200Client :: ClientM ()+get307Client :: ClientM ()+get200Client :<|> get307Client = client publicApi+++spec :: Spec+spec = describe "Servant.BrokenSpec" $ do+    brokenSpec++brokenSpec :: Spec+brokenSpec = beforeAll (startWaiApp brokenServer) $ afterAll endWaiApp $ do+    context "client returns errors for inconsistencies between client and server api" $ do+      it "reports FailureResponse with wrong 2xx status code" $ \(_, baseUrl) -> do+        res <- runClient get200Client baseUrl+        case res of+          Left (FailureResponse _ r) | responseStatusCode r == HTTP.status201 -> return ()+          _ -> fail $ "expected 201 broken response, but got " <> show res++      it "reports FailureResponse with wrong 3xx status code" $ \(_, baseUrl) -> do+        res <- runClient get307Client baseUrl+        case res of+          Left (FailureResponse _ r) | responseStatusCode r == HTTP.status301 -> return ()+          _ -> fail $ "expected 301 broken response, but got " <> show res
− test/Servant/ClientSpec.hs
@@ -1,514 +0,0 @@-{-# LANGUAGE CPP                    #-}-{-# LANGUAGE ConstraintKinds        #-}-{-# LANGUAGE DataKinds              #-}-{-# LANGUAGE DeriveGeneric          #-}-{-# LANGUAGE FlexibleContexts       #-}-{-# LANGUAGE FlexibleInstances      #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE GADTs                  #-}-{-# LANGUAGE MultiParamTypeClasses  #-}-{-# LANGUAGE OverloadedStrings      #-}-{-# LANGUAGE PolyKinds              #-}-{-# LANGUAGE RecordWildCards        #-}-{-# LANGUAGE ScopedTypeVariables    #-}-{-# LANGUAGE StandaloneDeriving     #-}-{-# LANGUAGE TypeApplications       #-}-{-# LANGUAGE TypeFamilies           #-}-{-# LANGUAGE TypeOperators          #-}-{-# LANGUAGE UndecidableInstances   #-}-{-# OPTIONS_GHC -freduction-depth=100 #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}-{-# OPTIONS_GHC -fno-warn-name-shadowing #-}--module Servant.ClientSpec (spec, Person(..), startWaiApp, endWaiApp) where--import           Prelude ()-import           Prelude.Compat--import           Control.Arrow-                 (left)-import           Control.Concurrent-                 (ThreadId, forkIO, killThread)-import           Control.Concurrent.STM-                 (atomically)-import           Control.Concurrent.STM.TVar-                 (newTVar, readTVar)-import           Control.Exception-                 (bracket, fromException)-import           Control.Monad.Error.Class-                 (throwError)-import           Data.Aeson-import           Data.Char-                 (chr, isPrint)-import           Data.Foldable-                 (forM_, toList)-import           Data.Maybe-                 (isJust, listToMaybe)-import           Data.Monoid ()-import           Data.Proxy-import           Data.Semigroup-                 ((<>))-import           GHC.Generics-                 (Generic)-import qualified Network.HTTP.Client                  as C-import qualified Network.HTTP.Types                   as HTTP-import           Network.Socket-import qualified Network.Wai                          as Wai-import           Network.Wai.Handler.Warp-import           System.IO.Unsafe-                 (unsafePerformIO)-import           Test.Hspec-import           Test.Hspec.QuickCheck-import           Test.HUnit-import           Test.QuickCheck-import           Web.FormUrlEncoded-                 (FromForm, ToForm)--import           Servant.API-                 ((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth,-                 BasicAuthData (..), Capture, CaptureAll, Delete,-                 DeleteNoContent, EmptyAPI, FormUrlEncoded, Get, Header,-                 Headers, JSON, NoContent (NoContent), Post, Put, QueryFlag,-                 QueryParam, QueryParams, Raw, ReqBody, addHeader, getHeaders)-import           Servant.Client-import qualified Servant.Client.Core.Auth    as Auth-import qualified Servant.Client.Core.Request as Req-import           Servant.Server-import           Servant.Server.Experimental.Auth-import           Servant.Test.ComprehensiveAPI---- This declaration simply checks that all instances are in place.-_ = client comprehensiveAPIWithoutStreaming--spec :: Spec-spec = describe "Servant.Client" $ do-    sucessSpec-    failSpec-    wrappedApiSpec-    basicAuthSpec-    genAuthSpec-    hoistClientSpec-    connectionErrorSpec---- * test data types--data Person = Person-  { _name :: String-  , _age  :: Integer-  } deriving (Eq, Show, Generic)--instance ToJSON Person-instance FromJSON Person--instance ToForm Person-instance FromForm Person--instance Arbitrary Person where-  arbitrary = Person <$> arbitrary <*> arbitrary--alice :: Person-alice = Person "Alice" 42--carol :: Person-carol = Person "Carol" 17--type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]--type Api =-  Get '[JSON] Person-  :<|> "get" :> Get '[JSON] Person-  :<|> "deleteEmpty" :> DeleteNoContent '[JSON] NoContent-  :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person-  :<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person]-  :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person-  :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person-  :<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]-  :<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool-  :<|> "rawSuccess" :> Raw-  :<|> "rawFailure" :> Raw-  :<|> "multiple" :>-            Capture "first" String :>-            QueryParam "second" Int :>-            QueryFlag "third" :>-            ReqBody '[JSON] [(String, [Rational])] :>-            Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])-  :<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)-  :<|> "deleteContentType" :> DeleteNoContent '[JSON] NoContent-  :<|> "redirectWithCookie" :> Raw-  :<|> "empty" :> EmptyAPI--api :: Proxy Api-api = Proxy--getRoot         :: ClientM Person-getGet          :: ClientM Person-getDeleteEmpty  :: ClientM NoContent-getCapture      :: String -> ClientM Person-getCaptureAll   :: [String] -> ClientM [Person]-getBody         :: Person -> ClientM Person-getQueryParam   :: Maybe String -> ClientM Person-getQueryParams  :: [String] -> ClientM [Person]-getQueryFlag    :: Bool -> ClientM Bool-getRawSuccess   :: HTTP.Method -> ClientM Response-getRawFailure   :: HTTP.Method -> ClientM Response-getMultiple     :: String -> Maybe Int -> Bool -> [(String, [Rational])]-  -> ClientM (String, Maybe Int, Bool, [(String, [Rational])])-getRespHeaders  :: ClientM (Headers TestHeaders Bool)-getDeleteContentType :: ClientM NoContent-getRedirectWithCookie :: HTTP.Method -> ClientM Response--getRoot-  :<|> getGet-  :<|> getDeleteEmpty-  :<|> getCapture-  :<|> getCaptureAll-  :<|> getBody-  :<|> getQueryParam-  :<|> getQueryParams-  :<|> getQueryFlag-  :<|> getRawSuccess-  :<|> getRawFailure-  :<|> getMultiple-  :<|> getRespHeaders-  :<|> getDeleteContentType-  :<|> getRedirectWithCookie-  :<|> EmptyClient = client api--server :: Application-server = serve api (-       return carol-  :<|> return alice-  :<|> return NoContent-  :<|> (\ name -> return $ Person name 0)-  :<|> (\ names -> return (zipWith Person names [0..]))-  :<|> return-  :<|> (\ name -> case name of-                   Just "alice" -> return alice-                   Just n -> throwError $ ServerError 400 (n ++ " not found") "" []-                   Nothing -> throwError $ ServerError 400 "missing parameter" "" [])-  :<|> (\ names -> return (zipWith Person names [0..]))-  :<|> return-  :<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "rawSuccess")-  :<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure")-  :<|> (\ a b c d -> return (a, b, c, d))-  :<|> (return $ addHeader 1729 $ addHeader "eg2" True)-  :<|> return NoContent-  :<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.found302 [("Location", "testlocation"), ("Set-Cookie", "testcookie=test")] "")-  :<|> emptyServer)--type FailApi =-       "get" :> Raw-  :<|> "capture" :> Capture "name" String :> Raw-  :<|> "body" :> Raw-failApi :: Proxy FailApi-failApi = Proxy--failServer :: Application-failServer = serve failApi (-       (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "")-  :<|> (\ _capture -> Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "application/json")] "")-  :<|> (Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "fooooo")] "")- )---- * basic auth stuff--type BasicAuthAPI =-       BasicAuth "foo-realm" () :> "private" :> "basic" :> Get '[JSON] Person--basicAuthAPI :: Proxy BasicAuthAPI-basicAuthAPI = Proxy--basicAuthHandler :: BasicAuthCheck ()-basicAuthHandler =-  let check (BasicAuthData username password) =-        if username == "servant" && password == "server"-        then return (Authorized ())-        else return Unauthorized-  in BasicAuthCheck check--basicServerContext :: Context '[ BasicAuthCheck () ]-basicServerContext = basicAuthHandler :. EmptyContext--basicAuthServer :: Application-basicAuthServer = serveWithContext basicAuthAPI basicServerContext (const (return alice))---- * general auth stuff--type GenAuthAPI =-  AuthProtect "auth-tag" :> "private" :> "auth" :> Get '[JSON] Person--genAuthAPI :: Proxy GenAuthAPI-genAuthAPI = Proxy--type instance AuthServerData (AuthProtect "auth-tag") = ()-type instance Auth.AuthClientData (AuthProtect "auth-tag") = ()--genAuthHandler :: AuthHandler Wai.Request ()-genAuthHandler =-  let handler req = case lookup "AuthHeader" (Wai.requestHeaders req) of-        Nothing -> throwError (err401 { errBody = "Missing auth header" })-        Just _ -> return ()-  in mkAuthHandler handler--genAuthServerContext :: Context '[ AuthHandler Wai.Request () ]-genAuthServerContext = genAuthHandler :. EmptyContext--genAuthServer :: Application-genAuthServer = serveWithContext genAuthAPI genAuthServerContext (const (return alice))--{-# NOINLINE manager' #-}-manager' :: C.Manager-manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings--runClient :: ClientM a -> BaseUrl -> IO (Either ClientError a)-runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')--sucessSpec :: Spec-sucessSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do-    it "Servant.API.Get root" $ \(_, baseUrl) -> do-      left show <$> runClient getRoot baseUrl  `shouldReturn` Right carol--    it "Servant.API.Get" $ \(_, baseUrl) -> do-      left show <$> runClient getGet baseUrl  `shouldReturn` Right alice--    describe "Servant.API.Delete" $ do-      it "allows empty content type" $ \(_, baseUrl) -> do-        left show <$> runClient getDeleteEmpty baseUrl `shouldReturn` Right NoContent--      it "allows content type" $ \(_, baseUrl) -> do-        left show <$> runClient getDeleteContentType baseUrl `shouldReturn` Right NoContent--    it "Servant.API.Capture" $ \(_, baseUrl) -> do-      left show <$> runClient (getCapture "Paula") baseUrl `shouldReturn` Right (Person "Paula" 0)--    it "Servant.API.CaptureAll" $ \(_, baseUrl) -> do-      let expected = [(Person "Paula" 0), (Person "Peta" 1)]-      left show <$> runClient (getCaptureAll ["Paula", "Peta"]) baseUrl `shouldReturn` Right expected--    it "Servant.API.ReqBody" $ \(_, baseUrl) -> do-      let p = Person "Clara" 42-      left show <$> runClient (getBody p) baseUrl `shouldReturn` Right p--    it "Servant.API FailureResponse" $ \(_, baseUrl) -> do-      left show <$> runClient (getQueryParam (Just "alice")) baseUrl `shouldReturn` Right alice-      Left (FailureResponse req _) <- runClient (getQueryParam (Just "bob")) baseUrl-      Req.requestPath req `shouldBe` (baseUrl, "/param")-      toList (Req.requestQueryString req) `shouldBe` [("name", Just "bob")]-      Req.requestMethod req `shouldBe` HTTP.methodGet--    it "Servant.API.QueryParam" $ \(_, baseUrl) -> do-      left show <$> runClient (getQueryParam (Just "alice")) baseUrl `shouldReturn` Right alice-      Left (FailureResponse _ r) <- runClient (getQueryParam (Just "bob")) baseUrl-      responseStatusCode r `shouldBe` HTTP.Status 400 "bob not found"--    it "Servant.API.QueryParam.QueryParams" $ \(_, baseUrl) -> do-      left show <$> runClient (getQueryParams []) baseUrl `shouldReturn` Right []-      left show <$> runClient (getQueryParams ["alice", "bob"]) baseUrl-        `shouldReturn` Right [Person "alice" 0, Person "bob" 1]--    context "Servant.API.QueryParam.QueryFlag" $-      forM_ [False, True] $ \ flag -> it (show flag) $ \(_, baseUrl) -> do-        left show <$> runClient (getQueryFlag flag) baseUrl `shouldReturn` Right flag--    it "Servant.API.Raw on success" $ \(_, baseUrl) -> do-      res <- runClient (getRawSuccess HTTP.methodGet) baseUrl-      case res of-        Left e -> assertFailure $ show e-        Right r -> do-          responseStatusCode r `shouldBe` HTTP.status200-          responseBody r `shouldBe` "rawSuccess"--    it "Servant.API.Raw should return a Left in case of failure" $ \(_, baseUrl) -> do-      res <- runClient (getRawFailure HTTP.methodGet) baseUrl-      case res of-        Right _ -> assertFailure "expected Left, but got Right"-        Left (FailureResponse _ r) -> do-          responseStatusCode r `shouldBe` HTTP.status400-          responseBody r `shouldBe` "rawFailure"-        Left e -> assertFailure $ "expected FailureResponse, but got " ++ show e--    it "Returns headers appropriately" $ \(_, baseUrl) -> do-      res <- runClient getRespHeaders baseUrl-      case res of-        Left e -> assertFailure $ show e-        Right val -> getHeaders val `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]--    it "Stores Cookie in CookieJar after a redirect" $ \(_, baseUrl) -> do-      mgr <- C.newManager C.defaultManagerSettings-      cj <- atomically . newTVar $ C.createCookieJar []-      _ <- runClientM (getRedirectWithCookie HTTP.methodGet) (ClientEnv mgr baseUrl (Just cj))-      cookie <- listToMaybe . C.destroyCookieJar <$> atomically (readTVar cj)-      C.cookie_name <$> cookie `shouldBe` Just "testcookie"-      C.cookie_value <$> cookie `shouldBe` Just "test"--    modifyMaxSuccess (const 20) $ do-      it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $ \(_, baseUrl) ->-        property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->-          ioProperty $ do-            result <- left show <$> runClient (getMultiple cap num flag body) baseUrl-            return $-              result === Right (cap, num, flag, body)---wrappedApiSpec :: Spec-wrappedApiSpec = describe "error status codes" $ do-  let serveW api = serve api $ throwError $ ServerError 500 "error message" "" []-  context "are correctly handled by the client" $-    let test :: (WrappedApi, String) -> Spec-        test (WrappedApi api, desc) =-          it desc $ bracket (startWaiApp $ serveW api) endWaiApp $ \(_, baseUrl) -> do-            let getResponse :: ClientM ()-                getResponse = client api-            Left (FailureResponse _ r) <- runClient getResponse baseUrl-            responseStatusCode r `shouldBe` (HTTP.Status 500 "error message")-    in mapM_ test $-        (WrappedApi (Proxy :: Proxy (Delete '[JSON] ())), "Delete") :-        (WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Get") :-        (WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Post") :-        (WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Put") :-        []--failSpec :: Spec-failSpec = beforeAll (startWaiApp failServer) $ afterAll endWaiApp $ do--    context "client returns errors appropriately" $ do-      it "reports FailureResponse" $ \(_, baseUrl) -> do-        let (_ :<|> _ :<|> getDeleteEmpty :<|> _) = client api-        Left res <- runClient getDeleteEmpty baseUrl-        case res of-          FailureResponse _ r | responseStatusCode r == HTTP.status404 -> return ()-          _ -> fail $ "expected 404 response, but got " <> show res--      it "reports DecodeFailure" $ \(_, baseUrl) -> do-        let (_ :<|> _ :<|> _ :<|> getCapture :<|> _) = client api-        Left res <- runClient (getCapture "foo") baseUrl-        case res of-          DecodeFailure _ _ -> return ()-          _ -> fail $ "expected DecodeFailure, but got " <> show res--      it "reports ConnectionError" $ \_ -> do-        let (getGetWrongHost :<|> _) = client api-        Left res <- runClient getGetWrongHost (BaseUrl Http "127.0.0.1" 19872 "")-        case res of-          ConnectionError _ -> return ()-          _ -> fail $ "expected ConnectionError, but got " <> show res--      it "reports UnsupportedContentType" $ \(_, baseUrl) -> do-        let (_ :<|> getGet :<|> _ ) = client api-        Left res <- runClient getGet baseUrl-        case res of-          UnsupportedContentType ("application/octet-stream") _ -> return ()-          _ -> fail $ "expected UnsupportedContentType, but got " <> show res--      it "reports InvalidContentTypeHeader" $ \(_, baseUrl) -> do-        let (_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> getBody :<|> _) = client api-        Left res <- runClient (getBody alice) baseUrl-        case res of-          InvalidContentTypeHeader _ -> return ()-          _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res--data WrappedApi where-  WrappedApi :: (HasServer (api :: *) '[], Server api ~ Handler a,-                 HasClient ClientM api, Client ClientM api ~ ClientM ()) =>-    Proxy api -> WrappedApi--basicAuthSpec :: Spec-basicAuthSpec = beforeAll (startWaiApp basicAuthServer) $ afterAll endWaiApp $ do-  context "Authentication works when requests are properly authenticated" $ do--    it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do-      let getBasic = client basicAuthAPI-      let basicAuthData = BasicAuthData "servant" "server"-      left show <$> runClient (getBasic basicAuthData) baseUrl `shouldReturn` Right alice--  context "Authentication is rejected when requests are not authenticated properly" $ do--    it "Authenticates a BasicAuth protected server appropriately" $ \(_,baseUrl) -> do-      let getBasic = client basicAuthAPI-      let basicAuthData = BasicAuthData "not" "password"-      Left (FailureResponse _ r) <- runClient (getBasic basicAuthData) baseUrl-      responseStatusCode r `shouldBe` HTTP.Status 403 "Forbidden"--genAuthSpec :: Spec-genAuthSpec = beforeAll (startWaiApp genAuthServer) $ afterAll endWaiApp $ do-  context "Authentication works when requests are properly authenticated" $ do--    it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do-      let getProtected = client genAuthAPI-      let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "AuthHeader" ("cool" :: String) req)-      left show <$> runClient (getProtected authRequest) baseUrl `shouldReturn` Right alice--  context "Authentication is rejected when requests are not authenticated properly" $ do--    it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do-      let getProtected = client genAuthAPI-      let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "Wrong" ("header" :: String) req)-      Left (FailureResponse _ r) <- runClient (getProtected authRequest) baseUrl-      responseStatusCode r `shouldBe` (HTTP.Status 401 "Unauthorized")---- * hoistClient--type HoistClientAPI = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int--hoistClientAPI :: Proxy HoistClientAPI-hoistClientAPI = Proxy--hoistClientServer :: Application -- implements HoistClientAPI-hoistClientServer = serve hoistClientAPI $ return 5 :<|> (\n -> return n)--hoistClientSpec :: Spec-hoistClientSpec = beforeAll (startWaiApp hoistClientServer) $ afterAll endWaiApp $ do-  describe "Servant.Client.hoistClient" $ do-    it "allows us to GET/POST/... requests in IO instead of ClientM" $ \(_, baseUrl) -> do-      let (getInt :<|> postInt)-            = hoistClient hoistClientAPI-                          (fmap (either (error . show) id) . flip runClient baseUrl)-                          (client hoistClientAPI)--      getInt `shouldReturn` 5-      postInt 5 `shouldReturn` 5---- * ConnectionError-type ConnectionErrorAPI = Get '[JSON] Int--connectionErrorAPI :: Proxy ConnectionErrorAPI-connectionErrorAPI = Proxy--connectionErrorSpec :: Spec-connectionErrorSpec = describe "Servant.Client.ClientError" $-    it "correctly catches ConnectionErrors when the HTTP request can't go through" $ do-        let getInt = client connectionErrorAPI-        let baseUrl' = BaseUrl Http "example.invalid" 80 ""-        let isHttpError (Left (ConnectionError e)) = isJust $ fromException @C.HttpException e-            isHttpError _ = False-        (isHttpError <$> runClient getInt baseUrl') `shouldReturn` True---- * utils--startWaiApp :: Application -> IO (ThreadId, BaseUrl)-startWaiApp app = do-    (port, socket) <- openTestSocket-    let settings = setPort port $ defaultSettings-    thread <- forkIO $ runSettingsSocket settings socket app-    return (thread, BaseUrl Http "localhost" port "")---endWaiApp :: (ThreadId, BaseUrl) -> IO ()-endWaiApp (thread, _) = killThread thread--openTestSocket :: IO (Port, Socket)-openTestSocket = do-  s <- socket AF_INET Stream defaultProtocol-  let localhost = tupleToHostAddress (127, 0, 0, 1)-  bind s (SockAddrInet defaultPort localhost)-  listen s 1-  port <- socketPort s-  return (fromIntegral port, s)--pathGen :: Gen (NonEmptyList Char)-pathGen = fmap NonEmpty path- where-  path = listOf1 $ elements $-    filter (not . (`elem` ("?%[]/#;" :: String))) $-    filter isPrint $-    map chr [0..127]
+ test/Servant/ClientTestUtils.hs view
@@ -0,0 +1,483 @@+{-# LANGUAGE ConstraintKinds       #-}+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE DeriveGeneric         #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE GADTs                 #-}+{-# LANGUAGE LambdaCase            #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE PolyKinds             #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE TypeApplications      #-}+{-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE TypeOperators         #-}+{-# LANGUAGE UndecidableInstances  #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.ClientTestUtils where++import           Prelude ()+import           Prelude.Compat++import           Control.Concurrent               (ThreadId, forkIO, killThread)+import           Control.Monad                    (join)+import           Control.Monad.Error.Class        (throwError)+import           Data.Aeson+import           Data.ByteString                  (ByteString)+import           Data.ByteString.Builder          (byteString)+import qualified Data.ByteString.Char8            as C8+import qualified Data.ByteString.Lazy             as LazyByteString+import           Data.Char                        (chr, isPrint)+import           Data.Maybe                       (fromMaybe)+import           Data.Monoid ()+import           Data.Proxy+import           Data.SOP+import           Data.Text                        (Text)+import qualified Data.Text                        as Text+import           Data.Text.Encoding               (decodeUtf8, encodeUtf8)+import qualified Generics.SOP                     as GSOP+import           GHC.Generics                     (Generic)+import qualified Network.HTTP.Client              as C+import qualified Network.HTTP.Types               as HTTP+import           Network.Socket+import qualified Network.Wai                      as Wai+import           Network.Wai.Handler.Warp+import           System.IO.Unsafe                 (unsafePerformIO)+import           Test.QuickCheck+import           Text.Read                        (readMaybe)+import           Web.FormUrlEncoded               (FromForm, ToForm)++import           Servant.API+                 (AuthProtect, BasicAuth, BasicAuthData (..), Capture,+                 CaptureAll, DeepQuery, DeleteNoContent, EmptyAPI,+                 FormUrlEncoded, Fragment, FromHttpApiData (..), Get, Header,+                 Headers, Host, JSON, MimeRender (mimeRender),+                 MimeUnrender (mimeUnrender), NamedRoutes,+                 NoContent (NoContent), PlainText, Post, QueryFlag, QueryParam,+                 QueryParams, QueryString, Raw, ReqBody, StdMethod (GET),+                 ToHttpApiData (..), UVerb, Union, Verb,+                 WithStatus (WithStatus), addHeader, (:<|>) ((:<|>)), (:>))+import           Servant.API.Generic              ((:-))+import           Servant.API.MultiVerb+import           Servant.API.QueryString+                 (FromDeepQuery (..), ToDeepQuery (..))+import           Servant.API.Range+import           Servant.Client+import qualified Servant.Client.Core.Auth         as Auth+import           Servant.Server+import           Servant.Server.Experimental.Auth+import           Servant.Test.ComprehensiveAPI++-- This declaration simply checks that all instances are in place.+_ = client comprehensiveAPIWithoutStreaming++-- * test data types++data Person = Person+  { _name :: String+  , _age  :: Integer+  } deriving (Eq, Show, Read, Generic)++instance ToJSON Person+instance FromJSON Person++instance ToForm Person+instance FromForm Person++instance Arbitrary Person where+  arbitrary = Person <$> arbitrary <*> arbitrary++instance MimeRender PlainText Person where+  mimeRender _ = LazyByteString.fromStrict . encodeUtf8 . Text.pack . show++instance MimeUnrender PlainText Person where+  mimeUnrender _ =+    -- This does not handle any errors, but it should be fine for tests+    Right . read . Text.unpack . decodeUtf8 . LazyByteString.toStrict+++alice :: Person+alice = Person "Alice" 42++carol :: Person+carol = Person "Carol" 17++type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]+type TestSetCookieHeaders = '[Header "Set-Cookie" String, Header "Set-Cookie" String]++data RecordRoutes mode = RecordRoutes+  { version :: mode :- "version" :> Get '[JSON] Int+  , echo :: mode :- "echo" :> Capture "string" String :> Get '[JSON] String+  , otherRoutes :: mode :- "other" :> Capture "someParam" Int :> NamedRoutes OtherRoutes+  } deriving Generic++newtype OtherRoutes mode = OtherRoutes+  { something :: mode :- "something" :> Get '[JSON] [String]+  } deriving Generic++-- Get for HTTP 307 Temporary Redirect+type Get307 = Verb 'GET 307++data Filter = Filter+  { ageFilter :: Integer+  , nameFilter :: String+  }+  deriving Show++instance FromDeepQuery Filter where+  fromDeepQuery params = do+    let maybeToRight l = maybe (Left l) Right+    age' <- maybeToRight "missing age" $ readMaybe . Text.unpack =<< join (lookup ["age"] params)+    name' <- maybeToRight "missing name" $ join $ lookup ["name"] params+    return $ Filter age' (Text.unpack name')++instance ToDeepQuery Filter where+  toDeepQuery (Filter age' name') =+    [ (["age"], Just (Text.pack $ show age'))+    , (["name"], Just (Text.pack name'))+    ]++-----------------------------+-- MultiVerb test endpoint --+-----------------------------++-- This is the list of all possible responses+type MultipleChoicesIntResponses =+  '[ RespondEmpty 400 "Negative"+   , Respond 200 "Even number" Bool+   , Respond 200 "Odd number" Int+   ]++data MultipleChoicesIntResult+  = NegativeNumber+  | Even Bool+  | Odd Int+  deriving stock (Generic)+  deriving (AsUnion MultipleChoicesIntResponses)+    via GenericAsUnion MultipleChoicesIntResponses MultipleChoicesIntResult++instance GSOP.Generic MultipleChoicesIntResult++-- This is our endpoint description+type MultipleChoicesInt =+  Capture "int" Int+  :> MultiVerb+    'GET+    '[JSON]+    MultipleChoicesIntResponses+    MultipleChoicesIntResult++type Api =+  Get '[JSON] Person+  :<|> "get" :> Get '[JSON] Person+  -- This endpoint returns a response with status code 307 Temporary Redirect,+  -- different from the ones in the 2xx successful class, to test derivation+  -- of clients' api.+  :<|> "get307" :> Get307 '[PlainText] Text+  :<|> "deleteEmpty" :> DeleteNoContent+  :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person+  :<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person]+  :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person+  :<|> "param" :> QueryParam "name" String :> Get '[FormUrlEncoded,JSON] Person+  -- This endpoint makes use of a 'Raw' server because it is not currently+  -- possible to handle arbitrary binary query param values with+  -- @servant-server@+  :<|> "param-binary" :> QueryParam "payload" UrlEncodedByteString :> Raw+  :<|> "params" :> QueryParams "names" String :> Get '[JSON] [Person]+  :<|> "flag" :> QueryFlag "flag" :> Get '[JSON] Bool+  :<|> "query-string" :> QueryString :> Get '[JSON] Person+  :<|> "deep-query" :> DeepQuery "filter" Filter :> Get '[JSON] Person+  :<|> "fragment" :> Fragment String :> Get '[JSON] Person+  :<|> "rawSuccess" :> Raw+  :<|> "rawSuccessPassHeaders" :> Raw+  :<|> "rawFailure" :> Raw+  :<|> "multiple" :>+            Capture "first" String :>+            QueryParam "second" Int :>+            QueryFlag "third" :>+            ReqBody '[JSON] [(String, [Rational])] :>+            Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])+  :<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)+  :<|> "uverb-headers" :> UVerb 'GET '[JSON] '[ WithStatus 200 (Headers TestHeaders Bool), WithStatus 204 String ]+  :<|> "set-cookie-headers" :> Get '[JSON] (Headers TestSetCookieHeaders Bool)+  :<|> "deleteContentType" :> DeleteNoContent+  :<|> "redirectWithCookie" :> Raw+  :<|> "empty" :> EmptyAPI+  :<|> "uverb-success-or-redirect" :>+            Capture "bool" Bool :>+            UVerb 'GET '[PlainText] '[WithStatus 200 Person,+                                      WithStatus 301 Text]+  :<|> "uverb-get-created" :> UVerb 'GET '[PlainText] '[WithStatus 201 Person]+  :<|> NamedRoutes RecordRoutes+  :<|> "multiple-choices-int" :> MultipleChoicesInt+  :<|> "captureVerbatim" :> Capture "someString" Verbatim :> Get '[PlainText] Text+  :<|> "host-test" :> Host "servant.example" :> Get '[JSON] Bool+  :<|> PaginatedAPI++api :: Proxy Api+api = Proxy++getRoot         :: ClientM Person+getGet          :: ClientM Person+getGet307       :: ClientM Text+getDeleteEmpty  :: ClientM NoContent+getCapture      :: String -> ClientM Person+getCaptureAll   :: [String] -> ClientM [Person]+getBody         :: Person -> ClientM Person+getQueryParam   :: Maybe String -> ClientM Person+getQueryParamBinary :: Maybe UrlEncodedByteString -> HTTP.Method -> ClientM Response+getQueryParams  :: [String] -> ClientM [Person]+getQueryFlag    :: Bool -> ClientM Bool+getQueryString  :: [(ByteString, Maybe ByteString)] -> ClientM Person+getDeepQuery    :: Filter -> ClientM Person+getFragment     :: ClientM Person+getRawSuccess   :: HTTP.Method -> ClientM Response+getRawSuccessPassHeaders :: HTTP.Method -> ClientM Response+getRawFailure   :: HTTP.Method -> ClientM Response+getMultiple     :: String -> Maybe Int -> Bool -> [(String, [Rational])]+  -> ClientM (String, Maybe Int, Bool, [(String, [Rational])])+getRespHeaders  :: ClientM (Headers TestHeaders Bool)+getUVerbRespHeaders  :: ClientM (Union '[ WithStatus 200 (Headers TestHeaders Bool), WithStatus 204 String ])+getSetCookieHeaders  :: ClientM (Headers TestSetCookieHeaders Bool)+getDeleteContentType :: ClientM NoContent+getRedirectWithCookie :: HTTP.Method -> ClientM Response+uverbGetSuccessOrRedirect :: Bool+                          -> ClientM (Union '[WithStatus 200 Person,+                                              WithStatus 301 Text])+uverbGetCreated :: ClientM (Union '[WithStatus 201 Person])+recordRoutes :: RecordRoutes (AsClientT ClientM)+multiChoicesInt :: Int -> ClientM MultipleChoicesIntResult+captureVerbatim :: Verbatim -> ClientM Text+getHost :: ClientM Bool+getPaginatedPerson :: Maybe (Range 1 100) -> ClientM [Person]++getRoot+  :<|> getGet+  :<|> getGet307+  :<|> getDeleteEmpty+  :<|> getCapture+  :<|> getCaptureAll+  :<|> getBody+  :<|> getQueryParam+  :<|> getQueryParamBinary+  :<|> getQueryParams+  :<|> getQueryFlag+  :<|> getQueryString+  :<|> getDeepQuery+  :<|> getFragment+  :<|> getRawSuccess+  :<|> getRawSuccessPassHeaders+  :<|> getRawFailure+  :<|> getMultiple+  :<|> getRespHeaders+  :<|> getUVerbRespHeaders+  :<|> getSetCookieHeaders+  :<|> getDeleteContentType+  :<|> getRedirectWithCookie+  :<|> EmptyClient+  :<|> uverbGetSuccessOrRedirect+  :<|> uverbGetCreated+  :<|> recordRoutes+  :<|> multiChoicesInt+  :<|> captureVerbatim+  :<|> getHost+  :<|> getPaginatedPerson = client api++server :: Application+server = serve api (+       return carol+  :<|> return alice+  :<|> return "redirecting"+  :<|> return NoContent+  :<|> (\ name -> return $ Person name 0)+  :<|> (\ names -> return (zipWith Person names [0..]))+  :<|> return+  :<|> (\case+          Just "alice" -> return alice+          Just n -> throwError $ ServerError 400 (n ++ " not found") "" []+          Nothing -> throwError $ ServerError 400 "missing parameter" "" [])+  :<|> const (Tagged $ \request respond ->+          respond . maybe (Wai.responseLBS HTTP.notFound404 [] "Missing: payload")+                          (Wai.responseLBS HTTP.ok200 [] . LazyByteString.fromStrict)+                  . join+                  . lookup "payload"+                  $ Wai.queryString request+       )+  :<|> (\ names -> return (zipWith Person names [0..]))+  :<|> return+  :<|> (\ q -> return alice { _name = maybe mempty C8.unpack $ join (lookup "name" q)+                            , _age = fromMaybe 0 (readMaybe . C8.unpack =<< join (lookup "age" q))+                            }+       )+  :<|> (\ filter'  -> return alice { _name = nameFilter filter'+                                   , _age = ageFilter filter'+                                   }+       )+  :<|> return alice+  :<|> Tagged (\ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "rawSuccess")+  :<|> Tagged (\ request respond -> respond $ Wai.responseLBS HTTP.ok200 (Wai.requestHeaders request) "rawSuccess")+  :<|> Tagged (\ _request respond -> respond $ Wai.responseLBS HTTP.badRequest400 [] "rawFailure")+  :<|> (\ a b c d -> return (a, b, c, d))+  :<|> return (addHeader 1729 $ addHeader "eg2" True)+  :<|> (pure . Z . I . WithStatus $ addHeader 1729 $ addHeader "eg2" True)+  :<|> return (addHeader "cookie1" $ addHeader "cookie2" True)+  :<|> return NoContent+  :<|> Tagged (\ _request respond -> respond $ Wai.responseLBS HTTP.found302 [("Location", "testlocation"), ("Set-Cookie", "testcookie=test")] "")+  :<|> emptyServer+  :<|> (\shouldRedirect -> if shouldRedirect+                              then respond (WithStatus @301 ("redirecting" :: Text))+                              else respond (WithStatus @200 alice ))+  :<|> respond (WithStatus @201 carol)+  :<|> RecordRoutes+         { version = pure 42+         , echo = pure+         , otherRoutes = const OtherRoutes+             { something = pure ["foo", "bar", "pweet"]+             }+         }+  :<|> (\param ->+          if param < 0+          then pure NegativeNumber+          else+            if even param+            then pure $ Odd 3+            else pure $ Even True+            )++  :<|> pure . decodeUtf8 . unVerbatim+  :<|> pure True+  :<|> usersServer+  )++-- * api for testing failures++type FailApi =+       "get" :> Raw+  :<|> "capture" :> Capture "name" String :> Raw+  :<|> "body" :> Raw+  :<|> "headers" :> Raw+failApi :: Proxy FailApi+failApi = Proxy++failServer :: Application+failServer = serve failApi (+       Tagged (\ _request respond -> respond $ Wai.responseLBS HTTP.ok200 [] "")+  :<|> (\ _capture -> Tagged $ \_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "application/json")] "")+  :<|> Tagged (\_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "fooooo")] "")+  :<|> Tagged (\_request respond -> respond $ Wai.responseLBS HTTP.ok200 [("content-type", "application/x-www-form-urlencoded"), ("X-Example1", "1"), ("X-Example2", "foo")] "")+  )++-- * basic auth stuff++type BasicAuthAPI =+       BasicAuth "foo-realm" () :> "private" :> "basic" :> Get '[JSON] Person++basicAuthAPI :: Proxy BasicAuthAPI+basicAuthAPI = Proxy++basicAuthHandler :: BasicAuthCheck ()+basicAuthHandler =+  let check (BasicAuthData username password) =+        if username == "servant" && password == "server"+        then return (Authorized ())+        else return Unauthorized+  in BasicAuthCheck check++basicServerContext :: Context '[ BasicAuthCheck () ]+basicServerContext = basicAuthHandler :. EmptyContext++basicAuthServer :: Application+basicAuthServer = serveWithContext basicAuthAPI basicServerContext (const (return alice))++-- * general auth stuff++type GenAuthAPI =+  AuthProtect "auth-tag" :> "private" :> "auth" :> Get '[JSON] Person++genAuthAPI :: Proxy GenAuthAPI+genAuthAPI = Proxy++type instance AuthServerData (AuthProtect "auth-tag") = ()+type instance Auth.AuthClientData (AuthProtect "auth-tag") = ()++genAuthHandler :: AuthHandler Wai.Request ()+genAuthHandler =+  let handler req = case lookup "AuthHeader" (Wai.requestHeaders req) of+        Nothing -> throwError (err401 { errBody = "Missing auth header" })+        Just _ -> return ()+  in mkAuthHandler handler++genAuthServerContext :: Context '[ AuthHandler Wai.Request () ]+genAuthServerContext = genAuthHandler :. EmptyContext++genAuthServer :: Application+genAuthServer = serveWithContext genAuthAPI genAuthServerContext (const (return alice))++{-# NOINLINE manager' #-}+manager' :: C.Manager+manager' = unsafePerformIO $ C.newManager C.defaultManagerSettings++runClient :: ClientM a -> BaseUrl -> IO (Either ClientError a)+runClient x baseUrl' = runClientM x (mkClientEnv manager' baseUrl')++-- * utils++startWaiApp :: Application -> IO (ThreadId, BaseUrl)+startWaiApp app = do+    (port, socket) <- openTestSocket+    let settings = setPort port defaultSettings+    thread <- forkIO $ runSettingsSocket settings socket app+    return (thread, BaseUrl Http "localhost" port "")+++endWaiApp :: (ThreadId, BaseUrl) -> IO ()+endWaiApp (thread, _) = killThread thread++openTestSocket :: IO (Port, Socket)+openTestSocket = do+  s <- socket AF_INET Stream defaultProtocol+  let localhost = tupleToHostAddress (127, 0, 0, 1)+  bind s (SockAddrInet defaultPort localhost)+  listen s 1+  port <- socketPort s+  return (fromIntegral port, s)++pathGen :: Gen (NonEmptyList Char)+pathGen = fmap NonEmpty path+ where+  path = listOf1 $ elements $+    filter (not . (`elem` ("?%[]/#;" :: String))) $+    filter isPrint $+    map chr [0..127]++newtype UrlEncodedByteString = UrlEncodedByteString { unUrlEncodedByteString :: ByteString }++instance ToHttpApiData UrlEncodedByteString where+    toEncodedUrlPiece = byteString . HTTP.urlEncode True . unUrlEncodedByteString+    toUrlPiece = decodeUtf8 . HTTP.urlEncode True . unUrlEncodedByteString++instance FromHttpApiData UrlEncodedByteString where+    parseUrlPiece = pure . UrlEncodedByteString . HTTP.urlDecode True . encodeUtf8++newtype Verbatim = Verbatim { unVerbatim :: ByteString }++instance ToHttpApiData Verbatim where+    toEncodedUrlPiece = byteString . unVerbatim+    toUrlPiece = decodeUtf8 . unVerbatim++instance FromHttpApiData Verbatim where+    parseUrlPiece = pure . Verbatim . encodeUtf8++-- * range type example++type PaginatedAPI =+  "users" :> QueryParam "page" (Range 1 100) :> Get '[JSON] [Person]++usersServer :: Maybe (Range 1 100) -> Handler [Person]+usersServer mpage = do+  let pageNum = maybe 1 unRange mpage+  -- pageNum is guaranteed to be between 1 and 100+  return [Person "Example" $ fromIntegral pageNum]
+ test/Servant/ConnectionErrorSpec.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeApplications       #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.ConnectionErrorSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Control.Exception+                 (fromException)+import           Data.Maybe+                 (isJust)+import           Data.Monoid ()+import           Data.Proxy+import qualified Network.HTTP.Client                  as C+import           Test.Hspec++import           Servant.API+                 (Get, JSON)+import           Servant.Client+import           Servant.ClientTestUtils+++spec :: Spec+spec = describe "Servant.ConnectionErrorSpec" $ do+    connectionErrorSpec++type ConnectionErrorAPI = Get '[JSON] Int++connectionErrorAPI :: Proxy ConnectionErrorAPI+connectionErrorAPI = Proxy++connectionErrorSpec :: Spec+connectionErrorSpec = describe "Servant.Client.ClientError" $+    it "correctly catches ConnectionErrors when the HTTP request can't go through" $ do+        let getInt = client connectionErrorAPI+        let baseUrl' = BaseUrl Http "example.invalid" 80 ""+        let isHttpError (Left (ConnectionError e)) = isJust $ fromException @C.HttpException e+            isHttpError _ = False+        (isHttpError <$> runClient getInt baseUrl') `shouldReturn` True
+ test/Servant/FailSpec.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.FailSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Data.Monoid ()+import qualified Network.HTTP.Types                   as HTTP+import           Test.Hspec++import           Servant.API+                 ((:<|>) ((:<|>)))+import           Servant.Client+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.FailSpec" $ do+    failSpec++failSpec :: Spec+failSpec = beforeAll (startWaiApp failServer) $ afterAll endWaiApp $ do++    context "client returns errors appropriately" $ do+      it "reports FailureResponse" $ \(_, baseUrl) -> do+        let (_ :<|> _ :<|> _ :<|> getDeleteEmpty :<|> _) = client api+        Left res <- runClient getDeleteEmpty baseUrl+        case res of+          FailureResponse _ r | responseStatusCode r == HTTP.status404 -> return ()+          _ -> fail $ "expected 404 response, but got " <> show res++      it "reports DecodeFailure" $ \(_, baseUrl) -> do+        let (_ :<|> _ :<|> _ :<|> _ :<|> getCapture :<|> _) = client api+        Left res <- runClient (getCapture "foo") baseUrl+        case res of+          DecodeFailure _ _ -> return ()+          _ -> fail $ "expected DecodeFailure, but got " <> show res++      it "reports ConnectionError" $ \_ -> do+        let (getGetWrongHost :<|> _) = client api+        Left res <- runClient getGetWrongHost (BaseUrl Http "127.0.0.1" 19872 "")+        case res of+          ConnectionError _ -> return ()+          _ -> fail $ "expected ConnectionError, but got " <> show res++      it "reports UnsupportedContentType" $ \(_, baseUrl) -> do+        let (_ :<|> getGet :<|> _ ) = client api+        Left res <- runClient getGet baseUrl+        case res of+          UnsupportedContentType "application/octet-stream" _ -> return ()+          _ -> fail $ "expected UnsupportedContentType, but got " <> show res++      it "reports UnsupportedContentType when there are response headers" $ \(_, baseUrl) -> do+        Left res <- runClient getRespHeaders baseUrl+        case res of+          UnsupportedContentType "application/x-www-form-urlencoded" _ -> return ()+          _ -> fail $ "expected UnsupportedContentType, but got " <> show res++      it "reports InvalidContentTypeHeader" $ \(_, baseUrl) -> do+        let (_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> getBody :<|> _) = client api+        Left res <- runClient (getBody alice) baseUrl+        case res of+          InvalidContentTypeHeader _ -> return ()+          _ -> fail $ "expected InvalidContentTypeHeader, but got " <> show res
+ test/Servant/GenAuthSpec.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.GenAuthSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Control.Arrow+                 (left)+import           Data.Monoid ()+import qualified Network.HTTP.Types                   as HTTP+import           Test.Hspec++import           Servant.Client+import qualified Servant.Client.Core.Auth    as Auth+import qualified Servant.Client.Core.Request as Req+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.GenAuthSpec" $ do+    genAuthSpec++genAuthSpec :: Spec+genAuthSpec = beforeAll (startWaiApp genAuthServer) $ afterAll endWaiApp $ do+  context "Authentication works when requests are properly authenticated" $ do++    it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do+      let getProtected = client genAuthAPI+      let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "AuthHeader" ("cool" :: String) req)+      left show <$> runClient (getProtected authRequest) baseUrl `shouldReturn` Right alice++  context "Authentication is rejected when requests are not authenticated properly" $ do++    it "Authenticates a AuthProtect protected server appropriately" $ \(_, baseUrl) -> do+      let getProtected = client genAuthAPI+      let authRequest = Auth.mkAuthenticatedRequest () (\_ req -> Req.addHeader "Wrong" ("header" :: String) req)+      Left (FailureResponse _ r) <- runClient (getProtected authRequest) baseUrl+      responseStatusCode r `shouldBe` HTTP.Status 401 "Unauthorized"+
+ test/Servant/GenericSpec.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.GenericSpec (spec) where++import           Test.Hspec++import           Servant.Client ((//), (/:))+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.GenericSpec" $ do+    genericSpec++genericSpec :: Spec+genericSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do+  context "Record clients work as expected" $ do++    it "Client functions return expected values" $ \(_,baseUrl) -> do+      runClient (recordRoutes // version) baseUrl `shouldReturn` Right 42+      runClient (recordRoutes // echo /: "foo") baseUrl `shouldReturn` Right "foo"+    it "Clients can be nested" $ \(_,baseUrl) -> do+      runClient (recordRoutes // otherRoutes /: 42 // something) baseUrl `shouldReturn` Right ["foo", "bar", "pweet"]
+ test/Servant/HoistClientSpec.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE RecordWildCards        #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.HoistClientSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Data.Monoid ()+import           Data.Proxy+import           Test.Hspec++import           Servant.API+                 ((:<|>) ((:<|>)), (:>), Capture,+                 Get, JSON, Post)+import           Servant.Client+import           Servant.Server+import           Servant.ClientTestUtils+++spec :: Spec+spec = describe "Servant.HoistClientSpec" $ do+    hoistClientSpec++type HoistClientAPI = Get '[JSON] Int :<|> Capture "n" Int :> Post '[JSON] Int++hoistClientAPI :: Proxy HoistClientAPI+hoistClientAPI = Proxy++hoistClientServer :: Application -- implements HoistClientAPI+hoistClientServer = serve hoistClientAPI $ return 5 :<|> return ++hoistClientSpec :: Spec+hoistClientSpec = beforeAll (startWaiApp hoistClientServer) $ afterAll endWaiApp $ do+  describe "Servant.Client.hoistClient" $ do+    it "allows us to GET/POST/... requests in IO instead of ClientM" $ \(_, baseUrl) -> do+      let (getInt :<|> postInt)+            = hoistClient hoistClientAPI+                          (fmap (either (error . show) id) . flip runClient baseUrl)+                          (client hoistClientAPI)++      getInt `shouldReturn` 5+      postInt 5 `shouldReturn` 5
+ test/Servant/MiddlewareSpec.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}++module Servant.MiddlewareSpec (spec) where++import Control.Arrow (left)+import Control.Concurrent (newEmptyMVar, putMVar, takeMVar)+import Control.Exception (Exception, throwIO, try)+import Control.Monad.IO.Class+import Data.ByteString.Builder (toLazyByteString)+import Data.IORef (modifyIORef, newIORef, readIORef)+import Data.Monoid ()+import Prelude.Compat+import Servant.Client+import Servant.Client.Core (RequestF (..))+import Servant.Client.Internal.HttpClient (ClientMiddleware)+import Servant.ClientTestUtils+import Test.Hspec+import Prelude ()++runClientWithMiddleware :: ClientM a -> ClientMiddleware -> BaseUrl -> IO (Either ClientError a)+runClientWithMiddleware x mid baseUrl' =+  runClientM x ((mkClientEnv manager' baseUrl') {middleware = mid})++data CustomException = CustomException deriving (Show, Eq)++instance Exception CustomException++spec :: Spec+spec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do+  it "Raw request and response can be accessed in middleware" $ \(_, baseUrl) -> do+    mvarReq <- newEmptyMVar+    mvarResp <- newEmptyMVar++    let mid :: ClientMiddleware+        mid oldApp req = do+          -- "Log" request+          liftIO $ putMVar mvarReq req+          -- perform request+          resp <- oldApp req+          -- "Log" response+          liftIO $ putMVar mvarResp resp+          pure resp++    -- Same as without middleware+    left show <$> runClientWithMiddleware getGet mid baseUrl `shouldReturn` Right alice++    -- Access some raw request data+    req <- takeMVar mvarReq+    toLazyByteString (requestPath req) `shouldBe` "/get"++    -- Access some raw response data+    resp <- takeMVar mvarResp+    responseBody resp `shouldBe` "{\"_age\":42,\"_name\":\"Alice\"}"++  it "errors can be thrown in middleware" $ \(_, baseUrl) -> do+    let mid :: ClientMiddleware+        mid oldApp req = do+          -- perform request+          resp <- oldApp req+          -- throw error+          _ <- liftIO $ throwIO CustomException+          pure resp++    try (runClientWithMiddleware getGet mid baseUrl) `shouldReturn` Left CustomException++  it "runs in the expected order" $ \(_, baseUrl) -> do+    ref <- newIORef []++    let mid1 :: ClientMiddleware+        mid1 oldApp req = do+          liftIO $ modifyIORef ref (\xs -> xs <> ["req1"])+          resp <- oldApp req+          liftIO $ modifyIORef ref (\xs -> xs <> ["resp1"])+          pure resp++    let mid2 :: ClientMiddleware+        mid2 oldApp req = do+          liftIO $ modifyIORef ref (\xs -> xs <> ["req2"])+          resp <- oldApp req+          liftIO $ modifyIORef ref (\xs -> xs <> ["resp2"])+          pure resp++    let mid3 :: ClientMiddleware+        mid3 oldApp req = do+          liftIO $ modifyIORef ref (\xs -> xs <> ["req3"])+          resp <- oldApp req+          liftIO $ modifyIORef ref (\xs -> xs <> ["resp3"])+          pure resp++    let mid :: ClientMiddleware+        mid = mid1 . mid2 . mid3+        -- \^ Composition in "reverse order".+        -- It is equivalent to the following, which is more intuitive:+        -- mid :: ClientMiddleware+        -- mid oldApp = mid1 (mid2 (mid3 oldApp))++    -- Same as without middleware+    left show <$> runClientWithMiddleware getGet mid baseUrl `shouldReturn` Right alice++    ref <- readIORef ref+    ref `shouldBe` ["req1", "req2", "req3", "resp3", "resp2", "resp1"]
test/Servant/StreamSpec.hs view
@@ -1,17 +1,13 @@ {-# LANGUAGE CPP                    #-} {-# LANGUAGE ConstraintKinds        #-} {-# LANGUAGE DataKinds              #-}-{-# LANGUAGE DeriveGeneric          #-} {-# LANGUAGE FlexibleContexts       #-} {-# LANGUAGE FlexibleInstances      #-}-{-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GADTs                  #-} {-# LANGUAGE MultiParamTypeClasses  #-} {-# LANGUAGE OverloadedStrings      #-} {-# LANGUAGE PolyKinds              #-}-{-# LANGUAGE RecordWildCards        #-} {-# LANGUAGE ScopedTypeVariables    #-}-{-# LANGUAGE StandaloneDeriving     #-} {-# LANGUAGE TypeFamilies           #-} {-# LANGUAGE TypeOperators          #-} {-# LANGUAGE UndecidableInstances   #-}@@ -21,16 +17,9 @@  module Servant.StreamSpec (spec) where -import           Control.Monad-                 (when)-import           Control.Monad.Codensity-                 (Codensity (..))-import           Control.Monad.IO.Class-                 (MonadIO (..)) import           Control.Monad.Trans.Except import qualified Data.ByteString               as BS import           Data.Proxy-import qualified Data.TDigest                  as TD import qualified Network.HTTP.Client           as C import           Prelude () import           Prelude.Compat@@ -39,9 +28,6 @@                  NewlineFraming, NoFraming, OctetStream, SourceIO, StreamGet,                  ) import           Servant.Client.Streaming-import           Servant.ClientSpec-                 (Person (..))-import qualified Servant.ClientSpec            as CS import           Servant.Server import           Servant.Test.ComprehensiveAPI import           Servant.Types.SourceT@@ -49,17 +35,9 @@                  (getEntropy, getHardwareEntropy) import           System.IO.Unsafe                  (unsafePerformIO)-import           System.Mem-                 (performGC) import           Test.Hspec--#if MIN_VERSION_base(4,10,0)-import           GHC.Stats-                 (gc, gcdetails_live_bytes, getRTSStats)-#else-import           GHC.Stats-                 (currentBytesUsed, getGCStats)-#endif+import           Servant.ClientTestUtils (Person(..))+import qualified Servant.ClientTestUtils as CT  -- This declaration simply checks that all instances are in place. -- Note: this is streaming client@@ -120,7 +98,7 @@ testRunSourceIO = runExceptT . runSourceT  streamSpec :: Spec-streamSpec = beforeAll (CS.startWaiApp server) $ afterAll CS.endWaiApp $ do+streamSpec = beforeAll (CT.startWaiApp server) $ afterAll CT.endWaiApp $ do     it "works with Servant.API.StreamGet.Newline" $ \(_, baseUrl) -> do         withClient getGetNL baseUrl $ \(Right res) ->             testRunSourceIO res `shouldReturn` Right [alice, bob, alice]@@ -135,50 +113,3 @@         where           input = ["foo", "", "bar"]           output = ["foo", "bar"]--{--    it "streams in constant memory" $ \(_, baseUrl) -> do-        Right rs <- runClient getGetALot baseUrl-        performGC-        -- usage0 <- getUsage-        -- putStrLn $ "Start:  " ++ show usage0-        tdigest <- memoryUsage $ joinCodensitySourceT rs--        -- putStrLn $ "Median: " ++ show (TD.median tdigest)-        -- putStrLn $ "Mean:   " ++ show (TD.mean tdigest)-        -- putStrLn $ "Stddev: " ++ show (TD.stddev tdigest)--        -- forM_ [0.01, 0.1, 0.2, 0.5, 0.8, 0.9, 0.99] $ \q ->-        --    putStrLn $ "q" ++ show q ++ ": " ++ show (TD.quantile q tdigest)--        let Just stddev = TD.stddev tdigest--        -- standard deviation of 100k is ok, we generate 256M of data after all.-        -- On my machine deviation is 40k-50k-        stddev `shouldSatisfy` (< 100000)--memoryUsage :: SourceT IO BS.ByteString -> IO (TD.TDigest 25)-memoryUsage src = unSourceT src $ loop mempty (0 :: Int)-  where-    loop !acc !_ Stop          = return acc-    loop !_   !_ (Error err)   = fail err -- !-    loop !acc !n (Skip s)      = loop acc n s-    loop !acc !n (Effect ms)   = ms >>= loop acc n-    loop !acc !n (Yield _bs s) =  do-        usage  <- liftIO getUsage-        -- We perform GC in between as we generate garbage.-        when (n `mod` 1024 == 0) $ liftIO performGC-        loop (TD.insert usage acc) (n + 1) s--getUsage :: IO Double-getUsage = fromIntegral .-#if MIN_VERSION_base(4,10,0)-    gcdetails_live_bytes . gc <$> getRTSStats-#else-    currentBytesUsed <$> getGCStats-#endif-       memUsed `shouldSatisfy` (< megabytes 22)--megabytes :: Num a => a -> a-megabytes n = n * (1000 ^ (2 :: Int))--}
+ test/Servant/SuccessSpec.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE CPP                   #-}+{-# LANGUAGE ConstraintKinds       #-}+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE GADTs                 #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE PolyKinds             #-}+{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE TypeApplications      #-}+{-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE UndecidableInstances  #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.SuccessSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Control.Arrow+                 (left)+import           Control.Concurrent.STM+                 (atomically)+import           Control.Concurrent.STM.TVar+                 (newTVar, readTVar)+import           Data.Foldable+                 (forM_, toList)+import           Data.Maybe+                 (listToMaybe)+import           Data.Monoid ()+import           Data.Text+                 (Text)+import           Data.Text.Encoding+                 (encodeUtf8)+import qualified Network.HTTP.Client                as C+import qualified Network.HTTP.Types                 as HTTP+import           Test.Hspec+import           Test.Hspec.QuickCheck+import           Test.HUnit+import           Test.QuickCheck++import           Servant.API+                 (NoContent (NoContent), WithStatus (WithStatus), getHeaders, Headers(..))+import           Servant.Client+import qualified Servant.Client.Core.Request        as Req+import           Servant.ClientTestUtils+import           Servant.Test.ComprehensiveAPI++-- This declaration simply checks that all instances are in place.+_ = client comprehensiveAPIWithoutStreaming++spec :: Spec+spec = describe "Servant.SuccessSpec" $ successSpec++successSpec :: Spec+successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do+    describe "Servant.API.Get" $ do+      it "get root endpoint" $ \(_, baseUrl) -> left show <$> runClient getRoot baseUrl  `shouldReturn` Right carol++      it "get simple endpoint" $ \(_, baseUrl) -> left show <$> runClient getGet baseUrl  `shouldReturn` Right alice++      it "get redirection endpoint" $ \(_, baseUrl) -> left show <$> runClient getGet307 baseUrl `shouldReturn` Right "redirecting"++    describe "Servant.API.Delete" $ do+      it "allows empty content type" $ \(_, baseUrl) -> left show <$> runClient getDeleteEmpty baseUrl `shouldReturn` Right NoContent++      it "allows content type" $ \(_, baseUrl) -> left show <$> runClient getDeleteContentType baseUrl `shouldReturn` Right NoContent++    it "Servant.API.Capture" $ \(_, baseUrl) -> left show <$> runClient (getCapture "Paula") baseUrl `shouldReturn` Right (Person "Paula" 0)++    it "Servant.API.CaptureAll" $ \(_, baseUrl) -> do+      let expected = [Person "Paula" 0, Person "Peta" 1]+      left show <$> runClient (getCaptureAll ["Paula", "Peta"]) baseUrl `shouldReturn` Right expected++    it "Servant.API.ReqBody" $ \(_, baseUrl) -> do+      let p = Person "Clara" 42+      left show <$> runClient (getBody p) baseUrl `shouldReturn` Right p++    it "Servant.API FailureResponse" $ \(_, baseUrl) -> do+      left show <$> runClient (getQueryParam (Just "alice")) baseUrl `shouldReturn` Right alice+      Left (FailureResponse req _) <- runClient (getQueryParam (Just "bob")) baseUrl+      Req.requestPath req `shouldBe` (baseUrl, "/param")+      toList (Req.requestQueryString req) `shouldBe` [("name", Just "bob")]+      Req.requestMethod req `shouldBe` HTTP.methodGet++    it "Servant.API.QueryParam" $ \(_, baseUrl) -> do+      left show <$> runClient (getQueryParam (Just "alice")) baseUrl `shouldReturn` Right alice+      Left (FailureResponse _ r) <- runClient (getQueryParam (Just "bob")) baseUrl+      responseStatusCode r `shouldBe` HTTP.Status 400 "bob not found"++    it "Servant.API.QueryParam.QueryParams" $ \(_, baseUrl) -> do+      left show <$> runClient (getQueryParams []) baseUrl `shouldReturn` Right []+      left show <$> runClient (getQueryParams ["alice", "bob"]) baseUrl+        `shouldReturn` Right [Person "alice" 0, Person "bob" 1]++    context "Servant.API.QueryParam.QueryFlag" $+      forM_ [False, True] $ \ flag -> it (show flag) $ \(_, baseUrl) -> left show <$> runClient (getQueryFlag flag) baseUrl `shouldReturn` Right flag++    it "Servant.API.QueryParam.QueryString" $ \(_, baseUrl) -> do+      let qs = [("name", Just "bob"), ("age", Just "1")]+      left show <$> runClient (getQueryString qs) baseUrl `shouldReturn` Right (Person "bob" 1)++    it "Servant.API.QueryParam.DeepQuery" $ \(_, baseUrl) -> left show <$> runClient (getDeepQuery $ Filter 1 "bob") baseUrl `shouldReturn` (Right (Person "bob" 1))++    it "Servant.API.Fragment" $ \(_, baseUrl) -> left id <$> runClient getFragment baseUrl `shouldReturn` Right alice++    it "Servant.API.Raw on success" $ \(_, baseUrl) -> do+      res <- runClient (getRawSuccess HTTP.methodGet) baseUrl+      case res of+        Left e -> assertFailure $ show e+        Right r -> do+          responseStatusCode r `shouldBe` HTTP.status200+          responseBody r `shouldBe` "rawSuccess"++    it "Servant.API.Raw should return a Left in case of failure" $ \(_, baseUrl) -> do+      res <- runClient (getRawFailure HTTP.methodGet) baseUrl+      case res of+        Right _ -> assertFailure "expected Left, but got Right"+        Left (FailureResponse _ r) -> do+          responseStatusCode r `shouldBe` HTTP.status400+          responseBody r `shouldBe` "rawFailure"+        Left e -> assertFailure $ "expected FailureResponse, but got " ++ show e++    it "Returns headers appropriately" $ \(_, baseUrl) -> do+      res <- runClient getRespHeaders baseUrl+      case res of+        Left e -> assertFailure $ show e+        Right val -> getHeaders val `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]++    it "Returns headers on UVerb requests" $ \(_, baseUrl) -> do+      res <- runClient getUVerbRespHeaders baseUrl+      case res of+        Left e -> assertFailure $ show e+        Right val -> case matchUnion val of+          Just (WithStatus val' :: WithStatus 200 (Headers TestHeaders Bool))+            -> getHeaders val' `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]+          Nothing -> assertFailure "unexpected alternative of union"++    it "Returns multiple Set-Cookie headers appropriately" $ \(_, baseUrl) -> do+      res <- runClient getSetCookieHeaders baseUrl+      case res of+        Left e -> assertFailure $ show e+        Right val -> getHeaders val `shouldBe` [("Set-Cookie", "cookie1"), ("Set-Cookie", "cookie2")]++    it "Stores Cookie in CookieJar after a redirect" $ \(_, baseUrl) -> do+      mgr <- C.newManager C.defaultManagerSettings+      cj <- atomically . newTVar $ C.createCookieJar []+      _ <- runClientM (getRedirectWithCookie HTTP.methodGet) (ClientEnv mgr baseUrl (Just cj) defaultMakeClientRequest id)+      cookie <- listToMaybe . C.destroyCookieJar <$> atomically (readTVar cj)+      C.cookie_name <$> cookie `shouldBe` Just "testcookie"+      C.cookie_value <$> cookie `shouldBe` Just "test"++    it "Can modify the outgoing Request using the ClientEnv" $ \(_, baseUrl) -> do+      mgr <- C.newManager C.defaultManagerSettings+      -- In proper situation, extra headers should probably be visible in API type.+      -- However, testing for response timeout is difficult, so we test with something which is easy to observe+      let createClientRequest url r = fmap (\req -> req { C.requestHeaders = [("X-Added-Header", "XXX")] })+                                           (defaultMakeClientRequest url r)+          clientEnv = (mkClientEnv mgr baseUrl) { makeClientRequest = createClientRequest }+      res <- runClientM (getRawSuccessPassHeaders HTTP.methodGet) clientEnv+      case res of+        Left e ->+          assertFailure $ show e+        Right r ->+          ("X-Added-Header", "XXX") `elem` toList (responseHeaders r) `shouldBe` True++    modifyMaxSuccess (const 20) $ it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $ \(_, baseUrl) ->+      property $ forAllShrink pathGen shrink $ \(NonEmpty cap) num flag body ->+        ioProperty $ do+          result <- left show <$> runClient (getMultiple cap num flag body) baseUrl+          return $+            result === Right (cap, num, flag, body)++    context "With a route that can either return success or redirect" $ do+      it "Redirects when appropriate" $ \(_, baseUrl) -> do+        eitherResponse <- runClient (uverbGetSuccessOrRedirect True) baseUrl+        case eitherResponse of+          Left clientError -> fail $ show clientError+          Right response -> matchUnion response `shouldBe` Just (WithStatus @301 @Text "redirecting")++      it "Returns a proper response when appropriate" $ \(_, baseUrl) -> do+        eitherResponse <- runClient (uverbGetSuccessOrRedirect False) baseUrl+        case eitherResponse of+          Left clientError -> fail $ show clientError+          Right response -> matchUnion response `shouldBe` Just (WithStatus @200 alice)++    context "with a route that uses uverb but only has a single response" $+      it "returns the expected response" $ \(_, baseUrl) -> do+        eitherResponse <- runClient uverbGetCreated baseUrl+        case eitherResponse of+          Left clientError -> fail $ show clientError+          Right response -> matchUnion response `shouldBe` Just (WithStatus @201 carol)++    it "encodes URL pieces following ToHttpApiData instance" $ \(_, baseUrl) -> do+      let textOrig = "*"+      eitherResponse <- runClient (captureVerbatim $ Verbatim $ encodeUtf8 textOrig) baseUrl+      case eitherResponse of+        Left clientError -> fail $ show clientError+        Right textBack -> textBack `shouldBe` textOrig
+ test/Servant/WrappedApiSpec.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE CPP                    #-}+{-# LANGUAGE ConstraintKinds        #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE GADTs                  #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE PolyKinds              #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeOperators          #-}+{-# LANGUAGE UndecidableInstances   #-}+{-# OPTIONS_GHC -freduction-depth=100 #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}++module Servant.WrappedApiSpec (spec) where++import           Prelude ()+import           Prelude.Compat++import           Data.Kind (Type)+import           Control.Exception+                 (bracket)+import           Control.Monad.Error.Class+                 (throwError)+import           Data.Monoid ()+import           Data.Proxy+import qualified Network.HTTP.Types                   as HTTP+import           Test.Hspec++import           Servant.API+                 (Delete, Get, JSON, Post, Put)+import           Servant.Client+import           Servant.Server+import           Servant.ClientTestUtils++spec :: Spec+spec = describe "Servant.WrappedApiSpec" $ do+    wrappedApiSpec++data WrappedApi where+  WrappedApi :: (HasServer (api :: Type) '[], Server api ~ Handler a,+                 HasClient ClientM api, Client ClientM api ~ ClientM ()) =>+    Proxy api -> WrappedApi++wrappedApiSpec :: Spec+wrappedApiSpec = describe "error status codes" $ do+  let serveW api = serve api $ throwError $ ServerError 500 "error message" "" []+  context "are correctly handled by the client" $+    let test :: (WrappedApi, String) -> Spec+        test (WrappedApi api, desc) =+          it desc $ bracket (startWaiApp $ serveW api) endWaiApp $ \(_, baseUrl) -> do+            let getResponse :: ClientM ()+                getResponse = client api+            Left (FailureResponse _ r) <- runClient getResponse baseUrl+            responseStatusCode r `shouldBe` HTTP.Status 500 "error message"+    in mapM_ test $+        (WrappedApi (Proxy :: Proxy (Delete '[JSON] ())), "Delete") :+        (WrappedApi (Proxy :: Proxy (Get '[JSON] ())), "Get") :+        (WrappedApi (Proxy :: Proxy (Post '[JSON] ())), "Post") :+        (WrappedApi (Proxy :: Proxy (Put '[JSON] ())), "Put") :+        []