@@ -2,8 +2,10 @@
2{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE DataKinds #-}
3{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE DeriveGeneric #-}
4{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE LambdaCase #-}
5{-# LANGUAGE NoMonomorphismRestriction #-}
6{-# LANGUAGE NoMonomorphismRestriction #-}
6{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE QuasiQuotes #-}
7{-# LANGUAGE RankNTypes #-}
9{-# LANGUAGE RankNTypes #-}
8{-# LANGUAGE RecordWildCards #-}
10{-# LANGUAGE RecordWildCards #-}
9{-# LANGUAGE ScopedTypeVariables #-}
11{-# LANGUAGE ScopedTypeVariables #-}
@@ -14,10 +16,12 @@ module Network.HTTP.ReqSpec
16import Control.Exception (throwIO)
18import Control.Exception (throwIO)
17import Control.Retry
20import Control.Retry
18import Data.Aeson (ToJSON (..))
21import Data.Aeson (ToJSON (..))
19import Data.ByteString (ByteString)
22import Data.ByteString (ByteString)
23+import Data.Either (isRight)
20−import Data.Maybe (isNothing, fromJust)
24+import Data.Maybe (isNothing, fromJust, fromMaybe)
21import Data.Proxy
25import Data.Proxy
22import Data.Text (Text)
26import Data.Text (Text)
23import Data.Time
27import Data.Time
@@ -26,17 +30,21 @@ import Network.HTTP.Req
26import Test.Hspec
30import Test.Hspec
27import Test.Hspec.Core.Spec (SpecM)
31import Test.Hspec.Core.Spec (SpecM)
28import Test.QuickCheck
32import Test.QuickCheck
29import qualified Blaze.ByteString.Builder as BB
34import qualified Blaze.ByteString.Builder as BB
30import qualified Data.Aeson as A
35import qualified Data.Aeson as A
31import qualified Data.ByteString as B
36import qualified Data.ByteString as B
32import qualified Data.ByteString.Char8 as B8
37import qualified Data.ByteString.Char8 as B8
33import qualified Data.ByteString.Lazy as BL
38import qualified Data.ByteString.Lazy as BL
34import qualified Data.CaseInsensitive as CI
39import qualified Data.CaseInsensitive as CI
40+import qualified Data.List.NonEmpty as NE
35import qualified Data.Text as T
41import qualified Data.Text as T
36import qualified Data.Text.Encoding as T
42import qualified Data.Text.Encoding as T
37import qualified Network.HTTP.Client as L
43import qualified Network.HTTP.Client as L
38import qualified Network.HTTP.Types as Y
44import qualified Network.HTTP.Types as Y
39import qualified Network.HTTP.Types.Header as Y
45import qualified Network.HTTP.Types.Header as Y
46+import qualified Text.URI as URI
47+import qualified Text.URI.QQ as QQ
41#if !MIN_VERSION_base(4,13,0)
49#if !MIN_VERSION_base(4,13,0)
42import Data.Semigroup ((<>))
50import Data.Semigroup ((<>))
@@ -104,67 +112,70 @@ spec = do
104 request <- req_ GET url' NoReqBody mempty
112 request <- req_ GET url' NoReqBody mempty
105 L.host request `shouldBe` urlEncode host
113 L.host request `shouldBe` urlEncode host
106 L.path request `shouldBe` encodePathPieces pieces
114 L.path request `shouldBe` encodePathPieces pieces
107− describe "parseUrlHttp" $ do
116+ describe "useHttpURI" $ do
108 it "does not recognize non-http schemes" $
117 it "does not recognize non-http schemes" $
119+ when (URI.uriScheme uri /= Just [QQ.scheme|http|]) $
109− parseUrlHttp "https://httpbin.org" `shouldSatisfy` isNothing
120+ useHttpURI uri `shouldSatisfy` isNothing
110− it "parses correct URLs" $
121+ it "accepts correct URLs" $
111− property $ \host mport' pieces queryParams -> do
122+ property $ \uri' -> do
112− let (url', path, queryString) =
123+ unless (isRight (URI.uriAuthority uri')) discard
113− assembleUrl Http host mport' pieces queryParams
124+ let uri = uri' { URI.uriScheme = Just [QQ.scheme|http|] }
114− (url'', options) = fromJust (parseUrlHttp url')
125+ (url', options) = fromJust (useHttpURI uri)
115− request <- req_ GET url'' NoReqBody options
126+ request <- req_ GET url' NoReqBody options
116− L.host request `shouldBe` urlEncode (unHost host)
127+ L.host request `shouldBe` uriHost uri
117− L.port request `shouldBe` maybe 80 getNonNegative mport'
128+ L.port request `shouldBe` uriPort 80 uri
118− L.path request `shouldBe` path
129+ L.path request `shouldBe` uriPath uri
119− L.queryString request `shouldBe` queryString
130+ L.queryString request `shouldBe` uriQuery uri
120− it "rejects gibberish in port component" $ do
131+ lookup "Authorization" (L.requestHeaders request)
121− parseUrlHttp "http://my-site.com:bob/far" `shouldSatisfy` isNothing
132+ `shouldBe` uriBasicAuth uri
122− parseUrlHttp "http://my-site.com:7001uh/api" `shouldSatisfy` isNothing
123− parseUrlHttp "http://my-site.com:/bar" `shouldSatisfy` isNothing
124− describe "parseUrlHttps" $ do
133+ describe "useHttpsURI" $ do
125 it "does not recognize non-https schemes" $
134 it "does not recognize non-https schemes" $
136+ when (URI.uriScheme uri /= Just [QQ.scheme|https|]) $
126− parseUrlHttps "http://httpbin.org" `shouldSatisfy` isNothing
137+ useHttpsURI uri `shouldSatisfy` isNothing
127 it "parses correct URLs" $
138 it "parses correct URLs" $
128− property $ \host mport' pieces queryParams -> do
139+ property $ \uri' -> do
129− let (url', path, queryString) =
140+ unless (isRight (URI.uriAuthority uri')) discard
130− assembleUrl Https host mport' pieces queryParams
141+ let uri = uri' { URI.uriScheme = Just [QQ.scheme|https|] }
131− (url'', options) = fromJust (parseUrlHttps url')
142+ (url', options) = fromJust (useHttpsURI uri)
132− request <- req_ GET url'' NoReqBody options
143+ request <- req_ GET url' NoReqBody options
133− L.host request `shouldBe` urlEncode (unHost host)
144+ L.host request `shouldBe` uriHost uri
134− L.port request `shouldBe` maybe 443 getNonNegative mport'
145+ L.port request `shouldBe` uriPort 443 uri
135− L.path request `shouldBe` path
146+ L.path request `shouldBe` uriPath uri
136− L.queryString request `shouldBe` queryString
147+ L.queryString request `shouldBe` uriQuery uri
137− it "rejects gibberish in port component" $ do
148+ lookup "Authorization" (L.requestHeaders request)
138− parseUrlHttp "https://my-site.com:bob/far" `shouldSatisfy` isNothing
149+ `shouldBe` uriBasicAuth uri
139− parseUrlHttp "https://my-site.com:7001uh/api" `shouldSatisfy` isNothing
140− parseUrlHttp "https://my-site.com:/bar" `shouldSatisfy` isNothing
141− describe "parseUrl" $ do
150+ describe "useURI" $ do
142 it "does not recognize non-http and non-https schemes" $
151 it "does not recognize non-http and non-https schemes" $
153+ when ((URI.uriScheme uri /= Just [QQ.scheme|http|] &&
154+ (URI.uriScheme uri /= Just [QQ.scheme|https|]))) $
143− parseUrl "ftp://httpbin.org" `shouldSatisfy` isNothing
155+ useURI uri `shouldSatisfy` isNothing
144− it "parses correct http URLs" $
156+ it "parses correct URLs" $
145− property $ \host mport' pieces queryParams -> do
157+ property $ \uri' -> do
146− let (url', path, queryString) =
158+ unless (isRight (URI.uriAuthority uri')) discard
147− assembleUrl Http host mport' pieces queryParams
159+ let uriHttp = uri' { URI.uriScheme = Just [QQ.scheme|http|] }
160+ uriHttps = uri' { URI.uriScheme = Just [QQ.scheme|https|] }
148− Left (url'', options) = fromJust (parseUrl url')
162+ let Left (url', options) = fromJust (useURI uriHttp)
163+ in req_ GET url' NoReqBody options
165+ let Right (url', options) = fromJust (useURI uriHttps)
149− request <- req_ GET url'' NoReqBody options
166+ in req_ GET url' NoReqBody options
150− L.host request `shouldBe` urlEncode (unHost host)
167+ L.host requestHttp `shouldBe` uriHost uriHttp
151− L.port request `shouldBe` maybe 80 getNonNegative mport'
168+ L.host requestHttps `shouldBe` uriHost uriHttps
152− L.path request `shouldBe` path
169+ L.port requestHttp `shouldBe` uriPort 80 uriHttp
153− L.queryString request `shouldBe` queryString
170+ L.port requestHttps `shouldBe` uriPort 443 uriHttps
154− it "parses correct https URLs" $
155− property $ \host mport' pieces queryParams -> do
156− let (url', path, queryString) =
157− assembleUrl Https host mport' pieces queryParams
158− Right (url'', options) = fromJust (parseUrl url')
159− request <- req_ GET url'' NoReqBody options
160− L.host request `shouldBe` urlEncode (unHost host)
161− L.port request `shouldBe` maybe 443 getNonNegative mport'
162− L.path request `shouldBe` path
171+ L.path requestHttp `shouldBe` uriPath uriHttp
163− L.queryString request `shouldBe` queryString
172+ L.path requestHttps `shouldBe` uriPath uriHttps
164− it "rejects gibberish in port component" $ do
173+ L.queryString requestHttp `shouldBe` uriQuery uriHttp
165− parseUrl "http://my-site.com:bob/far" `shouldSatisfy` isNothing
174+ L.queryString requestHttps `shouldBe` uriQuery uriHttps
166− parseUrl "https://my-site.com:7001uh/api" `shouldSatisfy` isNothing
175+ lookup "Authorization" (L.requestHeaders requestHttp)
167− parseUrl "http://my-site.com:/bar" `shouldSatisfy` isNothing
176+ `shouldBe` uriBasicAuth uriHttp
177+ lookup "Authorization" (L.requestHeaders requestHttps)
178+ `shouldBe` uriBasicAuth uriHttps
169 describe "bodies" $ do
180 describe "bodies" $ do
170 describe "NoReqBody" $
181 describe "NoReqBody" $
@@ -228,24 +239,22 @@ spec = do
228 property $ \username password -> do
239 property $ \username password -> do
229 request <- req_ GET url NoReqBody (basicAuth username password)
240 request <- req_ GET url NoReqBody (basicAuth username password)
230 lookup "Authorization" (L.requestHeaders request) `shouldBe`
241 lookup "Authorization" (L.requestHeaders request) `shouldBe`
231− pure (basicAuthHeader username password)
242+ Just (basicAuthHeader username password)
232 it "overwrites manual setting of header" $
243 it "overwrites manual setting of header" $
233 property $ \username password value -> do
244 property $ \username password value -> do
234 request0 <- req_ GET url NoReqBody
245 request0 <- req_ GET url NoReqBody
235 (basicAuth username password <> header "Authorization" value)
246 (basicAuth username password <> header "Authorization" value)
236 request1 <- req_ GET url NoReqBody
247 request1 <- req_ GET url NoReqBody
237 (header "Authorization" value <> basicAuth username password)
248 (header "Authorization" value <> basicAuth username password)
238− let result = basicAuthHeader username password
249+ let result = Just (basicAuthHeader username password)
239− lookup "Authorization" (L.requestHeaders request0) `shouldBe`
250+ lookup "Authorization" (L.requestHeaders request0) `shouldBe` result
241− lookup "Authorization" (L.requestHeaders request1) `shouldBe`
251+ lookup "Authorization" (L.requestHeaders request1) `shouldBe` result
243 it "left auth option wins" $
252 it "left auth option wins" $
244 property $ \username0 password0 username1 password1 -> do
253 property $ \username0 password0 username1 password1 -> do
245 request <- req_ GET url NoReqBody
254 request <- req_ GET url NoReqBody
246 (basicAuth username0 password0 <> basicAuth username1 password1)
255 (basicAuth username0 password0 <> basicAuth username1 password1)
247 lookup "Authorization" (L.requestHeaders request) `shouldBe`
256 lookup "Authorization" (L.requestHeaders request) `shouldBe`
248− pure (basicAuthHeader username0 password0)
257+ Just (basicAuthHeader username0 password0)
249 describe "oAuth2Bearer" $ do
258 describe "oAuth2Bearer" $ do
250 it "sets Authorization header to correct value" $
259 it "sets Authorization header to correct value" $
251 property $ \token -> do
260 property $ \token -> do
@@ -447,28 +456,47 @@ urlEncode = Y.urlEncode False . T.encodeUtf8
447encodePathPieces :: [Text] -> ByteString
456encodePathPieces :: [Text] -> ByteString
448encodePathPieces = BL.toStrict . BB.toLazyByteString . Y.encodePathSegments
457encodePathPieces = BL.toStrict . BB.toLazyByteString . Y.encodePathSegments
450−-- | Assemble a URL.
459+-- | Get host from a 'URI'. This function is not total.
452−assembleUrl
461+uriHost :: URI -> ByteString
453− :: Scheme -- ^ Scheme
462+uriHost uri = fromJust $
454− -> Host -- ^ Host
463+ urlEncode . URI.unRText . URI.authHost
455− -> Maybe (NonNegative Int) -- ^ Port
464+ <$> either (const Nothing) Just (URI.uriAuthority uri)
456− -> [Text] -- ^ Path pieces
465+
457− -> QueryParams -- ^ Query parameters
466+-- | Get part from a 'URI' defaulting to the provided value.
458− -> (ByteString, ByteString, ByteString) -- ^ URL, path, query string
467+
459−assembleUrl scheme' (Host host') mport' pathPieces (QueryParams queryParams) =
468+uriPort :: Int -> URI -> Int
460− (scheme <> host <> port' <> path <> queryString, path, queryString)
469+uriPort def uri = maybe def (fromIntegral) $
461− where
470+ either (const Nothing) Just (URI.uriAuthority uri) >>= URI.authPort
462− scheme = case scheme' of
471+
463− Http -> "http://"
472+-- | Get path from 'URI'.
464− Https -> "https://"
473+
465− host = urlEncode host'
474+uriPath :: URI -> ByteString
466− port' =
475+uriPath uri = fromMaybe "" $ do
467− case mport' of
476+ (_, xs) <- URI.uriPath uri
469− Just (NonNegative x) -> ":" <> B8.pack (show x)
470− path = encodePathPieces pathPieces
477+ (return . encodePathPieces . fmap URI.unRText . NE.toList) xs
479+-- | Get query string from 'URI'.
481+uriQuery :: URI -> ByteString
483+ let liftQueryParam = \case
484+ URI.QueryFlag t -> (URI.unRText t, Nothing)
485+ URI.QueryParam k v -> (URI.unRText k, Just (URI.unRText v))
471− queryString = Y.renderQuery True (Y.queryTextToQuery queryParams)
486+ Y.renderQuery True (Y.queryTextToQuery (liftQueryParam <$> (URI.uriQuery uri)))
488+-- | Predict the headrs that should be set if the given 'URI' has username
489+-- and password in it.
491+uriBasicAuth :: URI -> Maybe ByteString
492+uriBasicAuth uri = do
493+ auth <- either (const Nothing) Just (URI.uriAuthority uri)
494+ URI.UserInfo {..} <- URI.authUserInfo auth
495+ let username = T.encodeUtf8 (URI.unRText uiUsername)
496+ password = maybe "" (T.encodeUtf8 . URI.unRText) uiPassword
497+ return (basicAuthHeader username password)
499+-- | Render a query as lazy 'BL.ByteString'.
473renderQuery :: [(Text, Maybe Text)] -> BL.ByteString
501renderQuery :: [(Text, Maybe Text)] -> BL.ByteString
474renderQuery = BL.fromStrict . Y.renderQuery False . Y.queryTextToQuery
502renderQuery = BL.fromStrict . Y.renderQuery False . Y.queryTextToQuery