spire-server-0.1.0.0: test/Properties.hs
{-# LANGUAGE TemplateHaskell #-}
module Main where
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Control.Exception (evaluate, try, SomeException)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.CaseInsensitive as CI
import Network.HTTP.Types
( Status, ResponseHeaders, Header
, status200, status201, status204, status400, status404, status500
, statusCode
)
import Spire.Server.Parse (parseRequestHead, RequestHead(..))
import Spire.Server.Render (renderFull)
-- ===================================================================
-- Generators
-- ===================================================================
genStatus :: Gen Status
genStatus = Gen.element [status200, status201, status204, status400, status404, status500]
genHeaderName :: Gen ByteString
genHeaderName = Gen.element
[ "X-Custom", "X-Request-Id", "X-Trace", "Cache-Control", "Accept" ]
genHeaderValue :: Gen ByteString
genHeaderValue = Gen.bytes (Range.linear 1 50)
genHeader :: Gen Header
genHeader = do
name <- genHeaderName
value <- genHeaderValue
pure (CI.mk name, value)
genHeaders :: Gen ResponseHeaders
genHeaders = Gen.list (Range.linear 0 5) genHeader
genBody :: Gen ByteString
genBody = Gen.bytes (Range.linear 0 10000)
-- ===================================================================
-- Properties
-- ===================================================================
-- | renderFull produces valid HTTP: starts with "HTTP/1.1 ", contains the
-- status code, contains "Content-Length: N" matching body length, and ends
-- with the body bytes.
prop_renderFullValidHttp :: Property
prop_renderFullValidHttp = property $ do
st <- forAll genStatus
hdrs <- forAll genHeaders
body <- forAll genBody
let rendered = renderFull st hdrs body
renderedStr = BS8.unpack rendered
codeStr = show (statusCode st)
clHeader = "Content-Length: " ++ show (BS.length body)
-- Starts with HTTP/1.1
assert $ BS.isPrefixOf "HTTP/1.1 " rendered
-- Contains status code
assert $ clHeader `elem` lines (filter (/= '\r') renderedStr)
|| BS8.pack clHeader `BS.isInfixOf` rendered
-- Contains status code in status line
assert $ BS8.pack codeStr `BS.isInfixOf` rendered
-- Ends with the body bytes
assert $ BS.isSuffixOf body rendered
-- | The Content-Length header value in the rendered output matches the
-- body length exactly.
prop_contentLengthAccuracy :: Property
prop_contentLengthAccuracy = property $ do
st <- forAll genStatus
hdrs <- forAll genHeaders
body <- forAll genBody
let rendered = renderFull st hdrs body
-- Split on lines to find Content-Length header
linesList = BS8.lines rendered
clLines = filter (\l -> BS.isPrefixOf "Content-Length: " l) linesList
-- There should be exactly one Content-Length header
assert $ not (null clLines)
let clLine = case clLines of { (x:_) -> x; [] -> error "unreachable" }
-- Extract the value after "Content-Length: ", stripping trailing \r
valBS = BS.drop (BS.length "Content-Length: ") clLine
valStr = BS8.unpack (stripCR valBS)
read valStr === BS.length body
where
stripCR bs
| BS.null bs = bs
| BS.last bs == 0x0d = BS.init bs -- strip \r
| otherwise = bs
-- | renderFull is deterministic: same inputs always produce same output.
prop_renderFullDeterministic :: Property
prop_renderFullDeterministic = property $ do
st <- forAll genStatus
hdrs <- forAll genHeaders
body <- forAll genBody
renderFull st hdrs body === renderFull st hdrs body
-- ===================================================================
-- Fuzz: HTTP request parser never crashes on arbitrary input
-- ===================================================================
-- | Feed random bytes to parseRequestHead and assert it returns
-- Maybe (never throws an uncaught exception).
prop_httpParserDoesNotCrash :: Property
prop_httpParserDoesNotCrash = property $ do
bs <- forAll $ Gen.bytes (Range.linear 0 10000)
result <- evalIO $ try @SomeException $ case parseRequestHead bs of
Nothing -> evaluate ()
Just (rh, rest) ->
evaluate (BS.length (rhMethod rh)
`seq` BS.length (rhPath rh)
`seq` BS.length (rhVersion rh)
`seq` length (rhHeaders rh)
`seq` BS.length rest
`seq` ())
case result of
Left _ -> success -- exception is acceptable (not a crash)
Right _ -> success
tests :: IO Bool
tests = checkParallel $$(discover)
main :: IO ()
main = do
ok <- tests
if ok then pure () else error "Property tests failed"