http-core-0.1.0.0: test/Properties.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Main (main) where
import Http.Core
import Http.Core.Body
import Http.Core.Extensions (emptyExtensions, insertExtension, lookupExtension, deleteExtension)
import Data.Typeable (Typeable)
import qualified Data.ByteString as BS
import Data.ByteString (ByteString)
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.Text.Encoding as TE
import Network.HTTP.Types (Query)
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
-- ===================================================================
-- Test value types
-- ===================================================================
newtype TestVal = TestVal Int
deriving (Show, Eq, Typeable)
-- ===================================================================
-- Generators
-- ===================================================================
genInt :: Gen Int
genInt = Gen.int (Range.linearFrom 0 (-10000) 10000)
genByteString :: Gen ByteString
genByteString = Gen.bytes (Range.linear 0 256)
genText :: Gen Text
genText = Gen.text (Range.linear 0 256) Gen.unicode
genMethod :: Gen Method
genMethod = Gen.element
[methodGet, methodPost, methodPut, methodDelete, methodPatch, methodHead]
genPathSegment :: Gen Text
genPathSegment = Gen.text (Range.linear 1 20) Gen.alphaNum
genPath :: Gen [Text]
genPath = Gen.list (Range.linear 0 5) genPathSegment
genQuery :: Gen Query
genQuery = Gen.list (Range.linear 0 5) $ do
k <- Gen.bytes (Range.linear 1 20)
mv <- Gen.maybe (Gen.bytes (Range.linear 0 50))
pure (k, mv)
genHeader :: Gen Header
genHeader = do
name <- Gen.element ["Content-Type", "Accept", "Authorization", "X-Custom"]
val <- Gen.bytes (Range.linear 1 50)
pure (name, val)
genHeaders :: Gen RequestHeaders
genHeaders = Gen.list (Range.linear 0 5) genHeader
-- ===================================================================
-- Properties
-- ===================================================================
-- 1. Insert a value into Extensions, lookup returns Just that value.
prop_extensionsInsertLookup :: Property
prop_extensionsInsertLookup = property $ do
n <- forAll genInt
val <- evalIO $ do
exts <- emptyExtensions
insertExtension (TestVal n) exts
lookupExtension @TestVal exts
val === Just (TestVal n)
-- 2. Insert twice with the same type, lookup returns the second value.
prop_extensionsOverwrite :: Property
prop_extensionsOverwrite = property $ do
n1 <- forAll genInt
n2 <- forAll genInt
val <- evalIO $ do
exts <- emptyExtensions
insertExtension (TestVal n1) exts
insertExtension (TestVal n2) exts
lookupExtension @TestVal exts
val === Just (TestVal n2)
-- 3. Insert then delete, lookup returns Nothing.
prop_extensionsDeleteLookup :: Property
prop_extensionsDeleteLookup = property $ do
n <- forAll genInt
val <- evalIO $ do
exts <- emptyExtensions
insertExtension (TestVal n) exts
deleteExtension @TestVal exts
lookupExtension @TestVal exts
val === (Nothing :: Maybe TestVal)
-- 4. fromBytes roundtrip: bodyToStrict (fromBytes bs) == bs
prop_bodyFromBytesRoundtrip :: Property
prop_bodyFromBytesRoundtrip = property $ do
bs <- forAll genByteString
result <- evalIO $ bodyToStrict (fromBytes bs)
result === bs
-- 5. fromText roundtrip: bodyToStrict (fromText t) == encodeUtf8 t
prop_bodyFromTextRoundtrip :: Property
prop_bodyFromTextRoundtrip = property $ do
t <- forAll genText
result <- evalIO $ bodyToStrict (fromText t)
result === TE.encodeUtf8 t
-- 6. streamFromList roundtrip: concat of chunks
prop_streamFromListRoundtrip :: Property
prop_streamFromListRoundtrip = property $ do
chunks <- forAll $ Gen.list (Range.linear 0 10) genByteString
result <- evalIO $ do
body <- streamFromList chunks
bodyToStrict body
result === BS.concat chunks
-- 7. isEmptyBody correctness
prop_isEmptyBodyCorrect :: Property
prop_isEmptyBodyCorrect = withTests 1 $ property $ do
assert (isEmptyBody emptyBody)
assert (isEmptyBody (fromBytes ""))
assert (not (isEmptyBody (fromBytes "x")))
-- 8. splitRequest preserves method, path, query, headers
prop_splitRequestPreservesFields :: Property
prop_splitRequestPreservesFields = property $ do
method <- forAll genMethod
path <- forAll genPath
query <- forAll genQuery
headers <- forAll genHeaders
body <- forAll genByteString
let pathRaw = TE.encodeUtf8 ("/" <> T.intercalate "/" path)
(parts, splitBody) <- evalIO $ do
exts <- emptyExtensions
let req = Request
{ requestMethod = method
, requestPathRaw = pathRaw
, requestPath = path
, requestQuery = query
, requestHeaders = headers
, requestBody = body
, requestExtensions = exts
}
pure (splitRequest req)
rpMethod parts === method
rpPathRaw parts === pathRaw
rpPath parts === path
rpQuery parts === query
rpHeaders parts === headers
splitBody === body
-- ===================================================================
-- Main — TemplateHaskell discovery
-- ===================================================================
tests :: IO Bool
tests = checkParallel $$(discover)
main :: IO ()
main = do
passed <- tests
if passed
then pure ()
else error "Property tests failed"