@@ -11,21 +11,22 @@ import Control.Exception
11import Control.Monad (forM_)
11import Control.Monad (forM_)
12import Control.Monad.Trans.Control
12import Control.Monad.Trans.Control
13import Data.Aeson (ToJSON (..), Value (..), object, (.=))
13import Data.Aeson (ToJSON (..), Value (..), object, (.=))
14−import qualified Data.Aeson as A
14+import Data.Aeson qualified as A
15−import qualified Data.Aeson.KeyMap as Aeson.KeyMap
15+import Data.Aeson.KeyMap qualified as Aeson.KeyMap
16−import qualified Data.ByteString as B
16+import Data.ByteString qualified as B
17−import qualified Data.ByteString.Lazy as BL
17+import Data.ByteString.Lazy qualified as BL
18import Data.Functor.Identity (runIdentity)
18import Data.Functor.Identity (runIdentity)
19import Data.Maybe (fromJust)
19import Data.Maybe (fromJust)
20import Data.Proxy
20import Data.Proxy
21import Data.Text (Text)
21import Data.Text (Text)
22−import qualified Data.Text as T
22+import Data.Text qualified as T
23−import qualified Data.Text.Encoding as T
23+import Data.Text.Encoding qualified as T
24−import qualified Data.Text.IO as TIO
24+import Data.Text.IO qualified as TIO
25−import qualified Network.HTTP.Client as L
25+import Network.HTTP.Client qualified as L
26−import qualified Network.HTTP.Client.MultipartFormData as LM
26+import Network.HTTP.Client.MultipartFormData qualified as LM
27−import Network.HTTP.Req
27+import Network.HTTP.Req hiding (req)
28+import Network.HTTP.Req qualified as Req
28−import qualified Network.HTTP.Types as Y
29+import Network.HTTP.Types qualified as Y
29import Test.Hspec
30import Test.Hspec
30import Test.QuickCheck
31import Test.QuickCheck
@@ -33,25 +34,25 @@ spec :: Spec
34 describe "exception throwing on non-2xx status codes" $
35 describe "exception throwing on non-2xx status codes" $
35 it "throws indeed for non-2xx" $
36 it "throws indeed for non-2xx" $
36− req GET (httpbun /: "foo") NoReqBody ignoreResponse mempty
37+ req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty
37 `shouldThrow` selector404
38 `shouldThrow` selector404
39 describe "exception throwing on non-2xx status codes (Req monad)" $
40 describe "exception throwing on non-2xx status codes (Req monad)" $
40 it "throws indeed for non-2xx" $
41 it "throws indeed for non-2xx" $
41 asIO . runReq defaultHttpConfig $
42 asIO . runReq defaultHttpConfig $
42 liftBaseWith $ \run ->
43 liftBaseWith $ \run ->
43− run (req GET (httpbun /: "foo") NoReqBody ignoreResponse mempty)
44+ run (req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty)
44 `shouldThrow` selector404
45 `shouldThrow` selector404
46 describe "response check via httpConfigCheckResponse" $
47 describe "response check via httpConfigCheckResponse" $
47 context "if it's set to always throw" $
48 context "if it's set to always throw" $
48 it "throws indeed" $
49 it "throws indeed" $
49− blindlyThrowing (req GET httpbun NoReqBody ignoreResponse mempty)
50+ blindlyThrowing (req GET httpbin NoReqBody ignoreResponse mempty)
50 `shouldThrow` anyException
51 `shouldThrow` anyException
52 describe "isStatusCodeException" $
53 describe "isStatusCodeException" $
53 it "extracts non-2xx response" $
54 it "extracts non-2xx response" $
54− req GET (httpbun /: "foo") NoReqBody ignoreResponse mempty
55+ req GET (httpbin /: "foo") NoReqBody ignoreResponse mempty
55 `shouldThrow` selector404ByStatusCodeException
56 `shouldThrow` selector404ByStatusCodeException
57 describe "receiving user-agent header back" $
58 describe "receiving user-agent header back" $
@@ -59,7 +60,7 @@ spec = do
62− (httpbun /: "user-agent")
63+ (httpbin /: "user-agent")
64 jsonResponse
65 jsonResponse
65 (header "user-agent" "Req")
66 (header "user-agent" "Req")
@@ -74,7 +75,7 @@ spec = do
77− (httpbun /: "headers")
78+ (httpbin /: "headers")
79 jsonResponse
80 jsonResponse
80 (header "Foo" "bar" <> header "Baz" "quux")
81 (header "Foo" "bar" <> header "Baz" "quux")
@@ -92,12 +93,11 @@ spec = do
93 describe "receiving GET data back" $
94 describe "receiving GET data back" $
94 it "works" $ do
95 it "works" $ do
95− r <- req GET (httpbun /: "get") NoReqBody jsonResponse mempty
96+ r <- req GET (httpbin /: "get") NoReqBody jsonResponse mempty
96 (stripFunnyHeaders . stripOrigin) (responseBody r)
97 (stripFunnyHeaders . stripOrigin) (responseBody r)
97 `shouldBe` object
98 `shouldBe` object
98 [ "args" .= emptyObject,
99 [ "args" .= emptyObject,
99− "url" .= ("https://httpbun.org/get" :: Text),
100+ "url" .= ("http://localhost:1234/get" :: Text),
100− "method" .= ("GET" :: Text),
101 "headers"
101 "headers"
102 .= object
102 .= object
103 [ "Accept-Encoding" .= ("gzip" :: Text)
103 [ "Accept-Encoding" .= ("gzip" :: Text)
@@ -111,14 +111,13 @@ spec = do
111 it "works" $ do
111 it "works" $ do
112 let text = "foo" :: Text
112 let text = "foo" :: Text
113 reflected = reflectJSON text
113 reflected = reflectJSON text
114− r <- req POST (httpbun /: "post") (ReqBodyJson text) jsonResponse mempty
114+ r <- req POST (httpbin /: "post") (ReqBodyJson text) jsonResponse mempty
115 (stripFunnyHeaders . stripOrigin) (responseBody r)
115 (stripFunnyHeaders . stripOrigin) (responseBody r)
116 `shouldBe` object
116 `shouldBe` object
117 [ "args" .= emptyObject,
117 [ "args" .= emptyObject,
118 "json" .= text,
118 "json" .= text,
119 "data" .= reflected,
119 "data" .= reflected,
120− "url" .= ("https://httpbun.org/post" :: Text),
120+ "url" .= ("http://localhost:1234/post" :: Text),
121− "method" .= ("POST" :: Text),
122 "headers"
121 "headers"
123 .= object
122 .= object
124 [ "Content-Type" .= ("application/json; charset=utf-8" :: Text),
123 [ "Content-Type" .= ("application/json; charset=utf-8" :: Text),
@@ -139,15 +138,14 @@ spec = do
139 [ LM.partBS "foo" "foo data!",
138 [ LM.partBS "foo" "foo data!",
140 LM.partBS "bar" "bar data!"
139 LM.partBS "bar" "bar data!"
142− r <- req POST (httpbun /: "post") body jsonResponse mempty
141+ r <- req POST (httpbin /: "post") body jsonResponse mempty
143 let contentType = fromJust (getRequestContentType body)
142 let contentType = fromJust (getRequestContentType body)
144 (stripFunnyHeaders . stripOrigin) (responseBody r)
143 (stripFunnyHeaders . stripOrigin) (responseBody r)
145 `shouldBe` object
144 `shouldBe` object
146 [ "args" .= emptyObject,
145 [ "args" .= emptyObject,
147 "json" .= Null,
146 "json" .= Null,
148 "data" .= ("" :: Text),
147 "data" .= ("" :: Text),
149− "url" .= ("https://httpbun.org/post" :: Text),
148+ "url" .= ("http://localhost:1234/post" :: Text),
150− "method" .= ("POST" :: Text),
151 "headers"
149 "headers"
152 .= object
150 .= object
153 [ "Content-Type" .= T.decodeUtf8 contentType,
151 [ "Content-Type" .= T.decodeUtf8 contentType,
@@ -168,16 +166,15 @@ spec = do
168 describe "receiving PATCHed file back" $
166 describe "receiving PATCHed file back" $
169 it "works" $ do
167 it "works" $ do
170 let file :: FilePath
168 let file :: FilePath
171− file = "httpbun-data/robots.txt"
169+ file = "httpbin-data/robots.txt"
172 contents <- TIO.readFile file
170 contents <- TIO.readFile file
173− r <- req PATCH (httpbun /: "patch") (ReqBodyFile file) jsonResponse mempty
171+ r <- req PATCH (httpbin /: "patch") (ReqBodyFile file) jsonResponse mempty
174 (stripFunnyHeaders . stripOrigin) (responseBody r)
172 (stripFunnyHeaders . stripOrigin) (responseBody r)
175 `shouldBe` object
173 `shouldBe` object
176 [ "args" .= emptyObject,
174 [ "args" .= emptyObject,
177 "json" .= Null,
175 "json" .= Null,
178 "data" .= contents,
176 "data" .= contents,
179− "url" .= ("https://httpbun.org/patch" :: Text),
177+ "url" .= ("http://localhost:1234/patch" :: Text),
180− "method" .= ("PATCH" :: Text),
181 "headers"
178 "headers"
182 .= object
179 .= object
183 [ "Accept-Encoding" .= ("gzip" :: Text),
180 [ "Accept-Encoding" .= ("gzip" :: Text),
@@ -196,14 +193,13 @@ spec = do
196 "foo" =: ("bar" :: Text)
193 "foo" =: ("bar" :: Text)
197 <> "baz" =: (5 :: Int)
194 <> "baz" =: (5 :: Int)
198 <> queryFlag "quux"
195 <> queryFlag "quux"
199− r <- req PUT (httpbun /: "put") (ReqBodyUrlEnc params) jsonResponse mempty
196+ r <- req PUT (httpbin /: "put") (ReqBodyUrlEnc params) jsonResponse mempty
200 (stripFunnyHeaders . stripOrigin) (responseBody r)
197 (stripFunnyHeaders . stripOrigin) (responseBody r)
201 `shouldBe` object
198 `shouldBe` object
202 [ "args" .= emptyObject,
199 [ "args" .= emptyObject,
203 "json" .= Null,
200 "json" .= Null,
204 "data" .= ("" :: Text),
201 "data" .= ("" :: Text),
205− "url" .= ("https://httpbun.org/put" :: Text),
202+ "url" .= ("http://localhost:1234/put" :: Text),
206− "method" .= ("PUT" :: Text),
207 "headers"
203 "headers"
208 .= object
204 .= object
209 [ "Content-Type" .= ("application/x-www-form-urlencoded" :: Text),
205 [ "Content-Type" .= ("application/x-www-form-urlencoded" :: Text),
@@ -222,19 +218,24 @@ spec = do
222 responseStatusCode r `shouldBe` 200
218 responseStatusCode r `shouldBe` 200
223 responseStatusMessage r `shouldBe` "OK"
219 responseStatusMessage r `shouldBe` "OK"
225− -- describe "receiving UTF-8 encoded Unicode data" $
223+ describe "receiving UTF-8 encoded Unicode data" $
226− -- it "works" $ do
224+ it "works" $ do
230− -- (httpbun /: "encoding" /: "utf8")
228+ (httpbin /: "encoding" /: "utf8")
231− -- NoReqBody
229+ NoReqBody
232− -- bsResponse
230+ bsResponse
233− -- mempty
231+ mempty
234− -- utf8data <- B.readFile "httpbun-data/utf8.html"
232+ utf8data <- B.readFile "httpbin-data/utf8.html"
235− -- responseBody r `shouldBe` utf8data
233+ responseBody r `shouldBe` utf8data
236− -- responseStatusCode r `shouldBe` 200
234+ responseStatusCode r `shouldBe` 200
237− -- responseStatusMessage r `shouldBe` "OK"
235+ responseStatusMessage r `shouldBe` "OK"
239 describe "retrying" $
240 describe "retrying" $
240 it "retries as many times as specified" $ do
241 it "retries as many times as specified" $ do
@@ -245,7 +246,7 @@ spec = do
245 prepareForShit $
246 prepareForShit $
248− (httpbun /: "status" /~ status)
249+ (httpbin /: "status" /~ status)
249 NoReqBody
250 NoReqBody
250 ignoreResponse
251 ignoreResponse
@@ -257,18 +258,25 @@ spec = do
257 forM_ [400 .. 431] checkStatusCode
258 forM_ [400 .. 431] checkStatusCode
258 forM_ [500 .. 511] checkStatusCode
259 forM_ [500 .. 511] checkStatusCode
261+ -- TODO /response-headers
260 describe "redirects" $
264 describe "redirects" $
261 it "follows redirects" $ do
265 it "follows redirects" $ do
265− (httpbun /: "redirect-to")
269+ (httpbin /: "redirect-to")
266 NoReqBody
270 NoReqBody
267 ignoreResponse
271 ignoreResponse
268− ("url" =: ("https://httpbun.org" :: Text))
272+ ("url" =: ("https://httpbin.org" :: Text))
269 responseStatusCode r `shouldBe` 200
273 responseStatusCode r `shouldBe` 200
270 responseStatusMessage r `shouldBe` "OK"
274 responseStatusMessage r `shouldBe` "OK"
276+ -- TODO /relative-redicet
277+ -- TODO /absolute-redirect
272 describe "basicAuth" $ do
280 describe "basicAuth" $ do
273 let user, password :: Text
281 let user, password :: Text
274 user = "Scooby"
282 user = "Scooby"
@@ -279,32 +287,43 @@ spec = do
279 prepareForShit $
287 prepareForShit $
282− (httpbun /: "basic-auth" /~ user /~ password)
290+ (httpbin /: "basic-auth" /~ user /~ password)
283 NoReqBody
291 NoReqBody
284 ignoreResponse
292 ignoreResponse
286 responseStatusCode r `shouldBe` 401
294 responseStatusCode r `shouldBe` 401
287− responseStatusMessage r `shouldBe` "Unauthorized"
295+ responseStatusMessage r `shouldBe` "UNAUTHORIZED"
288 context "when we provide appropriate basic auth data" $
296 context "when we provide appropriate basic auth data" $
289 it "succeeds" $ do
297 it "succeeds" $ do
293− (httpbun /: "basic-auth" /~ user /~ password)
301+ (httpbin /: "basic-auth" /~ user /~ password)
294 NoReqBody
302 NoReqBody
295 ignoreResponse
303 ignoreResponse
296− (basicAuth (T.encodeUtf8 user) (T.encodeUtf8 password))
304+ (basicAuthUnsafe (T.encodeUtf8 user) (T.encodeUtf8 password))
297 responseStatusCode r `shouldBe` 200
305 responseStatusCode r `shouldBe` 200
298 responseStatusMessage r `shouldBe` "OK"
306 responseStatusMessage r `shouldBe` "OK"
308+ -- TODO /hidden-basic-auth
309+ -- TODO /digest-auth
300 describe "robots.txt" $
316 describe "robots.txt" $
301 it "works" $ do
317 it "works" $ do
302− r <- req GET (httpbun /: "robots.txt") NoReqBody bsResponse mempty
318+ r <- req GET (httpbin /: "robots.txt") NoReqBody bsResponse mempty
303− robots <- B.readFile "httpbun-data/robots.txt"
319+ robots <- B.readFile "httpbin-data/robots.txt"
304 responseBody r `shouldBe` robots
320 responseBody r `shouldBe` robots
305 responseStatusCode r `shouldBe` 200
321 responseStatusCode r `shouldBe` 200
306 responseStatusMessage r `shouldBe` "OK"
322 responseStatusMessage r `shouldBe` "OK"
308 describe "getting random bytes" $ do
327 describe "getting random bytes" $ do
309 it "works" $
328 it "works" $
310 property $ \n' -> do
329 property $ \n' -> do
@@ -313,7 +332,7 @@ spec = do
316− (httpbun /: "bytes" /~ n)
335+ (httpbin /: "bytes" /~ n)
317 NoReqBody
336 NoReqBody
318 lbsResponse
337 lbsResponse
@@ -329,27 +348,36 @@ spec = do
332− (httpbun /: "bytes" /~ n)
351+ (httpbin /: "bytes" /~ n)
333 NoReqBody
352 NoReqBody
334 (Proxy :: Proxy (JsonResponse Value))
353 (Proxy :: Proxy (JsonResponse Value))
336 `shouldThrow` selector
355 `shouldThrow` selector
338−-- describe "streaming random bytes" $
357+ describe "streaming random bytes" $
339−-- it "works" $
358+ it "works" $
340−-- property $ \n' -> do
359+ property $ \n' -> do
341−-- let n :: Word
360+ let n :: Word
342−-- n = getSmall n'
361+ n = getSmall n'
346−-- (httpbun /: "stream-bytes" /~ n)
365+ (httpbin /: "stream-bytes" /~ n)
347−-- NoReqBody
366+ NoReqBody
348−-- bsResponse
367+ bsResponse
350−-- responseBody r `shouldSatisfy` ((== n) . fromIntegral . B.length)
369+ responseBody r `shouldSatisfy` ((== n) . fromIntegral . B.length)
351−-- responseStatusCode r `shouldBe` 200
370+ responseStatusCode r `shouldBe` 200
352−-- responseStatusMessage r `shouldBe` "OK"
371+ responseStatusMessage r `shouldBe` "OK"
354----------------------------------------------------------------------------
382----------------------------------------------------------------------------
355-- Instances
383-- Instances
@@ -372,9 +400,29 @@ blindlyThrowing = runReq defaultHttpConfig {httpConfigCheckResponse = doit}
373 doit _ _ = error "Oops!"
401 doit _ _ = error "Oops!"
375−-- | 'Url' representing <https://httpbun.org>.
403+-- | 'Url' representing <https://httpbin.org>.
376−httpbun :: Url 'Https
404+httpbin :: Url 'Http
377−httpbun = https "httpbun.org"
405+httpbin = http "localhost"
411+ HttpResponse response,
412+ HttpBodyAllowed (AllowsBody method) (ProvidesBody body)
420+req method url body responseProxy options =
421+ Req.req method url body responseProxy (options <> defaultOptions)
423+-- | Options to apply by default.
424+defaultOptions :: Option scheme
425+defaultOptions = port 1234
379-- | Remove “origin” field from JSON value. Origin may change, we don't want
427-- | Remove “origin” field from JSON value. Origin may change, we don't want
380-- to depend on that.
428-- to depend on that.
@@ -384,21 +432,38 @@ stripOrigin value = value
385-- | Remove funny headers that might break the tests.
433-- | Remove funny headers that might break the tests.
386stripFunnyHeaders :: Value -> Value
434stripFunnyHeaders :: Value -> Value
387−stripFunnyHeaders (Object m) =
435+stripFunnyHeaders = \case
439+ ( Aeson.KeyMap.alterF
440+ (pure . fmap stripFunnyHeaders')
447+-- | Similar to 'stripFunnyHeaders', but acts directly on the argument
448+-- without trying to access its "headers" field.
449+stripFunnyHeaders' :: Value -> Value
450+stripFunnyHeaders' = \case
388− let f (Object p) = Object $ Aeson.KeyMap.filterWithKey (\k _ -> k `elem` hs) p
453+ Aeson.KeyMap.filterWithKey
454+ (\k _ -> k `elem` whitelistedHeaders)
389− f value = value
456+ value -> value
458+ whitelistedHeaders =
391− [ "Content-Type",
459+ [ "Content-Type",
392− "Accept-Encoding",
460+ "Accept-Encoding",
394− "Content-Length",
461+ "Content-Length",
398− in Object (runIdentity (Aeson.KeyMap.alterF (pure . fmap f) "headers" m))
465+
399−stripFunnyHeaders value = value
401−-- | This is a complete test case that makes use of <https://httpbun.org> to
466+-- | This is a complete test case that makes use of <https://httpbin.org> to
402-- get various response status codes.
467-- get various response status codes.
403checkStatusCode :: Int -> SpecWith ()
468checkStatusCode :: Int -> SpecWith ()
404checkStatusCode code =
469checkStatusCode code =
@@ -408,7 +473,7 @@ checkStatusCode code =
408 prepareForShit $
473 prepareForShit $
411− (httpbun /: "status" /~ code)
476+ (httpbin /: "status" /~ code)
412 NoReqBody
477 NoReqBody
413 ignoreResponse
478 ignoreResponse