Add functions to yesod-test to support the new CSRF middleware
This commit is contained in:
parent
33982b2112
commit
374195b4fa
@ -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
|
## 1.4.3.1
|
||||||
|
|
||||||
* Improved README
|
* Improved README
|
||||||
|
|||||||
@ -77,6 +77,8 @@ module Yesod.Test
|
|||||||
, addToken_
|
, addToken_
|
||||||
, addNonce
|
, addNonce
|
||||||
, addNonce_
|
, addNonce_
|
||||||
|
, addTokenFromCookie
|
||||||
|
, addTokenFromCookieNamedToHeaderNamed
|
||||||
|
|
||||||
-- * Assertions
|
-- * Assertions
|
||||||
, assertEqual
|
, assertEqual
|
||||||
@ -93,6 +95,7 @@ module Yesod.Test
|
|||||||
-- * Grab information
|
-- * Grab information
|
||||||
, getTestYesod
|
, getTestYesod
|
||||||
, getResponse
|
, getResponse
|
||||||
|
, getRequestCookies
|
||||||
|
|
||||||
-- * Debug output
|
-- * Debug output
|
||||||
, printBody
|
, printBody
|
||||||
@ -133,6 +136,7 @@ import qualified Data.Map as M
|
|||||||
import qualified Web.Cookie as Cookie
|
import qualified Web.Cookie as Cookie
|
||||||
import qualified Blaze.ByteString.Builder as Builder
|
import qualified Blaze.ByteString.Builder as Builder
|
||||||
import Data.Time.Clock (getCurrentTime)
|
import Data.Time.Clock (getCurrentTime)
|
||||||
|
import Control.Applicative ((<$>))
|
||||||
|
|
||||||
-- | The state used in a single test case defined using 'yit'
|
-- | The state used in a single test case defined using 'yit'
|
||||||
--
|
--
|
||||||
@ -589,6 +593,65 @@ addToken_ scope = do
|
|||||||
addToken :: RequestBuilder site ()
|
addToken :: RequestBuilder site ()
|
||||||
addToken = addToken_ ""
|
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@.
|
-- | Perform a POST request to @url@.
|
||||||
--
|
--
|
||||||
-- ==== __Examples__
|
-- ==== __Examples__
|
||||||
@ -759,7 +822,7 @@ request reqBuilder = do
|
|||||||
{ httpVersion = H.http11
|
{ httpVersion = H.http11
|
||||||
}
|
}
|
||||||
}) app
|
}) 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
|
cookies' = M.fromList [(Cookie.setCookieName c, c) | c <- newCookies] `M.union` cookies
|
||||||
ST.put $ YesodExampleData app site cookies' (Just response)
|
ST.put $ YesodExampleData app site cookies' (Just response)
|
||||||
where
|
where
|
||||||
@ -846,6 +909,10 @@ request reqBuilder = do
|
|||||||
, queryString = urlQuery
|
, queryString = urlQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
parseSetCookies :: [H.Header] -> [Cookie.SetCookie]
|
||||||
|
parseSetCookies headers = map (Cookie.parseSetCookie . snd) $ DL.filter (("Set-Cookie"==) . fst) $ headers
|
||||||
|
|
||||||
-- Yes, just a shortcut
|
-- Yes, just a shortcut
|
||||||
failure :: (MonadIO a) => T.Text -> a b
|
failure :: (MonadIO a) => T.Text -> a b
|
||||||
failure reason = (liftIO $ HUnit.assertFailure $ T.unpack reason) >> error ""
|
failure reason = (liftIO $ HUnit.assertFailure $ T.unpack reason) >> error ""
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
|
||||||
import Test.HUnit hiding (Test)
|
import Test.HUnit hiding (Test)
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
@ -166,6 +170,26 @@ main = hspec $ do
|
|||||||
statusIs 200
|
statusIs 200
|
||||||
printBody
|
printBody
|
||||||
bodyContains "Foo"
|
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
|
instance RenderMessage LiteApp FormMessage where
|
||||||
renderMessage _ _ = defaultFormMessage
|
renderMessage _ _ = defaultFormMessage
|
||||||
@ -210,3 +234,26 @@ cookieApp = liteApp $ do
|
|||||||
setMessage "Foo"
|
setMessage "Foo"
|
||||||
redirect ("/cookie/home" :: Text)
|
redirect ("/cookie/home" :: Text)
|
||||||
return ()
|
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.
|
||||||
|
|]
|
||||||
@ -37,7 +37,7 @@ library
|
|||||||
, time
|
, time
|
||||||
, blaze-builder
|
, blaze-builder
|
||||||
, cookie
|
, cookie
|
||||||
, yesod-core >= 1.4
|
, yesod-core >= 1.4.14
|
||||||
|
|
||||||
exposed-modules: Yesod.Test
|
exposed-modules: Yesod.Test
|
||||||
Yesod.Test.CssQuery
|
Yesod.Test.CssQuery
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user