@@ -0,0 +1,396 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+import Test.Framework (defaultMain, testGroup, Test)
3+import Test.Framework.Providers.HUnit
4+import Test.HUnit hiding (Test)
5+
6+import Network.Wai
7+import Network.Wai.Test
8+import Network.Wai.Parse
9+import qualified Data.ByteString as S
10+import qualified Data.ByteString.Char8 as S8
11+import qualified Data.ByteString.Lazy.Char8 as L8
12+import qualified Data.ByteString.Lazy as L
13+import Control.Arrow
14+
15+import Network.Wai.Middleware.Jsonp
16+import Network.Wai.Middleware.Gzip
17+import Network.Wai.Middleware.Vhost
18+import Network.Wai.Middleware.Autohead
19+import Network.Wai.Middleware.MethodOverride
20+import Network.Wai.Middleware.AcceptOverride
21+import Network.Wai.Middleware.Debug (debug)
22+import Codec.Compression.GZip (decompress)
23+
24+import Data.Enumerator (run_, enumList, ($$), Iteratee)
25+import Data.Enumerator.Binary (enumFile)
26+import Control.Monad.IO.Class (liftIO)
27+import Data.Maybe (fromMaybe)
28+import Network.HTTP.Types (parseSimpleQuery, status200)
29+
30+main :: IO ()
31+main = defaultMain [testSuite]
32+
33+testSuite :: Test
34+testSuite = testGroup "Network.Wai.Parse"
35+ [ testCase "parseQueryString" caseParseQueryString
36+ , testCase "parseQueryString with question mark" caseParseQueryStringQM
37+ , testCase "parseHttpAccept" caseParseHttpAccept
38+ , testCase "parseRequestBody" caseParseRequestBody
39+ , testCase "findBound" caseFindBound
40+ , testCase "sinkTillBound" caseSinkTillBound
41+ , testCase "killCR" caseKillCR
42+ , testCase "killCRLF" caseKillCRLF
43+ , testCase "takeLine" caseTakeLine
44+ , testCase "jsonp" caseJsonp
45+ , testCase "gzip" caseGzip
46+ , testCase "gzip not for MSIE" caseGzipMSIE
47+ , testCase "vhost" caseVhost
48+ , testCase "autohead" caseAutohead
49+ , testCase "method override" caseMethodOverride
50+ , testCase "accept override" caseAcceptOverride
51+ , testCase "dalvik multipart" caseDalvikMultipart
52+ , testCase "debug request body" caseDebugRequestBody
53+ ]
54+
55+caseParseQueryString :: Assertion
56+caseParseQueryString = do
57+ let go l r =
58+ map (S8.pack *** S8.pack) l @=? parseSimpleQuery (S8.pack r)
59+
60+ go [] ""
61+ go [("foo", "")] "foo"
62+ go [("foo", "bar")] "foo=bar"
63+ go [("foo", "bar"), ("baz", "bin")] "foo=bar&baz=bin"
64+ go [("%Q", "")] "%Q"
65+ go [("%1Q", "")] "%1Q"
66+ go [("%1", "")] "%1"
67+ go [("/", "")] "%2F"
68+ go [("/", "")] "%2f"
69+ go [("foo bar", "")] "foo+bar"
70+
71+caseParseQueryStringQM :: Assertion
72+caseParseQueryStringQM = do
73+ let go l r =
74+ map (S8.pack *** S8.pack) l
75+ @=? parseSimpleQuery (S8.pack $ '?' : r)
76+
77+ go [] ""
78+ go [("foo", "")] "foo"
79+ go [("foo", "bar")] "foo=bar"
80+ go [("foo", "bar"), ("baz", "bin")] "foo=bar&baz=bin"
81+ go [("%Q", "")] "%Q"
82+ go [("%1Q", "")] "%1Q"
83+ go [("%1", "")] "%1"
84+ go [("/", "")] "%2F"
85+ go [("/", "")] "%2f"
86+ go [("foo bar", "")] "foo+bar"
87+
88+caseParseHttpAccept :: Assertion
89+caseParseHttpAccept = do
90+ let input = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"
91+ expected = ["text/html", "text/x-c", "text/x-dvi", "text/plain"]
92+ expected @=? parseHttpAccept input
93+
94+parseRequestBody' :: Sink ([S8.ByteString] -> [S8.ByteString]) L.ByteString
95+ -> SRequest
96+ -> Iteratee S.ByteString IO ([(S.ByteString, S.ByteString)], [(S.ByteString, FileInfo L.ByteString)])
97+parseRequestBody' sink (SRequest req bod) =
98+ enumList 1 (L.toChunks bod) $$ parseRequestBody sink req
99+
100+caseParseRequestBody :: Assertion
101+caseParseRequestBody = run_ t where
102+ content2 = S8.pack $
103+ "--AaB03x\n" ++
104+ "Content-Disposition: form-data; name=\"document\"; filename=\"b.txt\"\n" ++
105+ "Content-Type: text/plain; charset=iso-8859-1\n\n" ++
106+ "This is a file.\n" ++
107+ "It has two lines.\n" ++
108+ "--AaB03x\n" ++
109+ "Content-Disposition: form-data; name=\"title\"\n" ++
110+ "Content-Type: text/plain; charset=iso-8859-1\n\n" ++
111+ "A File\n" ++
112+ "--AaB03x\n" ++
113+ "Content-Disposition: form-data; name=\"summary\"\n" ++
114+ "Content-Type: text/plain; charset=iso-8859-1\n\n" ++
115+ "This is my file\n" ++
116+ "file test\n" ++
117+ "--AaB03x--"
118+ content3 = S8.pack "------WebKitFormBoundaryB1pWXPZ6lNr8RiLh\r\nContent-Disposition: form-data; name=\"yaml\"; filename=\"README\"\r\nContent-Type: application/octet-stream\r\n\r\nPhoto blog using Hack.\n\r\n------WebKitFormBoundaryB1pWXPZ6lNr8RiLh--\r\n"
119+ t = do
120+ let content1 = "foo=bar&baz=bin"
121+ let ctype1 = "application/x-www-form-urlencoded"
122+ result1 <- parseRequestBody' lbsSink $ toRequest ctype1 content1
123+ liftIO $ assertEqual "parsing post x-www-form-urlencoded"
124+ (map (S8.pack *** S8.pack) [("foo", "bar"), ("baz", "bin")], [])
125+ result1
126+
127+ let ctype2 = "multipart/form-data; boundary=AaB03x"
128+ result2 <- parseRequestBody' lbsSink $ toRequest ctype2 content2
129+ let expectedsmap2 =
130+ [ ("title", "A File")
131+ , ("summary", "This is my file\nfile test")
132+ ]
133+ let textPlain = S8.pack $ "text/plain; charset=iso-8859-1"
134+ let expectedfile2 =
135+ [(S8.pack "document", FileInfo (S8.pack "b.txt") textPlain $ L8.pack
136+ "This is a file.\nIt has two lines.")]
137+ let expected2 = (map (S8.pack *** S8.pack) expectedsmap2, expectedfile2)
138+ liftIO $ assertEqual "parsing post multipart/form-data"
139+ expected2
140+ result2
141+
142+ let ctype3 = "multipart/form-data; boundary=----WebKitFormBoundaryB1pWXPZ6lNr8RiLh"
143+ result3 <- parseRequestBody' lbsSink $ toRequest ctype3 content3
144+ let expectedsmap3 = []
145+ let expectedfile3 = [(S8.pack "yaml", FileInfo (S8.pack "README") (S8.pack "application/octet-stream") $
146+ L8.pack "Photo blog using Hack.\n")]
147+ let expected3 = (expectedsmap3, expectedfile3)
148+ liftIO $ assertEqual "parsing actual post multipart/form-data"
149+ expected3
150+ result3
151+
152+ result2' <- parseRequestBody' lbsSink $ toRequest' ctype2 content2
153+ liftIO $ assertEqual "parsing post multipart/form-data 2"
154+ expected2
155+ result2'
156+
157+ result3' <- parseRequestBody' lbsSink $ toRequest' ctype3 content3
158+ liftIO $ assertEqual "parsing actual post multipart/form-data 2"
159+ expected3
160+ result3'
161+
162+toRequest :: S8.ByteString -> S8.ByteString -> SRequest
163+toRequest ctype content = SRequest (Request
164+ { requestHeaders = [("Content-Type", ctype)]
165+ , requestMethod = "POST"
166+ , rawPathInfo = ""
167+ , rawQueryString = ""
168+ }) (L.fromChunks [content])
169+
170+toRequest' :: S8.ByteString -> S8.ByteString -> SRequest
171+toRequest' ctype content = SRequest (Request
172+ { requestHeaders = [("Content-Type", ctype)]
173+ }) (L.fromChunks $ map S.singleton $ S.unpack content)
174+
175+caseFindBound :: Assertion
176+caseFindBound = do
177+ findBound (S8.pack "def") (S8.pack "abcdefghi") @?=
178+ FoundBound (S8.pack "abc") (S8.pack "ghi")
179+ findBound (S8.pack "def") (S8.pack "ABC") @?= NoBound
180+ findBound (S8.pack "def") (S8.pack "abcd") @?= PartialBound
181+ findBound (S8.pack "def") (S8.pack "abcdE") @?= NoBound
182+ findBound (S8.pack "def") (S8.pack "abcdEdef") @?=
183+ FoundBound (S8.pack "abcdE") (S8.pack "")
184+
185+caseSinkTillBound :: Assertion
186+caseSinkTillBound = do
187+ let iter () _ = return ()
188+ let src = S8.pack "this is some text"
189+ bound1 = S8.pack "some"
190+ bound2 = S8.pack "some!"
191+ let enum = enumList 1 [src]
192+ let helper _ _ = return ()
193+ (_, res1) <- run_ $ enum $$ sinkTillBound bound1 helper ()
194+ res1 @?= True
195+ (_, res2) <- run_ $ enum $$ sinkTillBound bound2 helper ()
196+ res2 @?= False
197+
198+caseKillCR :: Assertion
199+caseKillCR = do
200+ "foo" @=? killCR "foo"
201+ "foo" @=? killCR "foo\r"
202+ "foo\r\n" @=? killCR "foo\r\n"
203+ "foo\r'" @=? killCR "foo\r'"
204+
205+caseKillCRLF :: Assertion
206+caseKillCRLF = do
207+ "foo" @=? killCRLF "foo"
208+ "foo\r" @=? killCRLF "foo\r"
209+ "foo" @=? killCRLF "foo\r\n"
210+ "foo\r'" @=? killCRLF "foo\r'"
211+ "foo" @=? killCRLF "foo\n"
212+
213+caseTakeLine :: Assertion
214+caseTakeLine = do
215+ helper "foo\nbar\nbaz" "foo"
216+ helper "foo\r\nbar\nbaz" "foo"
217+ helper "foo\nbar\r\nbaz" "foo"
218+ helper "foo\rbar\r\nbaz" "foo\rbar"
219+ where
220+ helper haystack needle = do
221+ x <- run_ $ enumList 1 [haystack] $$ takeLine
222+ Just needle @=? x
223+
224+jsonpApp = jsonp $ const $ return $ responseLBS
225+ status200
226+ [("Content-Type", "application/json")]
227+ "{\"foo\":\"bar\"}"
228+
229+caseJsonp :: Assertion
230+caseJsonp = flip runSession jsonpApp $ do
231+ sres1 <- request Request
232+ { queryString = [("callback", Just "test")]
233+ , requestHeaders = [("Accept", "text/javascript")]
234+ }
235+ assertContentType "text/javascript" sres1
236+ assertBody "test({\"foo\":\"bar\"})" sres1
237+
238+ sres2 <- request Request
239+ { queryString = [("call_back", Just "test")]
240+ , requestHeaders = [("Accept", "text/javascript")]
241+ }
242+ assertContentType "application/json" sres2
243+ assertBody "{\"foo\":\"bar\"}" sres2
244+
245+ sres3 <- request Request
246+ { queryString = [("callback", Just "test")]
247+ , requestHeaders = [("Accept", "text/html")]
248+ }
249+ assertContentType "application/json" sres3
250+ assertBody "{\"foo\":\"bar\"}" sres3
251+
252+gzipApp = gzip True $ const $ return $ responseLBS status200 [] "test"
253+
254+caseGzip :: Assertion
255+caseGzip = flip runSession gzipApp $ do
256+ sres1 <- request Request
257+ { requestHeaders = [("Accept-Encoding", "gzip")]
258+ }
259+ assertHeader "Content-Encoding" "gzip" sres1
260+ liftIO $ decompress (simpleBody sres1) @?= "test"
261+
262+ sres2 <- request Request
263+ { requestHeaders = []
264+ }
265+ assertNoHeader "Content-Encoding" sres2
266+ assertBody "test" sres2
267+
268+caseGzipMSIE :: Assertion
269+caseGzipMSIE = flip runSession gzipApp $ do
270+ sres1 <- request Request
271+ { requestHeaders =
272+ [ ("Accept-Encoding", "gzip")
273+ , ("User-Agent", "Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 6.0)")
274+ ]
275+ }
276+ assertNoHeader "Content-Encoding" sres1
277+ liftIO $ simpleBody sres1 @?= "test"
278+
279+vhostApp1 = const $ return $ responseLBS status200 [] "app1"
280+vhostApp2 = const $ return $ responseLBS status200 [] "app2"
281+vhostApp = vhost
282+ [ ((== "foo.com") . serverName, vhostApp1)
283+ ]
284+ vhostApp2
285+
286+caseVhost = flip runSession vhostApp $ do
287+ sres1 <- request Request
288+ { serverName = "foo.com"
289+ }
290+ assertBody "app1" sres1
291+
292+ sres2 <- request Request
293+ { serverName = "bar.com"
294+ }
295+ assertBody "app2" sres2
296+
297+autoheadApp = autohead $ const $ return $ responseLBS status200
298+ [("Foo", "Bar")] "body"
299+
300+caseAutohead = flip runSession autoheadApp $ do
301+ sres1 <- request Request
302+ { requestMethod = "GET"
303+ }
304+ assertHeader "Foo" "Bar" sres1
305+ assertBody "body" sres1
306+
307+ sres1 <- request Request
308+ { requestMethod = "HEAD"
309+ }
310+ assertHeader "Foo" "Bar" sres1
311+ assertBody "" sres1
312+
313+moApp = methodOverride $ \req -> return $ responseLBS status200
314+ [("Method", requestMethod req)] ""
315+
316+caseMethodOverride = flip runSession moApp $ do
317+ sres1 <- request Request
318+ { requestMethod = "GET"
319+ , queryString = []
320+ }
321+ assertHeader "Method" "GET" sres1
322+
323+ sres2 <- request Request
324+ { requestMethod = "POST"
325+ , queryString = []
326+ }
327+ assertHeader "Method" "POST" sres2
328+
329+ sres3 <- request Request
330+ { requestMethod = "POST"
331+ , queryString = [("_method", Just "PUT")]
332+ }
333+ assertHeader "Method" "PUT" sres3
334+
335+aoApp = acceptOverride $ \req -> return $ responseLBS status200
336+ [("Accept", fromMaybe "" $ lookup "Accept" $ requestHeaders req)] ""
337+
338+caseAcceptOverride = flip runSession aoApp $ do
339+ sres1 <- request Request
340+ { queryString = []
341+ , requestHeaders = [("Accept", "foo")]
342+ }
343+ assertHeader "Accept" "foo" sres1
344+
345+ sres2 <- request Request
346+ { queryString = []
347+ , requestHeaders = [("Accept", "bar")]
348+ }
349+ assertHeader "Accept" "bar" sres2
350+
351+ sres3 <- request Request
352+ { queryString = [("_accept", Just "baz")]
353+ , requestHeaders = [("Accept", "bar")]
354+ }
355+ assertHeader "Accept" "baz" sres3
356+
357+caseDalvikMultipart = do
358+ let headers =
359+ [ ("content-length", "12098")
360+ , ("content-type", "multipart/form-data;boundary=*****")
361+ , ("GATEWAY_INTERFACE", "CGI/1.1")
362+ , ("PATH_INFO", "/")
363+ , ("QUERY_STRING", "")
364+ , ("REMOTE_ADDR", "192.168.1.115")
365+ , ("REMOTE_HOST", "ganjizza")
366+ , ("REQUEST_URI", "http://192.168.1.115:3000/")
367+ , ("REQUEST_METHOD", "POST")
368+ , ("HTTP_CONNECTION", "Keep-Alive")
369+ , ("HTTP_COOKIE", "_SESSION=fgUGM5J/k6mGAAW+MMXIJZCJHobw/oEbb6T17KQN0p9yNqiXn/m/ACrsnRjiCEgqtG4fogMUDI+jikoFGcwmPjvuD5d+MDz32iXvDdDJsFdsFMfivuey2H+n6IF6yFGD")
370+ , ("HTTP_USER_AGENT", "Dalvik/1.1.0 (Linux; U; Android 2.1-update1; sdk Build/ECLAIR)")
371+ , ("HTTP_HOST", "192.168.1.115:3000")
372+ , ("HTTP_ACCEPT", "*, */*")
373+ , ("HTTP_VERSION", "HTTP/1.1")
374+ , ("REQUEST_PATH", "/")
375+ ]
376+ let request = Request
377+ { requestHeaders = headers
378+ }
379+ (params, files) <- run_ $ enumFile "test/dalvik-request" $$ parseRequestBody lbsSink request
380+ lookup "scannedTime" params @?= Just "1.298590056748E9"
381+ lookup "geoLong" params @?= Just "0"
382+ lookup "geoLat" params @?= Just "0"
383+ length files @?= 1
384+
385+caseDebugRequestBody :: Assertion
386+caseDebugRequestBody = flip runSession debugApp $ do
387+ let req = toRequest "application/x-www-form-urlencoded" "foo=bar&baz=bin"
388+ res <- srequest req
389+ assertStatus 200 res
390+ assertHeader "Parsed" (S8.pack $ show ([("foo", "bar"), ("baz", "bin")], [] :: [Int])) res
391+
392+debugApp = debug $ \req -> do
393+ x <- parseRequestBody lbsSink req
394+ return $ responseLBS status200
395+ [ ("Parsed", S8.pack $ show x)
396+ ] ""