From cb4785cf28f41224e3bc2bd8ca3be1765899eb18 Mon Sep 17 00:00:00 2001 From: Maximilian Tagher Date: Sun, 25 Jan 2015 15:20:25 -0800 Subject: [PATCH] Document much of yesod-test, especially the "Making Requests" section --- yesod-test/README.md | 2 +- yesod-test/Yesod/Test.hs | 205 ++++++++++++++++++++++++++++++++------- 2 files changed, 171 insertions(+), 36 deletions(-) diff --git a/yesod-test/README.md b/yesod-test/README.md index 1567d028..f17be004 100644 --- a/yesod-test/README.md +++ b/yesod-test/README.md @@ -10,7 +10,7 @@ using css selectors. You can also easily build requests using forms present in the current page. This is very useful for testing web applications built in yesod for example, -were your forms may have field names generated by the framework or a randomly +where your forms may have field names generated by the framework or a randomly generated "\_token" field. Your database is also directly available so you can use runDB to set up diff --git a/yesod-test/Yesod/Test.hs b/yesod-test/Yesod/Test.hs index 6a7e1b46..74dff8d9 100644 --- a/yesod-test/Yesod/Test.hs +++ b/yesod-test/Yesod/Test.hs @@ -8,19 +8,19 @@ Yesod.Test is a pragmatic framework for testing web applications built using wai and persistent. -By pragmatic I may also mean 'dirty'. It's main goal is to encourage integration +By pragmatic I may also mean 'dirty'. Its main goal is to encourage integration and system testing of web applications by making everything /easy to test/. Your tests are like browser sessions that keep track of cookies and the last visited page. You can perform assertions on the content of HTML responses, -using css selectors to explore the document more easily. +using CSS selectors to explore the document more easily. You can also easily build requests using forms present in the current page. -This is very useful for testing web applications built in yesod for example, -were your forms may have field names generated by the framework or a randomly -generated '_nonce' field. +This is very useful for testing web applications built in yesod, for example, +where your forms may have field names generated by the framework or a randomly +generated nonce value. -Your database is also directly available so you can use runDBRunner to set up +Your database is also directly available so you can use 'runDB' to set up backend pre-conditions, or to assert that your session is having the desired effect. -} @@ -38,12 +38,12 @@ module Yesod.Test , yit -- * Making requests - -- | To make a request you need to point to an url and pass in some parameters. - -- - -- To build your parameters you will use the RequestBuilder monad that lets you - -- add values, add files, lookup fields by label and find the current - -- nonce value and add it to your request too. + -- | You can construct requests with the 'RequestBuilder' monad, which lets you + -- set the URL and add parameters, headers, and files. Helper functions are provided to + -- lookup fields by label and to add the current nonce value from your forms. + -- Once built, the request can be executed with the 'request' method. -- + -- Convenience functions like 'get' and 'post' build and execute common requests. , get , post , postBody @@ -57,16 +57,22 @@ module Yesod.Test , RequestBuilder , setUrl - -- | Yesod can auto generate field ids, so you are never sure what - -- the argument name should be for each one of your args when constructing + -- *** Adding fields by label + -- | Yesod can auto generate field names, so you are never sure what + -- the argument name should be for each one of your inputs when constructing -- your requests. What you do know is the /label/ of the field. -- These functions let you add parameters to your request based -- on currently displayed label names. , byLabel , fileByLabel - -- | Does the current form have a _nonce? Use any of these to add it to your - -- request parameters. + -- *** Nonces + -- | In order to prevent CSRF exploits, yesod-form adds a hidden input + -- to your forms with the name "_token". This token is a randomly generated, + -- per-session value called a /nonce/. + -- + -- In order to prevent your forms from being rejected in tests, use one of + -- these functions to add the nonce to your request. , addNonce , addNonce_ @@ -188,7 +194,7 @@ data RequestPart = ReqKvPart T.Text T.Text | ReqFilePart T.Text FilePath BSL8.ByteString T.Text --- | The RequestBuilder state monad constructs an url encoded string of arguments +-- | The 'RequestBuilder' state monad constructs a URL encoded string of arguments -- to send with your requests. Some of the functions that run on it use the current -- response to analyze the forms that the server is expecting to receive. type RequestBuilder site = ST.StateT (RequestBuilderData site) IO @@ -274,12 +280,12 @@ withResponse' getter f = maybe err f . getter =<< ST.get withResponse :: (SResponse -> YesodExample site a) -> YesodExample site a withResponse = withResponse' yedResponse --- | Use HXT to parse a value from an html tag. +-- | Use HXT to parse a value from an HTML tag. -- Check for usage examples in this module's source. parseHTML :: HtmlLBS -> Cursor parseHTML html = fromDocument $ HD.parseLBS html --- | Query the last response using css selectors, returns a list of matched fragments +-- | Query the last response using CSS selectors, returns a list of matched fragments htmlQuery' :: MonadIO m => (state -> Maybe SResponse) -> Query @@ -289,7 +295,7 @@ htmlQuery' getter query = withResponse' getter $ \ res -> Left err -> failure $ query <> " did not parse: " <> T.pack (show err) Right matches -> return $ map (encodeUtf8 . TL.pack) matches --- | Query the last response using css selectors, returns a list of matched fragments +-- | Query the last response using CSS selectors, returns a list of matched fragments htmlQuery :: Query -> YesodExample site [HtmlLBS] htmlQuery = htmlQuery' yedResponse @@ -354,7 +360,7 @@ bodyContains text = withResponse $ \ res -> contains :: BSL8.ByteString -> String -> Bool contains a b = DL.isInfixOf b (TL.unpack $ decodeUtf8 a) --- | Queries the html using a css selector, and all matched elements must contain +-- | Queries the HTML using a CSS selector, and all matched elements must contain -- the given string. htmlAllContain :: Query -> String -> YesodExample site () htmlAllContain query search = do @@ -364,7 +370,7 @@ htmlAllContain query search = do _ -> liftIO $ HUnit.assertBool ("Not all "++T.unpack query++" contain "++search) $ DL.all (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches) --- | Queries the html using a css selector, and passes if any matched +-- | Queries the HTML using a CSS selector, and passes if any matched -- element contains the given string. -- -- Since 0.3.5 @@ -376,7 +382,7 @@ htmlAnyContain query search = do _ -> liftIO $ HUnit.assertBool ("None of "++T.unpack query++" contain "++search) $ DL.any (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches) --- | Queries the html using a css selector, and fails if any matched +-- | Queries the HTML using a CSS selector, and fails if any matched -- element contains the given string (in other words, it is the logical -- inverse of htmlAnyContains). -- @@ -389,7 +395,7 @@ htmlNoneContain query search = do found -> failure $ "Found " <> T.pack (show $ length found) <> " instances of " <> T.pack search <> " in " <> query <> " elements" --- | Performs a css query on the last response and asserts the matched elements +-- | Performs a CSS query on the last response and asserts the matched elements -- are as many as expected. htmlCount :: Query -> Int -> YesodExample site () htmlCount query count = do @@ -408,7 +414,7 @@ printMatches query = do matches <- htmlQuery query liftIO $ hPutStrLn stderr $ show matches --- | Add a parameter with the given name and value. +-- | Add a parameter with the given name and value to the request body. addPostParam :: T.Text -> T.Text -> RequestBuilder site () addPostParam name value = ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) } @@ -416,16 +422,25 @@ addPostParam name value = addPostData (MultipleItemsPostData posts) = MultipleItemsPostData $ ReqKvPart name value : posts +-- | Add a parameter with the given name and value to the query string. addGetParam :: T.Text -> T.Text -> RequestBuilder site () addGetParam name value = ST.modify $ \rbd -> rbd { rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value) : rbdGets rbd } --- | Add a file to be posted with the current request +-- | Add a file to be posted with the current request. -- --- Adding a file will automatically change your request content-type to be multipart/form-data -addFile :: T.Text -> FilePath -> T.Text -> RequestBuilder site () +-- Adding a file will automatically change your request content-type to be multipart/form-data. +-- +-- ==== __Examples__ +-- +-- > request $ do +-- > addFile "profile_picture" "static/img/picture.png" "img/png" +addFile :: T.Text -- ^ The parameter name for the file. + -> FilePath -- ^ The path to the file. + -> T.Text -- ^ The MIME type of the file, e.g. "image/png". + -> RequestBuilder site () addFile name path mimetype = do contents <- liftIO $ BSL8.readFile path ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd) contents) } @@ -476,18 +491,75 @@ nameFromLabel label = do (<>) :: T.Text -> T.Text -> T.Text (<>) = T.append -byLabel :: T.Text -> T.Text -> RequestBuilder site () +-- How does this work for the alternate syntax? + +-- | Finds the @\