main.hs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE ScopedTypeVariables #-}
  3. {-# LANGUAGE CPP #-}
  4. {-# LANGUAGE MultiParamTypeClasses #-}
  5. {-# LANGUAGE TemplateHaskell #-}
  6. {-# LANGUAGE QuasiQuotes #-}
  7. {-# LANGUAGE TypeFamilies #-}
  8. import Test.HUnit hiding (Test)
  9. import Test.Hspec
  10. import Yesod.Core
  11. import Yesod.Form
  12. import Yesod.Test
  13. import Yesod.Test.CssQuery
  14. import Yesod.Test.TransversingCSS
  15. import Text.XML
  16. import Data.Text (Text, pack)
  17. import Data.Monoid ((<>))
  18. import Control.Applicative
  19. import Network.Wai (pathInfo, requestHeaders)
  20. import Data.Maybe (fromMaybe)
  21. import Data.Either (isLeft, isRight)
  22. import Control.Exception.Lifted(try, SomeException)
  23. import Data.ByteString.Lazy.Char8 ()
  24. import qualified Data.Map as Map
  25. import qualified Text.HTML.DOM as HD
  26. import Network.HTTP.Types.Status (status301, status303, unsupportedMediaType415)
  27. parseQuery_ = either error id . parseQuery
  28. findBySelector_ x = either error id . findBySelector x
  29. parseHtml_ = HD.parseLBS
  30. main :: IO ()
  31. main = hspec $ do
  32. describe "CSS selector parsing" $ do
  33. it "elements" $ parseQuery_ "strong" @?= [[DeepChildren [ByTagName "strong"]]]
  34. it "child elements" $ parseQuery_ "strong > i" @?= [[DeepChildren [ByTagName "strong"], DirectChildren [ByTagName "i"]]]
  35. it "comma" $ parseQuery_ "strong.bar, #foo" @?= [[DeepChildren [ByTagName "strong", ByClass "bar"]], [DeepChildren [ById "foo"]]]
  36. describe "find by selector" $ do
  37. it "XHTML" $
  38. let html = "<html><head><title>foo</title></head><body><p>Hello World</p></body></html>"
  39. query = "body > p"
  40. in findBySelector_ html query @?= ["<p>Hello World</p>"]
  41. it "HTML" $
  42. let html = "<html><head><title>foo</title></head><body><br><p>Hello World</p></body></html>"
  43. query = "body > p"
  44. in findBySelector_ html query @?= ["<p>Hello World</p>"]
  45. let query = "form.foo input[name=_token][type=hidden][value]"
  46. html = "<input name='_token' type='hidden' value='foo'><form class='foo'><input name='_token' type='hidden' value='bar'></form>"
  47. expected = "<input name=\"_token\" type=\"hidden\" value=\"bar\" />"
  48. in it query $ findBySelector_ html (pack query) @?= [expected]
  49. it "descendents and children" $
  50. let html = "<html><p><b><i><u>hello</u></i></b></p></html>"
  51. query = "p > b u"
  52. in findBySelector_ html query @?= ["<u>hello</u>"]
  53. it "hyphenated classes" $
  54. let html = "<html><p class='foo-bar'><b><i><u>hello</u></i></b></p></html>"
  55. query = "p.foo-bar u"
  56. in findBySelector_ html query @?= ["<u>hello</u>"]
  57. it "descendents" $
  58. let html = "<html><p><b><i>hello</i></b></p></html>"
  59. query = "p i"
  60. in findBySelector_ html query @?= ["<i>hello</i>"]
  61. describe "HTML parsing" $ do
  62. it "XHTML" $
  63. let html = "<html><head><title>foo</title></head><body><p>Hello World</p></body></html>"
  64. doc = Document (Prologue [] Nothing []) root []
  65. root = Element "html" Map.empty
  66. [ NodeElement $ Element "head" Map.empty
  67. [ NodeElement $ Element "title" Map.empty
  68. [NodeContent "foo"]
  69. ]
  70. , NodeElement $ Element "body" Map.empty
  71. [ NodeElement $ Element "p" Map.empty
  72. [NodeContent "Hello World"]
  73. ]
  74. ]
  75. in parseHtml_ html @?= doc
  76. it "HTML" $
  77. let html = "<html><head><title>foo</title></head><body><br><p>Hello World</p></body></html>"
  78. doc = Document (Prologue [] Nothing []) root []
  79. root = Element "html" Map.empty
  80. [ NodeElement $ Element "head" Map.empty
  81. [ NodeElement $ Element "title" Map.empty
  82. [NodeContent "foo"]
  83. ]
  84. , NodeElement $ Element "body" Map.empty
  85. [ NodeElement $ Element "br" Map.empty []
  86. , NodeElement $ Element "p" Map.empty
  87. [NodeContent "Hello World"]
  88. ]
  89. ]
  90. in parseHtml_ html @?= doc
  91. describe "basic usage" $ yesodSpec app $ do
  92. ydescribe "tests1" $ do
  93. yit "tests1a" $ do
  94. get ("/" :: Text)
  95. statusIs 200
  96. bodyEquals "Hello world!"
  97. yit "tests1b" $ do
  98. get ("/foo" :: Text)
  99. statusIs 404
  100. ydescribe "tests2" $ do
  101. yit "type-safe URLs" $ do
  102. get $ LiteAppRoute []
  103. statusIs 200
  104. yit "type-safe URLs with query-string" $ do
  105. get (LiteAppRoute [], [("foo", "bar")])
  106. statusIs 200
  107. bodyEquals "foo=bar"
  108. yit "post params" $ do
  109. post ("/post" :: Text)
  110. statusIs 500
  111. request $ do
  112. setMethod "POST"
  113. setUrl $ LiteAppRoute ["post"]
  114. addPostParam "foo" "foobarbaz"
  115. statusIs 200
  116. bodyEquals "foobarbaz"
  117. yit "labels" $ do
  118. get ("/form" :: Text)
  119. statusIs 200
  120. request $ do
  121. setMethod "POST"
  122. setUrl ("/form" :: Text)
  123. byLabel "Some Label" "12345"
  124. fileByLabel "Some File" "test/main.hs" "text/plain"
  125. addToken
  126. statusIs 200
  127. bodyEquals "12345"
  128. yit "finding html" $ do
  129. get ("/html" :: Text)
  130. statusIs 200
  131. htmlCount "p" 2
  132. htmlAllContain "p" "Hello"
  133. htmlAnyContain "p" "World"
  134. htmlAnyContain "p" "Moon"
  135. htmlNoneContain "p" "Sun"
  136. yit "finds the CSRF token by css selector" $ do
  137. get ("/form" :: Text)
  138. statusIs 200
  139. request $ do
  140. setMethod "POST"
  141. setUrl ("/form" :: Text)
  142. byLabel "Some Label" "12345"
  143. fileByLabel "Some File" "test/main.hs" "text/plain"
  144. addToken_ "body"
  145. statusIs 200
  146. bodyEquals "12345"
  147. ydescribe "utf8 paths" $ do
  148. yit "from path" $ do
  149. get ("/dynamic1/שלום" :: Text)
  150. statusIs 200
  151. bodyEquals "שלום"
  152. yit "from path, type-safe URL" $ do
  153. get $ LiteAppRoute ["dynamic1", "שלום"]
  154. statusIs 200
  155. printBody
  156. bodyEquals "שלום"
  157. yit "from WAI" $ do
  158. get ("/dynamic2/שלום" :: Text)
  159. statusIs 200
  160. bodyEquals "שלום"
  161. ydescribe "labels" $ do
  162. yit "can click checkbox" $ do
  163. get ("/labels" :: Text)
  164. request $ do
  165. setMethod "POST"
  166. setUrl ("/labels" :: Text)
  167. byLabel "Foo Bar" "yes"
  168. ydescribe "Content-Type handling" $ do
  169. yit "can set a content-type" $ do
  170. request $ do
  171. setUrl ("/checkContentType" :: Text)
  172. addRequestHeader ("Expected-Content-Type","text/plain")
  173. addRequestHeader ("Content-Type","text/plain")
  174. statusIs 200
  175. yit "adds the form-urlencoded Content-Type if you add parameters" $ do
  176. request $ do
  177. setUrl ("/checkContentType" :: Text)
  178. addRequestHeader ("Expected-Content-Type","application/x-www-form-urlencoded")
  179. addPostParam "foo" "foobarbaz"
  180. statusIs 200
  181. yit "defaults to no Content-Type" $ do
  182. get ("/checkContentType" :: Text)
  183. statusIs 200
  184. yit "returns a 415 for the wrong Content-Type" $ do
  185. -- Tests that the test handler is functioning
  186. request $ do
  187. setUrl ("/checkContentType" :: Text)
  188. addRequestHeader ("Expected-Content-Type","application/x-www-form-urlencoded")
  189. addRequestHeader ("Content-Type","text/plain")
  190. statusIs 415
  191. describe "cookies" $ yesodSpec cookieApp $ do
  192. yit "should send the cookie #730" $ do
  193. get ("/" :: Text)
  194. statusIs 200
  195. post ("/cookie/foo" :: Text)
  196. statusIs 303
  197. get ("/" :: Text)
  198. statusIs 200
  199. printBody
  200. bodyContains "Foo"
  201. describe "CSRF with cookies/headers" $ yesodSpec CsrfApp $ do
  202. yit "Should receive a CSRF cookie and add its value to the headers" $ do
  203. get ("/" :: Text)
  204. statusIs 200
  205. request $ do
  206. setMethod "POST"
  207. setUrl ("/" :: Text)
  208. addTokenFromCookie
  209. statusIs 200
  210. yit "Should 403 requests if we don't add the CSRF token" $ do
  211. get ("/" :: Text)
  212. statusIs 200
  213. request $ do
  214. setMethod "POST"
  215. setUrl ("/" :: Text)
  216. statusIs 403
  217. describe "test redirects" $ yesodSpec app $ do
  218. yit "follows 303 redirects when requested" $ do
  219. get ("/redirect303" :: Text)
  220. statusIs 303
  221. r <- followRedirect
  222. liftIO $ assertBool "expected a Right from a 303 redirect" $ isRight r
  223. statusIs 200
  224. bodyContains "we have been successfully redirected"
  225. yit "follows 301 redirects when requested" $ do
  226. get ("/redirect301" :: Text)
  227. statusIs 301
  228. r <- followRedirect
  229. liftIO $ assertBool "expected a Right from a 301 redirect" $ isRight r
  230. statusIs 200
  231. bodyContains "we have been successfully redirected"
  232. yit "returns a Left when no redirect was returned" $ do
  233. get ("/" :: Text)
  234. statusIs 200
  235. r <- followRedirect
  236. liftIO $ assertBool "expected a Left when not a redirect" $ isLeft r
  237. instance RenderMessage LiteApp FormMessage where
  238. renderMessage _ _ = defaultFormMessage
  239. app :: LiteApp
  240. app = liteApp $ do
  241. dispatchTo $ do
  242. mfoo <- lookupGetParam "foo"
  243. case mfoo of
  244. Nothing -> return "Hello world!"
  245. Just foo -> return $ "foo=" <> foo
  246. onStatic "dynamic1" $ withDynamic $ \d -> dispatchTo $ return (d :: Text)
  247. onStatic "dynamic2" $ onStatic "שלום" $ dispatchTo $ do
  248. req <- waiRequest
  249. return $ pathInfo req !! 1
  250. onStatic "post" $ dispatchTo $ do
  251. mfoo <- lookupPostParam "foo"
  252. case mfoo of
  253. Nothing -> error "No foo"
  254. Just foo -> return foo
  255. onStatic "redirect301" $ dispatchTo $ redirectWith status301 ("/redirectTarget" :: Text) >> return ()
  256. onStatic "redirect303" $ dispatchTo $ redirectWith status303 ("/redirectTarget" :: Text) >> return ()
  257. onStatic "redirectTarget" $ dispatchTo $ return ("we have been successfully redirected" :: Text)
  258. onStatic "form" $ dispatchTo $ do
  259. ((mfoo, widget), _) <- runFormPost
  260. $ renderDivs
  261. $ (,)
  262. <$> areq textField "Some Label" Nothing
  263. <*> areq fileField "Some File" Nothing
  264. case mfoo of
  265. FormSuccess (foo, _) -> return $ toHtml foo
  266. _ -> defaultLayout widget
  267. onStatic "html" $ dispatchTo $
  268. return ("<html><head><title>Hello</title></head><body><p>Hello World</p><p>Hello Moon</p></body></html>" :: Text)
  269. onStatic "labels" $ dispatchTo $
  270. return ("<html><label><input type='checkbox' name='fooname' id='foobar'>Foo Bar</label></html>" :: Text)
  271. onStatic "checkContentType" $ dispatchTo $ do
  272. headers <- requestHeaders <$> waiRequest
  273. let actual = lookup "Content-Type" headers
  274. expected = lookup "Expected-Content-Type" headers
  275. if actual == expected
  276. then return ()
  277. else sendResponseStatus unsupportedMediaType415 ()
  278. cookieApp :: LiteApp
  279. cookieApp = liteApp $ do
  280. dispatchTo $ fromMaybe "no message available" <$> getMessage
  281. onStatic "cookie" $ do
  282. onStatic "foo" $ dispatchTo $ do
  283. setMessage "Foo"
  284. redirect ("/cookie/home" :: Text)
  285. return ()
  286. data CsrfApp = CsrfApp
  287. mkYesod "CsrfApp" [parseRoutes|
  288. / HomeR GET POST
  289. |]
  290. instance Yesod CsrfApp where
  291. yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
  292. getHomeR :: Handler Html
  293. getHomeR = defaultLayout
  294. [whamlet|
  295. <p>
  296. Welcome to my test application.
  297. |]
  298. postHomeR :: Handler Html
  299. postHomeR = defaultLayout
  300. [whamlet|
  301. <p>
  302. Welcome to my test application.
  303. |]