packages feed

wai-extra-3.1.13.0: test/Network/Wai/Middleware/CombineHeadersSpec.hs

{-# LANGUAGE OverloadedStrings #-}
module Network.Wai.Middleware.CombineHeadersSpec
    ( main
    , spec
    ) where

import Data.ByteString (ByteString)
import Data.IORef (newIORef, readIORef, writeIORef)
import Network.HTTP.Types (status200)
import Network.HTTP.Types.Header
import Network.Wai
import Test.Hspec

import Network.Wai.Middleware.CombineHeaders (CombineSettings, combineHeaders, defaultCombineSettings, setRequestHeaders, setResponseHeaders)
import Network.Wai.Test (SResponse (simpleHeaders), request, runSession)

main :: IO ()
main = hspec spec

spec :: Spec
spec = do
    let test name settings reqHeaders expectedReqHeaders resHeaders expectedResHeaders = it name $ do
            (reqHdrs, resHdrs) <- runApp settings reqHeaders resHeaders
            reqHdrs `shouldBe` expectedReqHeaders
            resHdrs `shouldBe` expectedResHeaders
        testReqHdrs name a b =
            test name defaultCombineSettings a b [] []
        testResHdrs name a b =
            test name (setRequestHeaders False $ setResponseHeaders True defaultCombineSettings) [] [] a b

    -- Request Headers
    testReqHdrs
        "should reorder alphabetically (request)"
        [host      , userAgent, acceptHtml]
        [acceptHtml, host     , userAgent ]
    -- Response Headers
    testResHdrs
        "should reorder alphabetically (response)"
        [expires        , location, contentTypeHtml]
        [contentTypeHtml, expires , location       ]

    -- Request Headers
    testReqHdrs
        "combines Accept (in order)"
        [userAgent, acceptHtml, host, acceptJSON]
        [acceptHtml `combineHdrs` acceptJSON, host, userAgent]
    -- Response Headers
    testResHdrs
        -- Using the default header map, Cache-Control is a "combineable" header, "Set-Cookie" is not
        "combines Cache-Control (in order) and keeps Set-Cookie (in order)"
        [ cacheControlPublic, setCookie "2", date, cacheControlMax, setCookie "1"]
        [ cacheControlPublic `combineHdrs` cacheControlMax, date, setCookie "2", setCookie "1"]

    -- Request Headers
    testReqHdrs
        "KeepOnly works as expected (present | request)"
        -- "Alt-Svc" has (KeepOnly "clear")
        [ date, altSvc "wrong", altSvc "clear", altSvc "wrong again", host ]
        [ altSvc "clear", date, host ]
    testReqHdrs
        "KeepOnly works as expected ( absent | request)"
        -- "Alt-Svc" has (KeepOnly "clear"), but will combine when there's no "clear" (AND keeps order)
        [ date, altSvc "wrong", altSvc "not clear", altSvc "wrong again", host ]
        [ altSvc "wrong, not clear, wrong again", date, host ]

    -- Response Headers
    testResHdrs
        "KeepOnly works as expected (present | response)"
        -- "If-None-Match" has (KeepOnly "*")
        [ date, ifNoneMatch "wrong", ifNoneMatch "*", ifNoneMatch "wrong again", host ]
        [ date, host, ifNoneMatch "*" ]
    testResHdrs
        "KeepOnly works as expected ( absent | response)"
        -- "If-None-Match" has (KeepOnly "*"), but will combine when there's no "*" (AND keeps order)
        [ date, ifNoneMatch "wrong", ifNoneMatch "not *", ifNoneMatch "wrong again", host ]
        [ date, host, ifNoneMatch "wrong, not *, wrong again" ]

    -- Request Headers
    testReqHdrs
        "Technically acceptable headers get combined correctly (request)"
        [ ifNoneMatch "correct, ", ifNoneMatch "something else \t", ifNoneMatch "and more , "]
        [ ifNoneMatch "correct, something else, and more" ]
    -- Response Headers
    testResHdrs
        "Technically acceptable headers get combined correctly (response)"
        [ altSvc "correct\t, ", altSvc "something else", altSvc "and more, , "]
        [ altSvc "correct, something else, and more" ]

combineHdrs :: Header -> Header -> Header
combineHdrs (hname, h1) (_, h2) = (hname, h1 <> ", " <> h2)

acceptHtml,
    acceptJSON,
    cacheControlMax,
    cacheControlPublic,
    contentTypeHtml,
    date,
    expires,
    host,
    location,
    userAgent :: Header

acceptHtml = (hAccept, "text/html")
acceptJSON = (hAccept, "application/json")
altSvc :: ByteString -> Header
altSvc x = ("Alt-Svc", x)
cacheControlPublic = (hCacheControl, "public")
cacheControlMax = (hCacheControl, "public")
contentTypeHtml = (hContentType, "text/html")
date = (hDate, "Mon, 19 Aug 2022 18:18:31 GMT")
expires = (hExpires, "Mon, 19 Sep 2022 18:18:31 GMT")
host = (hHost, "google.com")
ifNoneMatch :: ByteString -> Header
ifNoneMatch x = (hIfNoneMatch, x)
location = (hLocation, "http://www.google.com/")
setCookie :: ByteString -> Header
setCookie val = (hSetCookie, val)
userAgent = (hUserAgent, "curl/7.68.0")

runApp :: CombineSettings -> RequestHeaders -> ResponseHeaders -> IO (RequestHeaders, ResponseHeaders)
runApp settings reqHeaders resHeaders = do
    reqHdrs <- newIORef $ error "IORef not set"
    sResponse <- runSession
        session
        $ combineHeaders settings $ app reqHdrs
    finalReqHeaders <- readIORef reqHdrs
    pure (finalReqHeaders, simpleHeaders sResponse)
  where
    session =
        request
            defaultRequest { requestHeaders = reqHeaders }
    app hdrRef req respond = do
        writeIORef hdrRef $ requestHeaders req
        respond $ responseLBS status200 resHeaders ""