packages feed

restman-0.7.3.1: test/HTTP/ClientIntegrationSpec.hs

{-# language OverloadedStrings #-}
module HTTP.ClientIntegrationSpec (tests) where

import Data.Aeson (FromJSON(..), eitherDecode, withObject, (.:))
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BL
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Text as T

import Test.Tasty
import Test.Tasty.HUnit

import qualified Network.HTTP.Client as HC

import Network.Wai.Handler.Warp (Port)

import HTTP.Client (Header, knownMethods, robustSettings)
import HTTP.EchoServer (withEchoServer)

-- ---------------------------------------------------------------------------
-- EchoResponse — mirrors the JSON produced by HTTP.EchoServer
-- ---------------------------------------------------------------------------

data EchoResponse = EchoResponse
    { echoMethod :: T.Text
    , echoPath :: T.Text
    , echoQuery :: T.Text
    , echoHeaders :: Map T.Text T.Text
    , echoBody :: T.Text
    } deriving (Eq, Show)

instance FromJSON EchoResponse where
    parseJSON = withObject "EchoResponse" $ \o ->
        EchoResponse
            <$> o .: "method"
            <*> o .: "path"
            <*> o .: "query"
            <*> o .: "headers"
            <*> o .: "body"

-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------

echoURL :: Port -> String -> String
echoURL port path = "http://localhost:" <> show port <> path

-- | Build and send an HTTP request using the restman 'robustSettings' manager,
-- then decode the echo server's JSON response.
sendRequest
    :: HC.Manager
    -> String        -- ^ Absolute URL
    -> String        -- ^ HTTP method string
    -> [Header]      -- ^ Extra request headers
    -> BL.ByteString -- ^ Request body (may be empty)
    -> IO EchoResponse
sendRequest mgr url method extraHdrs body = do
    req <- HC.parseRequest url
    let req' = req
            { HC.method         = BS8.pack method
            , HC.requestBody    = HC.RequestBodyLBS body
            , HC.requestHeaders = extraHdrs <> HC.requestHeaders req
            }
    resp <- HC.httpLbs req' mgr
    case eitherDecode (HC.responseBody resp) of
        Left err -> assertFailure ("Failed to decode echo response: " <> err)
        Right er -> pure er

-- ---------------------------------------------------------------------------
-- Tests
-- ---------------------------------------------------------------------------

tests :: TestTree
tests = testGroup "HTTP.Client (integration)"
    [ methodTests
    , headerTests
    , bodyTests
    ]

-- CONNECT requires proxy tunnelling semantics; HEAD strips the response body.
-- Both are excluded from the generic reflection test.
testableMethods :: [String]
testableMethods = filter (`notElem` ["CONNECT", "HEAD"]) knownMethods

methodTests :: TestTree
methodTests = testGroup "method reflection"
    [ testCase ("reflects " <> m) $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/") m [] ""
        echoMethod er @?= T.pack m
    | m <- testableMethods
    ]

headerTests :: TestTree
headerTests = testGroup "header reflection"
    [ testCase "single custom header is echoed" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/") "GET"
                   [("X-Restman-Test", "hello")] ""
        Map.lookup "x-restman-test" (echoHeaders er) @?= Just "hello"

    , testCase "multiple custom headers are all echoed" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/") "GET"
                   [("X-Foo", "foo-value"), ("X-Bar", "bar-value")] ""
        Map.lookup "x-foo" (echoHeaders er) @?= Just "foo-value"
        Map.lookup "x-bar" (echoHeaders er) @?= Just "bar-value"

    , testCase "header value is preserved verbatim" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/") "GET"
                   [("X-Custom", "value with spaces")] ""
        Map.lookup "x-custom" (echoHeaders er) @?= Just "value with spaces"
    ]

bodyTests :: TestTree
bodyTests = testGroup "body and path reflection"
    [ testCase "POST body is echoed" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/echo") "POST" [] "hello world"
        echoBody er @?= "hello world"

    , testCase "PUT body is echoed" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/data") "PUT" [] "{\"key\":\"value\"}"
        echoBody er @?= "{\"key\":\"value\"}"

    , testCase "PATCH body is echoed" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/patch") "PATCH" [] "patch-body"
        echoBody er @?= "patch-body"

    , testCase "path is reflected correctly" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/some/path") "GET" [] ""
        echoPath er @?= "/some/path"

    , testCase "query string is reflected correctly" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/search?q=restman&limit=10") "GET" [] ""
        echoQuery er @?= "?q=restman&limit=10"

    , testCase "empty body is reflected as empty text" $ withEchoServer $ \port -> do
        mgr <- HC.newManager robustSettings
        er  <- sendRequest mgr (echoURL port "/") "GET" [] ""
        echoBody er @?= ""
    ]