packages feed

wai-middleware-delegate-0.1.4.0: test/Test/HttpReply.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

module Test.HttpReply
  ( HttpReply (..)
  , HttpReplyMismatch (..)
  , compareHttpReplies
  , assertHttpRepliesAreEq
  , assertHttpRepliesDiffer
  )
where

import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C8
import Data.CaseInsensitive (original)
import Data.List (intercalate)
import Data.Maybe (catMaybes, isJust, isNothing)
import Network.HTTP.Types (Header, HeaderName)


data HttpReply = HttpReply
  { hrSecure :: !Bool
  , hrStatus :: !Int
  , hrHeaders :: ![Header]
  , hrBytes :: !BS.ByteString
  }


instance Show HttpReply where
  show r =
    intercalate "\n" $
      [ "Status: " ++ (show . hrStatus) r
      , "Body:"
      , C8.unpack $ hrBytes r
      ]
        <> (map (show . concatHeader) $ hrHeaders r)
    where
      concatHeader (f, v) = BS.concat ["  ", original f, ": ", v]


data HttpReplyMismatch
  = StatusMismatch Int Int
  | HeaderMismatch
      HeaderName
      (Maybe C8.ByteString)
      (Maybe C8.ByteString)
  | BodyMismatch BS.ByteString BS.ByteString
  | MissingViaHeader
  | UnexpectedViaHeader
  deriving (Eq)


instance Show HttpReplyMismatch where
  show (StatusMismatch x y) = "HTTP status codes don't match : " ++ show x ++ " /= " ++ show y
  show (HeaderMismatch name x y) = "Header field '" ++ show name ++ "' doesn't match : '" ++ show x ++ "' /= '" ++ show y
  show (BodyMismatch x y) = "HTTP response bodies are different :\n" ++ C8.unpack x ++ "\n-----------\n" ++ C8.unpack y
  show MissingViaHeader = "Error: Proxy connection should contain 'X-Via-Proxy' header."
  show UnexpectedViaHeader = "Error: Direct connection should not contain 'X-Via-Proxy' header."


assertHttpRepliesAreEq :: HttpReply -> HttpReply -> IO ()
assertHttpRepliesAreEq direct proxied = do
  let assertNoMismatches [] = return ()
      assertNoMismatches xs = error $ intercalate "\n" $ map show xs
  assertNoMismatches $ compareHttpReplies direct proxied


assertHttpRepliesDiffer :: HttpReply -> HttpReply -> IO ()
assertHttpRepliesDiffer direct proxied = do
  let assertHasMismatches [] = error "Responses should be different!"
      assertHasMismatches _ = return ()
  assertHasMismatches $ compareHttpReplies direct proxied


compareHttpReplies :: HttpReply -> HttpReply -> [HttpReplyMismatch]
compareHttpReplies direct proxied = catMaybes mbMismatches
  where
    mbMismatches =
      [ compare' hrStatus StatusMismatch
      , compareBodys (hrBytes direct) (hrBytes proxied)
      , missingViaHeader
      , unexpectedViaHeader
      , mismatchedHeader "server"
      , mismatchedHeader "content-type"
      , mismatchedHeader "content-length"
      ]
    wantSecure = hrSecure direct
    maybeHeader n r = lookup n $ hrHeaders r
    compare' f g
      | f direct == f proxied = Nothing
      | otherwise = Just $ g (f direct) (f proxied)
    missingViaHeader
      | not wantSecure && (isJust $ maybeHeader "X-Via-Proxy" direct) = Just UnexpectedViaHeader
      | otherwise = Nothing
    unexpectedViaHeader
      | not wantSecure && (isNothing $ maybeHeader "X-Via-Proxy" proxied) = Just MissingViaHeader
      | otherwise = Nothing
    mismatchedHeader name
      | x' /= y' = Just $ HeaderMismatch name x' y'
      | otherwise = Nothing
      where
        x' = maybeHeader name direct
        y' = maybeHeader name proxied


compareBodys :: ByteString -> ByteString -> Maybe HttpReplyMismatch
compareBodys direct proxied =
  let direct' = replaceAmznTraceId direct
      proxied' = replaceAmznTraceId proxied
      compare' a b
        | a == b = Nothing
        | otherwise = Just $ BodyMismatch a b
   in compare' direct' proxied'


replaceAmznTraceId :: ByteString -> ByteString
replaceAmznTraceId x =
  let asLines = C8.lines x
      checkTrace y = "X-Amzn-Trace-Id" `C8.isInfixOf` y
      dropTrace y = if checkTrace y then Nothing else Just y
   in C8.unlines $ catMaybes $ dropTrace <$> asLines