packages feed

snap-server-0.3.0: test/suite/Snap/Internal/Http/Parser/Tests.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Snap.Internal.Http.Parser.Tests
  ( tests ) where

import qualified Control.Exception as E
import           Control.Exception hiding (try, assert)
import           Control.Monad
import           Control.Parallel.Strategies
import           Data.Attoparsec hiding (Result(..))
import           Data.ByteString (ByteString)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import           Data.ByteString.Internal (c2w)
import           Data.List
import qualified Data.Map as Map
import           Data.Maybe (isNothing)
import           Data.Monoid
import           Test.Framework
import           Test.Framework.Providers.HUnit
import           Test.Framework.Providers.QuickCheck2
import           Test.QuickCheck
import qualified Test.QuickCheck.Monadic as QC
import           Test.QuickCheck.Monadic hiding (run, assert)
import           Test.HUnit hiding (Test, path)
import           Text.Printf

import           Snap.Internal.Http.Parser
import           Snap.Internal.Http.Types
import           Snap.Internal.Debug
import           Snap.Iteratee hiding (map, sequence)
import qualified Snap.Iteratee as I
import           Snap.Test.Common()


tests :: [Test]
tests = [ testShow
        , testCookie
        , testChunked
        , testBothChunked
        , testBothChunkedPipelined
        , testBothChunkedEmpty
        , testP2I
        , testNull
        , testPartial
        , testParseError
        , testFormEncoded ]


emptyParser :: Parser ByteString
emptyParser = option "foo" $ string "bar"

testShow :: Test
testShow = testCase "parser/show" $ do
    let i = IRequest GET "/" (1,1) []
    let !b = show i `using` rdeepseq
    return $ b `seq` ()


testP2I :: Test
testP2I = testCase "parser/iterParser" $ do
    i <- liftM (enumBS "z") $ runIteratee (iterParser emptyParser)
    l <- run_ i

    assertEqual "should be foo" "foo" l


forceErr :: SomeException -> IO ()
forceErr e = f `seq` (return ())
  where
    !f = show e


testNull :: Test
testNull = testCase "parser/shortParse" $ do
    f <- run_ (parseRequest)
    assertBool "should be Nothing" $ isNothing f


testPartial :: Test
testPartial = testCase "parser/partial" $ do
    i <- liftM (enumBS "GET / ") $ runIteratee parseRequest
    f <- E.try $ run_ i

    case f of (Left e)  -> forceErr e
              (Right x) -> assertFailure $ "expected exception, got " ++ show x


testParseError :: Test
testParseError = testCase "parser/error" $ do
    step <- runIteratee parseRequest
    let i = enumBS "ZZZZZZZZZZ" step
    f <- E.try $ run_ i

    case f of (Left e)  -> forceErr e
              (Right x) -> assertFailure $ "expected exception, got " ++ show x


-- | convert a bytestring to chunked transfer encoding
transferEncodingChunked :: L.ByteString -> L.ByteString
transferEncodingChunked = f . L.toChunks
  where
    toChunk s = L.concat [ len, "\r\n", L.fromChunks [s], "\r\n" ]
      where
        len = L.pack $ map c2w $ printf "%x" $ S.length s

    f l = L.concat $ (map toChunk l ++ ["0\r\n\r\n"])


-- | ensure that running the 'readChunkedTransferEncoding' iteratee against
-- 'transferEncodingChunked' returns the original string
testChunked :: Test
testChunked = testProperty "parser/chunkedTransferEncoding" $
              monadicIO $ forAllM arbitrary prop_chunked
  where
    prop_chunked s = do
        QC.run $ debug "=============================="
        QC.run $ debug $ "input is " ++ show s
        QC.run $ debug $ "chunked is " ++ show chunked
        QC.run $ debug "------------------------------"
        sstep <- QC.run $ runIteratee $ stream2stream
        step  <- QC.run $ runIteratee $
                 joinI $ readChunkedTransferEncoding sstep

        out   <- QC.run $ run_ $ enum step

        QC.assert $ s == out
        QC.run $ debug "==============================\n"

      where
        chunked = (transferEncodingChunked s)
        enum = enumLBS chunked


testBothChunked :: Test
testBothChunked = testProperty "parser/invertChunked" $
                  monadicIO $ forAllM arbitrary prop
  where
    prop s = do
        sstep <- QC.run $ runIteratee stream2stream
        let it = joinI $ writeChunkedTransferEncoding sstep

        bs <- QC.run $ runIteratee it >>= run_ . enumLBS s

        let enum = enumLBS bs

        x <- QC.run $
             runIteratee (joinI $ readChunkedTransferEncoding sstep) >>=
             run_ . enum

        QC.assert $ s == x



testBothChunkedPipelined :: Test
testBothChunkedPipelined = testProperty "parser/testBothChunkedPipelined" $
                           monadicIO prop
  where
    prop = do
        sz     <- QC.pick (choose (1000,4000))
        s'     <- QC.pick $ resize sz arbitrary
        ntimes <- QC.pick (choose (4,7))
        --let s' = L.take 2000 $ L.fromChunks $ repeat s

        let e = enumLBS s'
        let n = fromEnum $ L.length s'

        let enum = foldl' (>==>) (enumBS "") (replicate ntimes e)

        bufi <- QC.run $
                unsafeBufferIteratee copyingStream2Stream >>= runIteratee

        iter' <- QC.run $ runIteratee $ joinI $
                 writeChunkedTransferEncoding bufi
        let iter = I.joinI $ I.take n iter'

        let iters = replicate ntimes iter
        let mothra = foldM (\s it -> it >>= \t -> return $ s `mappend` t)
                           mempty
                           iters

        bs <- QC.run $ runIteratee mothra >>= run_ . enum

        let e2 = enumBS bs

        let pcrlf = \s -> iterParser $ string "\r\n" >> return s

        sstep <- QC.run $ runIteratee stream2stream

        let iters' = replicate ntimes $ joinI $
                     readChunkedTransferEncoding sstep
        let godzilla = sequence $ map (>>= pcrlf) iters'

        x <- QC.run $ runIteratee godzilla >>= run_ . e2

        QC.assert $
          x == (replicate ntimes s')



testBothChunkedEmpty :: Test
testBothChunkedEmpty = testCase "parser/testBothChunkedEmpty" prop
  where
    prop = do
        let s' = ""
        let e = enumLBS s'
        let n = fromEnum $ L.length s'

        let ntimes = 5
        let enum = foldl' (>==>) (enumBS "") (replicate ntimes e)

        sstep <- runIteratee stream2stream

        step <- runIteratee $
                joinI $
                writeChunkedTransferEncoding sstep
        iter <- liftM returnI $ runIteratee $ joinI $ I.take n step

        let iters = replicate ntimes (iter :: Iteratee ByteString IO L.ByteString)
        let mothra = foldM (\s it -> it >>= \t -> return $ s `mappend` t)
                           mempty
                           iters

        mothraStep <- runIteratee mothra
        bs <- run_ $ enum mothraStep

        let e2 = enumLBS bs

        let pcrlf = \s -> iterParser $ string "\r\n" >> return s

        let iters' = replicate ntimes $ joinI $
                     readChunkedTransferEncoding sstep
        godzilla <- runIteratee $ sequence $ map (>>= pcrlf) iters'

        x <- run_ $ e2 godzilla

        assertBool "empty chunked transfer" $
          x == (replicate ntimes s')


testCookie :: Test
testCookie =
    testCase "parser/parseCookie" $ do
        assertEqual "cookie parsing" (Just [cv]) cv2

  where
    cv  = Cookie nm v Nothing Nothing Nothing
    cv2 = parseCookie ct

    nm     = "foo"
    v      = "bar"

    ct = S.concat [ nm
                  , "="
                  , v ]


testFormEncoded :: Test
testFormEncoded = testCase "parser/formEncoded" $ do
    let bs = "foo1=bar1&foo2=bar2+baz2&foo3=foo%20bar"
    let mp = parseUrlEncoded bs

    assertEqual "foo1" (Just ["bar1"]     ) $ Map.lookup "foo1" mp
    assertEqual "foo2" (Just ["bar2 baz2"]) $ Map.lookup "foo2" mp
    assertEqual "foo3" (Just ["foo bar"]  ) $ Map.lookup "foo3" mp


copyingStream2Stream :: (Monad m) => Iteratee ByteString m ByteString
copyingStream2Stream = go []
  where
    go l = do
        mbx <- I.head
        maybe (return $ S.concat $ reverse l)
              (\x -> let !z = S.copy x in go (z:l))
              mbx

stream2stream :: (Monad m) => Iteratee ByteString m L.ByteString
stream2stream = liftM L.fromChunks consume