1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE ScopedTypeVariables #-}
4module RunSpec (main, spec, withApp) where
6import Control.Concurrent (forkIO, killThread, threadDelay)
7import Control.Monad (forM_, replicateM_)
8import System.Timeout (timeout)
9import Control.Monad.IO.Class (MonadIO, liftIO)
10import Data.ByteString (ByteString, hPutStr, hGetSome)
11import qualified Data.ByteString as S
12import qualified Data.ByteString.Char8 as S8
13import qualified Data.ByteString.Lazy as L
14import Data.Conduit (($$), (=$))
15import qualified Data.Conduit.List
16import qualified Data.IORef as I
17import Network (connectTo, PortID (PortNumber))
18import Network.HTTP.Types
20import Network.Wai.Handler.Warp
21import System.IO (hFlush, hClose)
22import System.IO.Unsafe (unsafePerformIO)
24import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar)
25import Control.Exception.Lifted (bracket, try, IOException, onException)
26import Data.Conduit.Network (bindPort)
27import Network.Socket (sClose)
28import qualified Network.HTTP as HTTP
33type Counter = I.IORef (Either String Int)
34type CounterApplication = Counter -> Application
36incr :: MonadIO m => Counter -> m ()
37incr icount = liftIO $ I.atomicModifyIORef icount $ \ecount ->
40 Right i -> Right $ i + 1), ())
42err :: (MonadIO m, Show a) => Counter -> a -> m ()
43err icount msg = liftIO $ I.writeIORef icount $ Left $ show msg
45readBody :: CounterApplication
46readBody icount req = do
47 body <- requestBody req $$ Data.Conduit.List.consume
50 | pathInfo req == ["hello"] && L.fromChunks body /= "Hello"
51 -> err icount ("Invalid hello" :: String, body)
52 | requestMethod req == "GET" && L.fromChunks body /= ""
53 -> err icount ("Invalid GET" :: String, body)
54 | not $ requestMethod req `elem` ["GET", "POST"]
55 -> err icount ("Invalid request method (readBody)" :: String, requestMethod req)
56 | otherwise -> incr icount
57 return $ responseLBS status200 [] "Read the body"
59ignoreBody :: CounterApplication
60ignoreBody icount req = do
61 if (requestMethod req `elem` ["GET", "POST"])
63 else err icount ("Invalid request method" :: String, requestMethod req)
64 return $ responseLBS status200 [] "Ignored the body"
66doubleConnect :: CounterApplication
67doubleConnect icount req = do
68 _ <- requestBody req $$ Data.Conduit.List.consume
69 _ <- requestBody req $$ Data.Conduit.List.consume
71 return $ responseLBS status200 [] "double connect"
73nextPort :: I.IORef Int
74nextPort = unsafePerformIO $ I.newIORef 5000
78 port <- I.atomicModifyIORef nextPort $ \p -> (p + 1, p)
79 esocket <- try $ bindPort port HostIPv4
81 Left (_ :: IOException) -> RunSpec.getPort
86withApp :: Settings -> Application -> (Int -> IO a) -> IO a
87withApp settings app f = do
88 port <- RunSpec.getPort
90 let settings' = setPort port
91 $ setBeforeMainLoop (putMVar baton ())
94 (forkIO $ runSettings settings' app `onException` putMVar baton ())
96 (const $ takeMVar baton >> f port)
98runTest :: Int -- ^ expected number of requests
100 -> [ByteString] -- ^ chunks to send
102runTest expected app chunks = do
103 ref <- I.newIORef (Right 0)
104 withApp defaultSettings (app ref) $ \port -> do
105 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
106 forM_ chunks $ \chunk -> hPutStr handle chunk >> hFlush handle
107 _ <- timeout 100000 $ replicateM_ expected $ hGetSome handle 4096
108 res <- I.readIORef ref
111 Right i -> i `shouldBe` expected
113dummyApp :: Application
114dummyApp _ = return $ responseLBS status200 [] "foo"
116runTerminateTest :: InvalidRequest
119runTerminateTest expected input = do
120 ref <- I.newIORef Nothing
121 let onExc _ = I.writeIORef ref . Just
122 withApp (setOnException onExc defaultSettings) dummyApp $ \port -> do
123 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
128 res <- I.readIORef ref
129 show res `shouldBe` show (Just expected)
131singleGet :: ByteString
132singleGet = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
134singlePostHello :: ByteString
135singlePostHello = "POST /hello HTTP/1.1\r\nHost: localhost\r\nContent-length: 5\r\n\r\nHello"
139 describe "non-pipelining" $ do
140 it "no body, read" $ runTest 5 readBody $ replicate 5 singleGet
141 it "no body, ignore" $ runTest 5 ignoreBody $ replicate 5 singleGet
142 it "has body, read" $ runTest 2 readBody
146 it "has body, ignore" $ runTest 2 ignoreBody
150 describe "pipelining" $ do
151 it "no body, read" $ runTest 5 readBody [S.concat $ replicate 5 singleGet]
152 it "no body, ignore" $ runTest 5 ignoreBody [S.concat $ replicate 5 singleGet]
153 it "has body, read" $ runTest 2 readBody $ return $ S.concat
157 it "has body, ignore" $ runTest 2 ignoreBody $ return $ S.concat
161 describe "no hanging" $ do
162 it "has body, read" $ runTest 1 readBody $ map S.singleton $ S.unpack singlePostHello
163 it "double connect" $ runTest 1 doubleConnect [singlePostHello]
165 describe "connection termination" $ do
166-- it "ConnectionClosedByPeer" $ runTerminateTest ConnectionClosedByPeer "GET / HTTP/1.1\r\ncontent-length: 10\r\n\r\nhello"
167 it "IncompleteHeaders" $ runTerminateTest IncompleteHeaders "GET / HTTP/1.1\r\ncontent-length: 10\r\n"
169 describe "special input" $ do
170 it "multiline headers" $ do
171 iheaders <- I.newIORef []
173 liftIO $ I.writeIORef iheaders $ requestHeaders req
174 return $ responseLBS status200 [] ""
175 withApp defaultSettings app $ \port -> do
176 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
178 [ "GET / HTTP/1.1\r\nfoo: bar\r\n baz\r\n\tbin\r\n\r\n"
184 headers <- I.readIORef iheaders
186 [ ("foo", "bar baz\tbin")
188 it "no space between colon and value" $ do
189 iheaders <- I.newIORef []
191 liftIO $ I.writeIORef iheaders $ requestHeaders req
192 return $ responseLBS status200 [] ""
193 withApp defaultSettings app $ \port -> do
194 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
196 [ "GET / HTTP/1.1\r\nfoo:bar\r\n\r\n"
202 headers <- I.readIORef iheaders
207 describe "chunked bodies" $ do
209 ifront <- I.newIORef id
211 bss <- requestBody req $$ Data.Conduit.List.consume
212 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
213 return $ responseLBS status200 [] ""
214 withApp defaultSettings app $ \port -> do
215 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
217 [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
218 , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n\r\n"
219 , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
220 , "b\r\nHello World\r\n0\r\n\r\n"
226 front <- I.readIORef ifront
231 it "lots of chunks" $ do
232 ifront <- I.newIORef id
234 bss <- requestBody req $$ Data.Conduit.List.consume
235 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
236 return $ responseLBS status200 [] ""
237 withApp defaultSettings app $ \port -> do
238 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
239 let input = concat $ replicate 2 $
240 ["POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"] ++
241 (replicate 50 "5\r\n12345\r\n") ++
243 mapM_ (\bs -> hPutStr handle bs >> hFlush handle) input
246 front <- I.readIORef ifront
247 front [] `shouldBe` replicate 2 (S.concat $ replicate 50 "12345")
249 ifront <- I.newIORef id
251 bss <- requestBody req $$ Data.Conduit.List.consume
252 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
253 return $ responseLBS status200 [] ""
254 withApp defaultSettings app $ \port -> do
255 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
257 [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
258 , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n"
259 , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
260 , "b\r\nHello World\r\n0\r\n\r\n"
262 mapM_ (\bs -> hPutStr handle bs >> hFlush handle) $ map S.singleton $ S.unpack input
265 front <- I.readIORef ifront
270 it "timeout in request body" $ do
271 ifront <- I.newIORef id
273 bss <- (requestBody req $$ Data.Conduit.List.consume) `onException`
274 liftIO (I.atomicModifyIORef ifront (\front -> (front . ("consume interrupted":), ())))
275 liftIO $ threadDelay 4000000 `onException`
276 I.atomicModifyIORef ifront (\front -> (front . ("threadDelay interrupted":), ()))
277 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
278 return $ responseLBS status200 [] ""
279 withApp (setTimeout 1 defaultSettings) app $ \port -> do
280 let bs1 = S.replicate 2048 88
281 bs2 = "This is short"
282 bs = S.append bs1 bs2
283 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
284 hPutStr handle "POST / HTTP/1.1\r\n"
285 hPutStr handle "content-length: "
286 hPutStr handle $ S8.pack $ show $ S.length bs
287 hPutStr handle "\r\n\r\n"
294 front <- I.readIORef ifront
295 S.concat (front []) `shouldBe` bs
296 describe "raw body" $ do
299 let backup = responseLBS status200 [] "Not raw"
300 return $ flip responseRaw backup $ \src sink ->
302 $$ Data.Conduit.List.map doubleBS
304 doubleBS = S.concatMap $ \w -> S.pack [w, w]
305 withApp defaultSettings app $ \port -> do
306 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
307 hPutStr handle "POST / HTTP/1.1\r\n\r\n12345"
309 timeout 100000 (S.hGet handle 10) >>= (`shouldBe` Just "1122334455")
310 hPutStr handle "67890"
312 timeout 100000 (S.hGet handle 10) >>= (`shouldBe` Just "6677889900")
314 it "only one date and server header" $ do
315 let app _ = return $ responseLBS status200
316 [ ("server", "server")
319 withApp defaultSettings app $ \port -> do
320 Right res <- HTTP.simpleHTTP (HTTP.getRequest $ "http://127.0.0.1:" ++ show port)
321 map HTTP.hdrValue (HTTP.retrieveHeaders HTTP.HdrServer res)
322 `shouldBe` ["server"]
323 map HTTP.hdrValue (HTTP.retrieveHeaders HTTP.HdrDate res)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list