-
Notifications
You must be signed in to change notification settings - Fork 374
Performing Authentication during Testing
[WARNING] Yesod Cookbook has moved to a new place. Please contribute there.
This article is now updated so that the code works with recent scaffolding and libraries. It certainly assumes the scaffolding is generated by yesod-bin-1.4.1
or later, and this code has been tested with a fresh site generated by yesod-bin-1.4.3.11
.
When testing a real application using Yesod.Test
, each test case may need to perform a login. This is not difficult, but as it involves several steps, it is a little tedious to work through the details.
Here is some example code. It is designed around the user/password authentication provided by Yesod.Auth.HashDB
, but can probably be used as a model for other authentication schemes.
The idea is to be able write test specs which go like this:
module Handler.HomeSpec (spec) where
import TestImport
import TestTools
spec :: Spec
spec = withApp $ do
it "requires login" $ do
needsLogin GET ("/" :: Text)
it "loads the index and checks it looks right" $ do
doLogin "testuser" "testpassword"
get HomeR
statusIs 200
htmlAllContain "h1" "Welcome to Yesod"
Notice that doLogin
must appear in every test item which requires authentication, since the session cookie is not preserved between tests.
A few things may need changing for your particular usage:
- The testRoot, which would normally be the same as the approot in your
test-settings.yml
file (orsettings.yml
iftest-settings.yml
does not override it). - The "Login" string used to confirm we have reached the login page.
- Decide how to set up the user in the database. Recent scaffolding (
yesod-bin-1.4.3.4
or later) includes code inTestImport.hs
to wipe the database between tests. You either need to remove that code, so that you can set up a user entry by hand, or set the user and password in the code at the start of every test which relies on authentication!
So with those caveats, here we are. Use it as you wish!
module TestTools (
assertFailure,
urlPath,
needsLogin,
doLogin,
StdMethod(..)
) where
import TestImport
import Yesod.Core (RedirectUrl)
import Network.URI (URI(uriPath), parseURI)
import Network.HTTP.Types (StdMethod(..), renderStdMethod, Status(..))
import Network.Wai.Test (SResponse(..))
-- Adjust as necessary to the url prefix in the Testing configuration
testRoot :: ByteString
testRoot = "http://localhost:3000"
-- Adjust as necessary for the expected path part of the URL after login
afterLogin :: ByteString
afterLogin = "/"
-- Force failure by swearing that black is white, and pigs can fly...
assertFailure :: String -> YesodExample App ()
assertFailure msg = assertEqual msg True False
-- Convert an absolute URL (eg extracted from responses) to just the path
-- for use in test requests.
urlPath :: Text -> Text
urlPath = pack . maybe "" uriPath . parseURI . unpack
-- Internal use only - actual urls are ascii, so exact encoding is irrelevant
urlPathB :: ByteString -> Text
urlPathB = urlPath . decodeUtf8
-- Stages in login process, used below
firstRedirect :: RedirectUrl App url =>
StdMethod -> url -> YesodExample App (Maybe ByteString)
firstRedirect method url = do
request $ do
setMethod $ renderStdMethod method
setUrl url
extractLocation -- We should get redirected to the login page
assertLoginPage :: ByteString -> YesodExample App ()
assertLoginPage loc = do
assertEqual "correct login redirection location"
(testRoot ++ "/auth/login") loc
get $ urlPathB loc
statusIs 200
bodyContains "Login"
submitLogin :: Text -> Text -> YesodExample App (Maybe ByteString)
submitLogin user pass = do
-- Ideally we would extract this url from the login form on the current page
request $ do
setMethod "POST"
setUrl $ urlPathB $ testRoot ++ "/auth/page/hashdb/login"
addPostParam "username" user
addPostParam "password" pass
extractLocation -- Successful login should redirect to the home page
extractLocation :: YesodExample App (Maybe ByteString)
extractLocation = do
withResponse ( \ SResponse { simpleStatus = s, simpleHeaders = h } -> do
let code = statusCode s
assertEqual ("Expected a 302 or 303 redirection status "
++ "but received " ++ show code)
(code `oelem` [302,303])
True
return $ lookup "Location" h
)
-- Check that accessing the url with the given method requires login, and
-- that it redirects us to what looks like the login page. Note that this is
-- *not* an ajax request, whatever the method, so the redirection *should*
-- result in the HTML login page.
--
needsLogin :: RedirectUrl App url => StdMethod -> url -> YesodExample App ()
needsLogin method url = do
mbloc <- firstRedirect method url
maybe (assertFailure "Should have location header") assertLoginPage mbloc
-- Do a login (using hashdb auth). This just attempts to go to the home
-- url, and follows through the login process. It should probably be the
-- first thing in each "it" spec.
--
doLogin :: Text -> Text -> YesodExample App ()
doLogin user pass = do
mbloc <- firstRedirect GET $ urlPathB testRoot
maybe (assertFailure "Should have location header") assertLoginPage mbloc
mbloc2 <- submitLogin user pass
maybe (assertFailure "Should have second location header")
(assertEqual "Check after-login redirection" $ testRoot ++ afterLogin)
mbloc2