packages feed

warp-1.3.0: test/main.hs

{-# LANGUAGE OverloadedStrings #-}
import Network.Wai
import Network.Wai.Handler.Warp
import qualified Data.IORef as I
import Control.Monad.IO.Class (MonadIO, liftIO)
import Network.HTTP.Types
import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Monad (forM_)

import System.IO (hFlush, hClose)
import System.IO.Unsafe (unsafePerformIO)
import Data.ByteString (ByteString, hPutStr, hGetSome)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Network (connectTo, PortID (PortNumber))

import Test.Hspec.Monadic
import Test.Hspec.HUnit ()
import Test.HUnit

import Data.Conduit (($$))
import qualified Data.Conduit.List

type Counter = I.IORef (Either String Int)
type CounterApplication = Counter -> Application

incr :: MonadIO m => Counter -> m ()
incr icount = liftIO $ I.atomicModifyIORef icount $ \ecount ->
    ((case ecount of
        Left s -> Left s
        Right i -> Right $ i + 1), ())

err :: (MonadIO m, Show a) => Counter -> a -> m ()
err icount msg = liftIO $ I.writeIORef icount $ Left $ show msg

readBody :: CounterApplication
readBody icount req = do
    body <- requestBody req $$ Data.Conduit.List.consume
    case () of
        ()
            | pathInfo req == ["hello"] && L.fromChunks body /= "Hello"
                -> err icount ("Invalid hello" :: String, body)
            | requestMethod req == "GET" && L.fromChunks body /= ""
                -> err icount ("Invalid GET" :: String, body)
            | not $ requestMethod req `elem` ["GET", "POST"]
                -> err icount ("Invalid request method (readBody)" :: String, requestMethod req)
            | otherwise -> incr icount
    return $ responseLBS status200 [] "Read the body"

ignoreBody :: CounterApplication
ignoreBody icount req = do
    if (requestMethod req `elem` ["GET", "POST"])
        then incr icount
        else err icount ("Invalid request method" :: String, requestMethod req)
    return $ responseLBS status200 [] "Ignored the body"

doubleConnect :: CounterApplication
doubleConnect icount req = do
    _ <- requestBody req $$ Data.Conduit.List.consume
    _ <- requestBody req $$ Data.Conduit.List.consume
    incr icount
    return $ responseLBS status200 [] "double connect"

nextPort :: I.IORef Int
nextPort = unsafePerformIO $ I.newIORef 5000

getPort :: IO Int
getPort = I.atomicModifyIORef nextPort $ \p -> (p + 1, p)

runTest :: Int -- ^ expected number of requests
        -> CounterApplication
        -> [ByteString] -- ^ chunks to send
        -> IO ()
runTest expected app chunks = do
    port <- getPort
    ref <- I.newIORef (Right 0)
    tid <- forkIO $ run port $ app ref
    threadDelay 1000
    handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
    forM_ chunks $ \chunk -> hPutStr handle chunk >> hFlush handle
    _ <- hGetSome handle 4096
    threadDelay 1000
    killThread tid
    res <- I.readIORef ref
    case res of
        Left s -> error s
        Right i -> i @?= expected

dummyApp :: Application
dummyApp _ = return $ responseLBS status200 [] "foo"

runTerminateTest :: InvalidRequest
                 -> ByteString
                 -> IO ()
runTerminateTest expected input = do
    port <- getPort
    ref <- I.newIORef Nothing
    tid <- forkIO $ runSettings defaultSettings
        { settingsOnException = \e -> I.writeIORef ref $ Just e
        , settingsPort = port
        } dummyApp
    threadDelay 1000
    handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
    hPutStr handle input
    hFlush handle
    hClose handle
    threadDelay 1000
    killThread tid
    res <- I.readIORef ref
    show res @?= show (Just expected)

singleGet :: ByteString
singleGet = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"

singlePostHello :: ByteString
singlePostHello = "POST /hello HTTP/1.1\r\nHost: localhost\r\nContent-length: 5\r\n\r\nHello"

main :: IO ()
main = hspec $ do
    describe "non-pipelining" $ do
        it "no body, read" $ runTest 5 readBody $ replicate 5 singleGet
        it "no body, ignore" $ runTest 5 ignoreBody $ replicate 5 singleGet
        it "has body, read" $ runTest 2 readBody
            [ singlePostHello
            , singleGet
            ]
        it "has body, ignore" $ runTest 2 ignoreBody
            [ singlePostHello
            , singleGet
            ]
    describe "pipelining" $ do
        it "no body, read" $ runTest 5 readBody [S.concat $ replicate 5 singleGet]
        it "no body, ignore" $ runTest 5 ignoreBody [S.concat $ replicate 5 singleGet]
        it "has body, read" $ runTest 2 readBody $ return $ S.concat
            [ singlePostHello
            , singleGet
            ]
        it "has body, ignore" $ runTest 2 ignoreBody $ return $ S.concat
            [ singlePostHello
            , singleGet
            ]
    describe "no hanging" $ do
        it "has body, read" $ runTest 1 readBody $ map S.singleton $ S.unpack singlePostHello
        it "double connect" $ runTest 1 doubleConnect [singlePostHello]

    describe "connection termination" $ do
        it "ConnectionClosedByPeer" $ runTerminateTest ConnectionClosedByPeer "GET / HTTP/1.1\r\ncontent-length: 10\r\n\r\nhello"
        it "IncompleteHeaders" $ runTerminateTest IncompleteHeaders "GET / HTTP/1.1\r\ncontent-length: 10\r\n"

    describe "special input" $ do
        it "multiline headers" $ do
            iheaders <- I.newIORef []
            port <- getPort
            tid <- forkIO $ run port $ \req -> do
                liftIO $ I.writeIORef iheaders $ requestHeaders req
                return $ responseLBS status200 [] ""
            threadDelay 1000
            handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
            let input = S.concat
                    [ "GET / HTTP/1.1\r\nfoo:    bar\r\n baz\r\n\tbin\r\n\r\n"
                    ]
            hPutStr handle input
            hFlush handle
            hClose handle
            threadDelay 1000
            killThread tid
            headers <- I.readIORef iheaders
            headers @?=
                [ ("foo", "bar baz\tbin")
                ]
        it "no space between colon and value" $ do
            iheaders <- I.newIORef []
            port <- getPort
            tid <- forkIO $ run port $ \req -> do
                liftIO $ I.writeIORef iheaders $ requestHeaders req
                return $ responseLBS status200 [] ""
            threadDelay 1000
            handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
            let input = S.concat
                    [ "GET / HTTP/1.1\r\nfoo:bar\r\n\r\n"
                    ]
            hPutStr handle input
            hFlush handle
            hClose handle
            threadDelay 1000
            killThread tid
            headers <- I.readIORef iheaders
            headers @?=
                [ ("foo", "bar")
                ]
        it "extra spaces in first line" $ do
            iheaders <- I.newIORef []
            port <- getPort
            tid <- forkIO $ run port $ \req -> do
                liftIO $ I.writeIORef iheaders $ requestHeaders req
                return $ responseLBS status200 [] ""
            threadDelay 1000
            handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
            let input = S.concat
                    [ "GET    /    HTTP/1.1\r\nfoo: bar\r\n\r\n"
                    ]
            hPutStr handle input
            hFlush handle
            hClose handle
            threadDelay 1000
            killThread tid
            headers <- I.readIORef iheaders
            headers @?=
                [ ("foo", "bar")
                ]
        it "spaces in http version" $ do
            iversion <- I.newIORef $ error "Version not parsed"
            port <- getPort
            tid <- forkIO $ run port $ \req -> do
                liftIO $ I.writeIORef iversion $ httpVersion req
                return $ responseLBS status200 [] ""
            threadDelay 1000
            handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
            let input = S.concat
                    [ "GET    /    HTTP\t/  1 .   1  \r\nfoo: bar\r\n\r\n"
                    ]
            hPutStr handle input
            hFlush handle
            hClose handle
            threadDelay 1000
            killThread tid
            version <- I.readIORef iversion
            version @?= http11

    describe "chunked bodies" $ do
        it "works" $ do
            ifront <- I.newIORef id
            port <- getPort
            tid <- forkIO $ run port $ \req -> do
                bss <- requestBody req $$ Data.Conduit.List.consume
                liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
                return $ responseLBS status200 [] ""
            threadDelay 1000
            handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
            let input = S.concat
                    [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
                    , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n"
                    , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
                    , "b\r\nHello World\r\n0\r\n"
                    ]
            hPutStr handle input
            hFlush handle
            hClose handle
            threadDelay 1000
            killThread tid
            front <- I.readIORef ifront
            front [] @?=
                [ "Hello World\nBye"
                , "Hello World"
                ]
        it "lots of chunks" $ do
            ifront <- I.newIORef id
            port <- getPort
            tid <- forkIO $ run port $ \req -> do
                bss <- requestBody req $$ Data.Conduit.List.consume
                liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
                return $ responseLBS status200 [] ""
            threadDelay 1000
            handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
            let input = concat $ replicate 2 $
                    ["POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"] ++
                    (replicate 50 "5\r\n12345\r\n") ++
                    ["0\r\n"]
            mapM_ (\bs -> hPutStr handle bs >> hFlush handle) input
            hClose handle
            threadDelay 1000
            killThread tid
            front <- I.readIORef ifront
            front [] @?= replicate 2 (S.concat $ replicate 50 "12345")
        it "in chunks" $ do
            ifront <- I.newIORef id
            port <- getPort
            tid <- forkIO $ run port $ \req -> do
                bss <- requestBody req $$ Data.Conduit.List.consume
                liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
                return $ responseLBS status200 [] ""
            threadDelay 1000
            handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
            let input = S.concat
                    [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
                    , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n"
                    , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
                    , "b\r\nHello World\r\n0\r\n"
                    ]
            mapM_ (\bs -> hPutStr handle bs >> hFlush handle) $ map S.singleton $ S.unpack input
            hClose handle
            threadDelay 1000
            killThread tid
            front <- I.readIORef ifront
            front [] @?=
                [ "Hello World\nBye"
                , "Hello World"
                ]