yesod-session-persist-0.0.0.0: tests/Yesod/Session/Persist/YesodSpec.hs
module Yesod.Session.Persist.YesodSpec
( spec
) where
import Test.Prelude
import Control.Concurrent.STM.TVar (readTVarIO)
import Data.Aeson (encode, object)
import Data.List qualified as List
import Data.Sequence qualified as Seq
import Data.Text.Encoding (encodeUtf8)
import Network.Wai (Middleware)
import Network.Wai.Test (simpleHeaders)
import Time
import Web.Cookie (SetCookie (..), def, parseSetCookie)
import Yesod.Session.Persist
import Yesod.Session.Persist.YesodApp (App (..), Route (..), newApp)
import Yesod.Session.Storage.Operation
import Yesod.Test
( YesodExample
, bodyEquals
, getTestYesod
, request
, setMethod
, setRequestBody
, setUrl
, withResponse
)
spec :: Spec
spec =
withApp defaultTimingOptions $ do
context "Yesod App" $ do
specify @(YesodExample App ()) "Sets a session cookie" $ do
app <- getTestYesod
now <- liftIO $ readTVarIO app.mock.currentTime
-- Make a request to a normal route
request $ do setUrl HomeR; setMethod "GET"
-- Since there was no session, one should be inserted
sessionKey :: SessionKey <- do
transcript <- liftIO $ takeTranscript app.mock.mockStorage
case toList transcript of
[StorageOperation' (InsertSession s)] -> pure s.key
_ -> liftIO $ fail $ show transcript
-- We should receive a set-cookie header
assertSetCookie
$ def
{ setCookieName = "session-key"
, setCookieValue = encodeUtf8 sessionKey.text
, setCookiePath = Just "/"
, setCookieExpires =
Just
$ truncateToSeconds
$ addUTCTime (60 * 60 * 8) now
, setCookieHttpOnly = True
}
specify @(YesodExample App ())
"Doesn't set a session cookie if sessions are disabled"
$ do
app <- getTestYesod
-- Make a request to a route without session management
request $ do setUrl PingR; setMethod "GET"
-- No storage operations should have been performed
transcript <- liftIO $ takeTranscript app.mock.mockStorage
liftIO $ transcript `shouldBe` Seq.empty
-- No cookie should be set
assertNoSetCookie
specify @(YesodExample App ()) "Saves the session"
$ do
app <- getTestYesod
-- Log in
request $ do
setUrl LogInR
setMethod "POST"
setRequestBody $ encode $ object [("uid", "xyz")]
sessionKey :: SessionKey <- do
transcript <- liftIO $ takeTranscript app.mock.mockStorage
case toList transcript of
[StorageOperation' (InsertSession s)] -> pure s.key
_ -> liftIO $ fail $ show transcript
replicateM_ 3 $ do
-- A short pause should not affect anything
advanceTime 90 app.mock
-- Verify that we're now logged in
request $ do setUrl UserR; setMethod "GET"
bodyEquals (show @ByteString "xyz")
liftIO $ do
transcript <- takeTranscript app.mock.mockStorage
toList transcript
`shouldBe` [StorageOperation' (GetSession sessionKey)]
specify @(YesodExample App ()) "Does not load an expired session"
$ do
app <- getTestYesod
request $ do
setUrl LogInR
setMethod "POST"
setRequestBody $ encode $ object [("uid", "xyz")]
-- A pause longer than the idle timeout should kill the session
advanceTime (60 * 60 * 10) app.mock
request $ do setUrl UserR; setMethod "GET"
bodyEquals "-"
specify @(YesodExample App ()) "rotates the key when 'rotateSessionKey' is used" $ do
app <- getTestYesod
-- Log in
request $ do
setUrl LogInR
setMethod "POST"
setRequestBody $ encode $ object [("uid", "xyz")]
-- Get the session
transcript <- liftIO $ takeTranscript app.mock.mockStorage
sessionKey :: SessionKey <-
case toList transcript of
[StorageOperation' (InsertSession s)] -> pure s.key
_ -> liftIO $ fail $ show transcript
-- Make a request to the route that does a key rotation
request $ do setUrl RotateR; setMethod "GET"
-- The old session should be deleted
transcript' <- liftIO $ takeTranscript app.mock.mockStorage
liftIO
$ List.take 2 (toList transcript')
`shouldBe` [ StorageOperation' (GetSession sessionKey)
, StorageOperation' (DeleteSession sessionKey)
]
-- But we're still logged in
request $ do setUrl UserR; setMethod "GET"
bodyEquals (show @ByteString "xyz")
specify @(YesodExample App ()) "rotates the key on auth changes"
$ do
app <- getTestYesod
-- Log in
request $ do
setUrl LogInR
setMethod "POST"
setRequestBody $ encode $ object [("uid", "xyz")]
-- Get the session
transcript <- liftIO $ takeTranscript app.mock.mockStorage
sessionKey :: SessionKey <-
case toList transcript of
[StorageOperation' (InsertSession s)] -> pure s.key
_ -> liftIO $ fail $ show transcript
-- Log in differently
request $ do
setUrl LogInR
setMethod "POST"
setRequestBody $ encode $ object [("uid", "hello")]
-- The old session should be deleted
transcript' <- liftIO $ takeTranscript app.mock.mockStorage
liftIO
$ List.take 2 (toList transcript')
`shouldBe` [ StorageOperation' (GetSession sessionKey)
, StorageOperation' (DeleteSession sessionKey)
]
-- But we're still logged in as the new user
request $ do setUrl UserR; setMethod "GET"
bodyEquals (show @ByteString "hello")
withApp :: TimingOptions NominalDiffTime -> SpecWith (App, Middleware) -> Spec
withApp timing = around ((=<< newApp timing) . (. (,id :: Middleware)))
-- | Assert that the response contains a set-cookie header that parses to a particular value
assertSetCookie :: SetCookie -> YesodExample site ()
assertSetCookie expected =
withResponse $ \response ->
case List.lookup "set-cookie" $ simpleHeaders response of
Just value -> liftIO $ parseSetCookie value `shouldBe` expected
Nothing -> liftIO $ expectationFailure "no set-cookie header present"
-- | Fail if set-cookie is present in the response
assertNoSetCookie :: YesodExample site ()
assertNoSetCookie =
withResponse $ \response ->
case List.lookup "set-cookie" $ simpleHeaders response of
Just value ->
liftIO $ expectationFailure $ "expected no set-cookie, but got " <> show value
Nothing -> pure ()
-- | Round a time to seconds, because that is the precision in cookie headers
truncateToSeconds :: UTCTime -> UTCTime
truncateToSeconds x =
x {utctDayTime = secondsToDiffTime (truncate (utctDayTime x))}