Add functions to yesod-test to support the new CSRF middleware

This commit is contained in:
Maximilian Tagher 2015-08-19 21:36:18 -07:00
parent 33982b2112
commit 374195b4fa
4 changed files with 121 additions and 2 deletions

View File

@ -1,3 +1,8 @@
## 1.4.3.2
* Add `addTokenFromCookie` and `addTokenFromCookieNamedToHeaderNamed`, which support the new CSRF token middleware [#1058](https://github.com/yesodweb/yesod/pull/1058)
* Add `getRequestCookies`, which returns the cookies from the most recent request [#1058](https://github.com/yesodweb/yesod/pull/1058)
## 1.4.3.1
* Improved README

View File

@ -77,6 +77,8 @@ module Yesod.Test
, addToken_
, addNonce
, addNonce_
, addTokenFromCookie
, addTokenFromCookieNamedToHeaderNamed
-- * Assertions
, assertEqual
@ -93,6 +95,7 @@ module Yesod.Test
-- * Grab information
, getTestYesod
, getResponse
, getRequestCookies
-- * Debug output
, printBody
@ -133,6 +136,7 @@ import qualified Data.Map as M
import qualified Web.Cookie as Cookie
import qualified Blaze.ByteString.Builder as Builder
import Data.Time.Clock (getCurrentTime)
import Control.Applicative ((<$>))
-- | The state used in a single test case defined using 'yit'
--
@ -589,6 +593,65 @@ addToken_ scope = do
addToken :: RequestBuilder site ()
addToken = addToken_ ""
-- | Calls 'addTokenFromCookieNamedToHeaderNamed' with the 'defaultCsrfCookieName' and 'defaultCsrfHeaderName'.
--
-- Use this function if you're using the CSRF middleware from "Yesod.Core" and haven't customized the cookie or header name.
--
-- ==== __Examples__
--
-- > request $ do
-- > addTokenFromCookie
--
-- Since 1.4.3.2
addTokenFromCookie :: RequestBuilder site ()
addTokenFromCookie = addTokenFromCookieNamedToHeaderNamed defaultCsrfCookieName defaultCsrfHeaderName
-- | Looks up the CSRF token stored in the cookie with the given name and adds it to the request headers. An error is thrown if the cookie can't be found.
--
-- Use this function if you're using the CSRF middleware from "Yesod.Core" and have customized the cookie or header name.
--
-- See "Yesod.Core.Handler" for details on this approach to CSRF protection.
--
-- ==== __Examples__
--
-- > import Data.CaseInsensitive (CI)
-- > request $ do
-- > addTokenFromCookieNamedToHeaderNamed "cookieName" (CI "headerName")
--
-- Since 1.4.3.2
addTokenFromCookieNamedToHeaderNamed :: ByteString -- ^ The name of the cookie
-> CI ByteString -- ^ The name of the header
-> RequestBuilder site ()
addTokenFromCookieNamedToHeaderNamed cookieName headerName = do
cookies <- getRequestCookies
case M.lookup cookieName cookies of
Just csrfCookie -> addRequestHeader (headerName, Cookie.setCookieValue csrfCookie)
Nothing -> failure $ T.concat
[ "addTokenFromCookieNamedToHeaderNamed failed to lookup CSRF cookie with name: "
, T.pack $ show cookieName
, ". Cookies were: "
, T.pack $ show cookies
]
-- | Returns the 'Cookies' from the most recent request. If a request hasn't been made, an error is raised.
--
-- ==== __Examples__
--
-- > request $ do
-- > cookies <- getRequestCookies
-- > liftIO $ putStrLn $ "Cookies are: " ++ show cookies
--
-- Since 1.4.3.2
getRequestCookies :: RequestBuilder site Cookies
getRequestCookies = do
requestBuilderData <- ST.get
headers <- case simpleHeaders <$> rbdResponse requestBuilderData of
Just h -> return h
Nothing -> failure "getRequestCookies: No request has been made yet; the cookies can't be looked up."
return $ M.fromList $ map (\c -> (Cookie.setCookieName c, c)) (parseSetCookies headers)
-- | Perform a POST request to @url@.
--
-- ==== __Examples__
@ -759,7 +822,7 @@ request reqBuilder = do
{ httpVersion = H.http11
}
}) app
let newCookies = map (Cookie.parseSetCookie . snd) $ DL.filter (("Set-Cookie"==) . fst) $ simpleHeaders response
let newCookies = parseSetCookies $ simpleHeaders response
cookies' = M.fromList [(Cookie.setCookieName c, c) | c <- newCookies] `M.union` cookies
ST.put $ YesodExampleData app site cookies' (Just response)
where
@ -846,6 +909,10 @@ request reqBuilder = do
, queryString = urlQuery
}
parseSetCookies :: [H.Header] -> [Cookie.SetCookie]
parseSetCookies headers = map (Cookie.parseSetCookie . snd) $ DL.filter (("Set-Cookie"==) . fst) $ headers
-- Yes, just a shortcut
failure :: (MonadIO a) => T.Text -> a b
failure reason = (liftIO $ HUnit.assertFailure $ T.unpack reason) >> error ""

View File

@ -2,6 +2,10 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}
import Test.HUnit hiding (Test)
import Test.Hspec
@ -166,6 +170,26 @@ main = hspec $ do
statusIs 200
printBody
bodyContains "Foo"
describe "CSRF with cookies/headers" $ yesodSpec CsrfApp $ do
yit "Should receive a CSRF cookie and add its value to the headers" $ do
get ("/" :: Text)
statusIs 200
request $ do
setMethod "POST"
setUrl ("/" :: Text)
addTokenFromCookie
statusIs 200
yit "Should 403 requests if we don't add the CSRF token" $ do
get ("/" :: Text)
statusIs 200
request $ do
setMethod "POST"
setUrl ("/" :: Text)
statusIs 403
instance RenderMessage LiteApp FormMessage where
renderMessage _ _ = defaultFormMessage
@ -210,3 +234,26 @@ cookieApp = liteApp $ do
setMessage "Foo"
redirect ("/cookie/home" :: Text)
return ()
data CsrfApp = CsrfApp
mkYesod "CsrfApp" [parseRoutes|
/ HomeR GET POST
|]
instance Yesod CsrfApp where
yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware
getHomeR :: Handler Html
getHomeR = defaultLayout
[whamlet|
<p>
Welcome to my test application.
|]
postHomeR :: Handler Html
postHomeR = defaultLayout
[whamlet|
<p>
Welcome to my test application.
|]

View File

@ -37,7 +37,7 @@ library
, time
, blaze-builder
, cookie
, yesod-core >= 1.4
, yesod-core >= 1.4.14
exposed-modules: Yesod.Test
Yesod.Test.CssQuery