{-# 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 ""