@@ -2,8 +2,10 @@
22{-# LANGUAGE DataKinds #-}
33{-# LANGUAGE DeriveGeneric #-}
44{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE LambdaCase #-}
56{-# LANGUAGE NoMonomorphismRestriction #-}
67{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE QuasiQuotes #-}
79{-# LANGUAGE RankNTypes #-}
810{-# LANGUAGE RecordWildCards #-}
911{-# LANGUAGE ScopedTypeVariables #-}
@@ -14,10 +16,12 @@ module Network.HTTP.ReqSpec
1416where
1517
1618import Control.Exception (throwIO)
19+import Control.Monad
1720import Control.Retry
1821import Data.Aeson (ToJSON (..))
1922import Data.ByteString (ByteString)
20−import Data.Maybe (isNothing, fromJust)
23+import Data.Either (isRight)
24+import Data.Maybe (isNothing, fromJust, fromMaybe)
2125import Data.Proxy
2226import Data.Text (Text)
2327import Data.Time
@@ -26,17 +30,21 @@ import Network.HTTP.Req
2630import Test.Hspec
2731import Test.Hspec.Core.Spec (SpecM)
2832import Test.QuickCheck
33+import Text.URI (URI)
2934import qualified Blaze.ByteString.Builder as BB
3035import qualified Data.Aeson as A
3136import qualified Data.ByteString as B
3237import qualified Data.ByteString.Char8 as B8
3338import qualified Data.ByteString.Lazy as BL
3439import qualified Data.CaseInsensitive as CI
40+import qualified Data.List.NonEmpty as NE
3541import qualified Data.Text as T
3642import qualified Data.Text.Encoding as T
3743import qualified Network.HTTP.Client as L
3844import qualified Network.HTTP.Types as Y
3945import qualified Network.HTTP.Types.Header as Y
46+import qualified Text.URI as URI
47+import qualified Text.URI.QQ as QQ
4048
4149#if !MIN_VERSION_base(4,13,0)
4250import Data.Semigroup ((<>))
@@ -104,67 +112,70 @@ spec = do
104112 request <- req_ GET url' NoReqBody mempty
105113 L.host request `shouldBe` urlEncode host
106114 L.path request `shouldBe` encodePathPieces pieces
107− describe "parseUrlHttp" $ do
115+
116+ describe "useHttpURI" $ do
108117 it "does not recognize non-http schemes" $
109− parseUrlHttp "https://httpbin.org" `shouldSatisfy` isNothing
110− it "parses correct URLs" $
111− property $ \host mport' pieces queryParams -> do
112− let (url', path, queryString) =
113− assembleUrl Http host mport' pieces queryParams
114− (url'', options) = fromJust (parseUrlHttp url')
115− request <- req_ GET url'' NoReqBody options
116− L.host request `shouldBe` urlEncode (unHost host)
117− L.port request `shouldBe` maybe 80 getNonNegative mport'
118− L.path request `shouldBe` path
119− L.queryString request `shouldBe` queryString
120− it "rejects gibberish in port component" $ do
121− parseUrlHttp "http://my-site.com:bob/far" `shouldSatisfy` isNothing
122− parseUrlHttp "http://my-site.com:7001uh/api" `shouldSatisfy` isNothing
123− parseUrlHttp "http://my-site.com:/bar" `shouldSatisfy` isNothing
124− describe "parseUrlHttps" $ do
118+ property $ \uri ->
119+ when (URI.uriScheme uri /= Just [QQ.scheme|http|]) $
120+ useHttpURI uri `shouldSatisfy` isNothing
121+ it "accepts correct URLs" $
122+ property $ \uri' -> do
123+ unless (isRight (URI.uriAuthority uri')) discard
124+ let uri = uri' { URI.uriScheme = Just [QQ.scheme|http|] }
125+ (url', options) = fromJust (useHttpURI uri)
126+ request <- req_ GET url' NoReqBody options
127+ L.host request `shouldBe` uriHost uri
128+ L.port request `shouldBe` uriPort 80 uri
129+ L.path request `shouldBe` uriPath uri
130+ L.queryString request `shouldBe` uriQuery uri
131+ lookup "Authorization" (L.requestHeaders request)
132+ `shouldBe` uriBasicAuth uri
133+ describe "useHttpsURI" $ do
125134 it "does not recognize non-https schemes" $
126− parseUrlHttps "http://httpbin.org" `shouldSatisfy` isNothing
135+ property $ \uri ->
136+ when (URI.uriScheme uri /= Just [QQ.scheme|https|]) $
137+ useHttpsURI uri `shouldSatisfy` isNothing
127138 it "parses correct URLs" $
128− property $ \host mport' pieces queryParams -> do
129− let (url', path, queryString) =
130− assembleUrl Https host mport' pieces queryParams
131− (url'', options) = fromJust (parseUrlHttps url')
132− request <- req_ GET url'' NoReqBody options
133− L.host request `shouldBe` urlEncode (unHost host)
134− L.port request `shouldBe` maybe 443 getNonNegative mport'
135− L.path request `shouldBe` path
136− L.queryString request `shouldBe` queryString
137− it "rejects gibberish in port component" $ do
138− parseUrlHttp "https://my-site.com:bob/far" `shouldSatisfy` isNothing
139− parseUrlHttp "https://my-site.com:7001uh/api" `shouldSatisfy` isNothing
140− parseUrlHttp "https://my-site.com:/bar" `shouldSatisfy` isNothing
141− describe "parseUrl" $ do
139+ property $ \uri' -> do
140+ unless (isRight (URI.uriAuthority uri')) discard
141+ let uri = uri' { URI.uriScheme = Just [QQ.scheme|https|] }
142+ (url', options) = fromJust (useHttpsURI uri)
143+ request <- req_ GET url' NoReqBody options
144+ L.host request `shouldBe` uriHost uri
145+ L.port request `shouldBe` uriPort 443 uri
146+ L.path request `shouldBe` uriPath uri
147+ L.queryString request `shouldBe` uriQuery uri
148+ lookup "Authorization" (L.requestHeaders request)
149+ `shouldBe` uriBasicAuth uri
150+ describe "useURI" $ do
142151 it "does not recognize non-http and non-https schemes" $
143− parseUrl "ftp://httpbin.org" `shouldSatisfy` isNothing
144− it "parses correct http URLs" $
145− property $ \host mport' pieces queryParams -> do
146− let (url', path, queryString) =
147− assembleUrl Http host mport' pieces queryParams
148− Left (url'', options) = fromJust (parseUrl url')
149− request <- req_ GET url'' NoReqBody options
150− L.host request `shouldBe` urlEncode (unHost host)
151− L.port request `shouldBe` maybe 80 getNonNegative mport'
152− L.path request `shouldBe` path
153− L.queryString request `shouldBe` queryString
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
163− L.queryString request `shouldBe` queryString
164− it "rejects gibberish in port component" $ do
165− parseUrl "http://my-site.com:bob/far" `shouldSatisfy` isNothing
166− parseUrl "https://my-site.com:7001uh/api" `shouldSatisfy` isNothing
167− parseUrl "http://my-site.com:/bar" `shouldSatisfy` isNothing
152+ property $ \uri ->
153+ when ((URI.uriScheme uri /= Just [QQ.scheme|http|] &&
154+ (URI.uriScheme uri /= Just [QQ.scheme|https|]))) $
155+ useURI uri `shouldSatisfy` isNothing
156+ it "parses correct URLs" $
157+ property $ \uri' -> do
158+ unless (isRight (URI.uriAuthority uri')) discard
159+ let uriHttp = uri' { URI.uriScheme = Just [QQ.scheme|http|] }
160+ uriHttps = uri' { URI.uriScheme = Just [QQ.scheme|https|] }
161+ requestHttp <-
162+ let Left (url', options) = fromJust (useURI uriHttp)
163+ in req_ GET url' NoReqBody options
164+ requestHttps <-
165+ let Right (url', options) = fromJust (useURI uriHttps)
166+ in req_ GET url' NoReqBody options
167+ L.host requestHttp `shouldBe` uriHost uriHttp
168+ L.host requestHttps `shouldBe` uriHost uriHttps
169+ L.port requestHttp `shouldBe` uriPort 80 uriHttp
170+ L.port requestHttps `shouldBe` uriPort 443 uriHttps
171+ L.path requestHttp `shouldBe` uriPath uriHttp
172+ L.path requestHttps `shouldBe` uriPath uriHttps
173+ L.queryString requestHttp `shouldBe` uriQuery uriHttp
174+ L.queryString requestHttps `shouldBe` uriQuery uriHttps
175+ lookup "Authorization" (L.requestHeaders requestHttp)
176+ `shouldBe` uriBasicAuth uriHttp
177+ lookup "Authorization" (L.requestHeaders requestHttps)
178+ `shouldBe` uriBasicAuth uriHttps
168179
169180 describe "bodies" $ do
170181 describe "NoReqBody" $
@@ -228,24 +239,22 @@ spec = do
228239 property $ \username password -> do
229240 request <- req_ GET url NoReqBody (basicAuth username password)
230241 lookup "Authorization" (L.requestHeaders request) `shouldBe`
231− pure (basicAuthHeader username password)
242+ Just (basicAuthHeader username password)
232243 it "overwrites manual setting of header" $
233244 property $ \username password value -> do
234245 request0 <- req_ GET url NoReqBody
235246 (basicAuth username password <> header "Authorization" value)
236247 request1 <- req_ GET url NoReqBody
237248 (header "Authorization" value <> basicAuth username password)
238− let result = basicAuthHeader username password
239− lookup "Authorization" (L.requestHeaders request0) `shouldBe`
240− pure result
241− lookup "Authorization" (L.requestHeaders request1) `shouldBe`
242− pure result
249+ let result = Just (basicAuthHeader username password)
250+ lookup "Authorization" (L.requestHeaders request0) `shouldBe` result
251+ lookup "Authorization" (L.requestHeaders request1) `shouldBe` result
243252 it "left auth option wins" $
244253 property $ \username0 password0 username1 password1 -> do
245254 request <- req_ GET url NoReqBody
246255 (basicAuth username0 password0 <> basicAuth username1 password1)
247256 lookup "Authorization" (L.requestHeaders request) `shouldBe`
248− pure (basicAuthHeader username0 password0)
257+ Just (basicAuthHeader username0 password0)
249258 describe "oAuth2Bearer" $ do
250259 it "sets Authorization header to correct value" $
251260 property $ \token -> do
@@ -447,28 +456,47 @@ urlEncode = Y.urlEncode False . T.encodeUtf8
447456encodePathPieces :: [Text] -> ByteString
448457encodePathPieces = BL.toStrict . BB.toLazyByteString . Y.encodePathSegments
449458
450−-- | Assemble a URL.
451−
452−assembleUrl
453− :: Scheme -- ^ Scheme
454− -> Host -- ^ Host
455− -> Maybe (NonNegative Int) -- ^ Port
456− -> [Text] -- ^ Path pieces
457− -> QueryParams -- ^ Query parameters
458− -> (ByteString, ByteString, ByteString) -- ^ URL, path, query string
459−assembleUrl scheme' (Host host') mport' pathPieces (QueryParams queryParams) =
460− (scheme <> host <> port' <> path <> queryString, path, queryString)
461− where
462− scheme = case scheme' of
463− Http -> "http://"
464− Https -> "https://"
465− host = urlEncode host'
466− port' =
467− case mport' of
468− Nothing -> ""
469− Just (NonNegative x) -> ":" <> B8.pack (show x)
470− path = encodePathPieces pathPieces
471− queryString = Y.renderQuery True (Y.queryTextToQuery queryParams)
459+-- | Get host from a 'URI'. This function is not total.
460+
461+uriHost :: URI -> ByteString
462+uriHost uri = fromJust $
463+ urlEncode . URI.unRText . URI.authHost
464+ <$> either (const Nothing) Just (URI.uriAuthority uri)
465+
466+-- | Get part from a 'URI' defaulting to the provided value.
467+
468+uriPort :: Int -> URI -> Int
469+uriPort def uri = maybe def (fromIntegral) $
470+ either (const Nothing) Just (URI.uriAuthority uri) >>= URI.authPort
471+
472+-- | Get path from 'URI'.
473+
474+uriPath :: URI -> ByteString
475+uriPath uri = fromMaybe "" $ do
476+ (_, xs) <- URI.uriPath uri
477+ (return . encodePathPieces . fmap URI.unRText . NE.toList) xs
478+
479+-- | Get query string from 'URI'.
480+
481+uriQuery :: URI -> ByteString
482+uriQuery uri = do
483+ let liftQueryParam = \case
484+ URI.QueryFlag t -> (URI.unRText t, Nothing)
485+ URI.QueryParam k v -> (URI.unRText k, Just (URI.unRText v))
486+ Y.renderQuery True (Y.queryTextToQuery (liftQueryParam <$> (URI.uriQuery uri)))
487+
488+-- | Predict the headrs that should be set if the given 'URI' has username
489+-- and password in it.
490+
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)
498+
499+-- | Render a query as lazy 'BL.ByteString'.
472500
473501renderQuery :: [(Text, Maybe Text)] -> BL.ByteString
474502renderQuery = BL.fromStrict . Y.renderQuery False . Y.queryTextToQuery