yesod-test 1.4.2.2 → 1.4.3
raw patch · 4 files changed
+198/−46 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Yesod.Test: addToken :: RequestBuilder site ()
+ Yesod.Test: addToken_ :: Query -> RequestBuilder site ()
Files
- README.md +2/−2
- Yesod/Test.hs +194/−42
- test/main.hs +1/−1
- yesod-test.cabal +1/−1
README.md view
@@ -10,7 +10,7 @@ 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 @@ -50,7 +50,7 @@ -- Performs the post using the current page to extract field values: post "url/to/post/to" $ do - addNonce -- Add the _nonce field with the currently shown value + addToken -- Add the CSRF _token field with the currently shown value -- Lookup field by the text on the labels pointing to them. byLabel "Email:" "gustavo@cerati.com"
Yesod/Test.hs view
@@ -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 CSRF token input. -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 @@ , 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 CSRF token 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,24 @@ , 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.+ -- *** CSRF Tokens+ -- | 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.+ --+ -- In order to prevent your forms from being rejected in tests, use one of+ -- these functions to add the token to your request.+ , addToken+ , addToken_ , addNonce , addNonce_ @@ -188,7 +196,7 @@ = 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 +282,12 @@ 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 +297,7 @@ 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 +362,7 @@ 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 +372,7 @@ _ -> 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 +384,7 @@ _ -> 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 +397,7 @@ 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 +416,7 @@ 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 +424,25 @@ 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,31 +493,107 @@ (<>) :: T.Text -> T.Text -> T.Text (<>) = T.append -byLabel :: T.Text -> T.Text -> RequestBuilder site ()+-- How does this work for the alternate <label><input></label> syntax?++-- | Finds the @\<label>@ with the given value, finds its corresponding @\<input>@, then adds a parameter+-- for that input to the request body.+--+-- ==== __Examples__+--+-- Given this HTML, we want to submit @f1=Michael@ to the server:+--+-- > <form method="POST">+-- > <label for="user">Username</label>+-- > <input id="user" name="f1" />+-- > </form>+--+-- You can set this parameter like so:+--+-- > request $ do+-- > byLabel "Username" "Michael"+--+-- This function also supports the implicit label syntax, in which+-- the @\<input>@ is nested inside the @\<label>@ rather than specified with @for@:+--+-- > <form method="POST">+-- > <label>Username <input name="f1"> </label>+-- > </form>+byLabel :: T.Text -- ^ The text contained in the @\<label>@.+ -> T.Text -- ^ The value to set the parameter to.+ -> RequestBuilder site () byLabel label value = do name <- nameFromLabel label addPostParam name value -fileByLabel :: T.Text -> FilePath -> T.Text -> RequestBuilder site ()+-- | Finds the @\<label>@ with the given value, finds its corresponding @\<input>@, then adds a file for that input to the request body.+--+-- ==== __Examples__+--+-- Given this HTML, we want to submit a file with the parameter name @f1@ to the server:+--+-- > <form method="POST">+-- > <label for="imageInput">Please submit an image</label>+-- > <input id="imageInput" type="file" name="f1" accept="image/*">+-- > </form>+--+-- You can set this parameter like so:+--+-- > request $ do +-- > fileByLabel "Please submit an image" "static/img/picture.png" "img/png"+--+-- This function also supports the implicit label syntax, in which+-- the @\<input>@ is nested inside the @\<label>@ rather than specified with @for@:+--+-- > <form method="POST">+-- > <label>Please submit an image <input type="file" name="f1"> </label>+-- > </form>+fileByLabel :: T.Text -- ^ The text contained in the @\<label>@.+ -> FilePath -- ^ The path to the file.+ -> T.Text -- ^ The MIME type of the file, e.g. "image/png".+ -> RequestBuilder site () fileByLabel label path mime = do name <- nameFromLabel label addFile name path mime --- | Lookup a _nonce form field and add it's value to the params.--- Receives a CSS selector that should resolve to the form element containing the nonce.+-- | An alias for 'addToken_'. addNonce_ :: Query -> RequestBuilder site ()-addNonce_ scope = do+addNonce_ = addToken_+{-# DEPRECATED addNonce_ "Use 'addToken_' instead; 'addNonce_' will be removed in the next major version. Reasoning: Yesod's CSRF tokens are not actually nonces (one-time values), so yesod-form moved to calling them tokens instead. yesod-test is now using the word token as well. See https://github.com/yesodweb/yesod/issues/914 for details." #-}++-- | An alias for 'addToken'.+addNonce :: RequestBuilder site ()+addNonce = addToken+{-# DEPRECATED addNonce "Use 'addToken' instead; 'addNonce' will be removed in the next major version. Reasoning: Yesod's CSRF tokens are not actually nonces (one-time values), so yesod-form moved to calling them tokens instead. yesod-test is now using the word token as well. See https://github.com/yesodweb/yesod/issues/914 for details." #-}++-- | Lookups the hidden input named "_token" and adds its value to the params.+-- Receives a CSS selector that should resolve to the form element containing the token.+--+-- ==== __Examples__+--+-- > request $ do+-- > addToken_ "#formID"+addToken_ :: Query -> RequestBuilder site ()+addToken_ scope = do matches <- htmlQuery' rbdResponse $ scope <> "input[name=_token][type=hidden][value]" case matches of- [] -> failure $ "No nonce found in the current page"+ [] -> failure $ "No CSRF token found in the current page" element:[] -> addPostParam "_token" $ head $ attribute "value" $ parseHTML element- _ -> failure $ "More than one nonce found in the page"+ _ -> failure $ "More than one CSRF token found in the page" --- | For responses that display a single form, just lookup the only nonce available.-addNonce :: RequestBuilder site ()-addNonce = addNonce_ ""+-- | For responses that display a single form, just lookup the only CSRF token available.+--+-- ==== __Examples__+--+-- > request $ do+-- > addToken+addToken :: RequestBuilder site ()+addToken = addToken_ "" --- | Perform a POST request to url+-- | Perform a POST request to @url@.+--+-- ==== __Examples__+--+-- > post HomeR post :: (Yesod site, RedirectUrl site url) => url -> YesodExample site ()@@ -508,7 +601,14 @@ setMethod "POST" setUrl url --- | Perform a POST request to url with sending a body into it.+-- | Perform a POST request to @url@ with the given body.+--+-- ==== __Examples__+--+-- > postBody HomeR "foobar"+--+-- > import Data.Aeson+-- > postBody HomeR (encode $ object ["age" .= (1 :: Integer)]) postBody :: (Yesod site, RedirectUrl site url) => url -> BSL8.ByteString@@ -518,7 +618,13 @@ setUrl url setRequestBody body --- | Perform a GET request to url, using params+-- | Perform a GET request to @url@.+--+-- ==== __Examples__+--+-- > get HomeR+--+-- > get ("http://google.com" :: Text) get :: (Yesod site, RedirectUrl site url) => url -> YesodExample site ()@@ -526,9 +632,28 @@ setMethod "GET" setUrl url +-- | Sets the HTTP method used by the request.+--+-- ==== __Examples__+--+-- > request $ do+-- > setMethod "POST"+--+-- > import Network.HTTP.Types.Method+-- > request $ do+-- > setMethod methodPut setMethod :: H.Method -> RequestBuilder site () setMethod m = ST.modify $ \rbd -> rbd { rbdMethod = m } +-- | Sets the URL used by the request.+--+-- ==== __Examples__+--+-- > request $ do+-- > setUrl HomeR+--+-- > request $ do+-- > setUrl ("http://google.com/" :: Text) setUrl :: (Yesod site, RedirectUrl site url) => url -> RequestBuilder site ()@@ -551,18 +676,45 @@ } -- | Simple way to set HTTP request body+--+-- ==== __ Examples__+--+-- > request $ do+-- > setRequestBody "foobar"+--+-- > import Data.Aeson+-- > request $ do+-- > setRequestBody $ encode $ object ["age" .= (1 :: Integer)] setRequestBody :: (Yesod site) => BSL8.ByteString -> RequestBuilder site () setRequestBody body = ST.modify $ \rbd -> rbd { rbdPostData = BinaryPostData body } +-- | Adds the given header to the request; see "Network.HTTP.Types.Header" for creating 'Header's.+--+-- ==== __Examples__+--+-- > import Network.HTTP.Types.Header+-- > request $ do+-- > addRequestHeader (hUserAgent, "Chrome/41.0.2228.0") addRequestHeader :: H.Header -> RequestBuilder site () addRequestHeader header = ST.modify $ \rbd -> rbd { rbdHeaders = header : rbdHeaders rbd } --- | General interface to performing requests, allowing you to add extra--- headers as well as letting you specify the request method.+-- | The general interface for performing requests. 'request' takes a 'RequestBuilder',+-- constructs a request, and executes it.+--+-- The 'RequestBuilder' allows you to build up attributes of the request, like the+-- headers, parameters, and URL of the request.+--+-- ==== __Examples__+--+-- > request $ do+-- > addToken+-- > byLabel "First Name" "Felipe"+-- > setMethod "PUT"+-- > setUrl NameR request :: Yesod site => RequestBuilder site () -> YesodExample site ()
test/main.hs view
@@ -122,7 +122,7 @@ setUrl ("/form" :: Text) byLabel "Some Label" "12345" fileByLabel "Some File" "test/main.hs" "text/plain"- addNonce+ addToken statusIs 200 bodyEquals "12345" yit "finding html" $ do
yesod-test.cabal view
@@ -1,5 +1,5 @@ name: yesod-test-version: 1.4.2.2+version: 1.4.3 license: MIT license-file: LICENSE author: Nubis <nubis@woobiz.com.ar>