packages feed

yesod-session-persist-0.0.0.0: tests/Yesod/Session/Manager/SaveSpec.hs

module Yesod.Session.Manager.SaveSpec
  ( spec
  ) where

import Test.Prelude

import Control.Concurrent.STM.TVar (readTVarIO)
import Control.Monad.State (execState)
import Data.Map.Strict qualified as Map
import Data.Sequence qualified as Seq
import Embedding
import Session.KeyRotation
import Session.Timing.Time
import Yesod.Core (SessionMap)
import Yesod.Session.Embedding.Options
import Yesod.Session.Manager
import Yesod.Session.Manager.Load
import Yesod.Session.Manager.Save
import Yesod.Session.Options
import Yesod.Session.SaveResult
import Yesod.Session.SessionType
import Yesod.Session.Storage.Operation

spec :: Spec
spec = context "saveSession" $ do
  specify "doesn't unnecessarily create a session"
    $ forAll (genMockInit id)
    $ \mockInit -> ioProperty $ do
      mock@Mock {sessionManager} <- newMock id mockInit
      load <- loadNothing sessionManager
      save <- saveSession sessionManager load $ loadedData load
      transcript <- takeTranscript mock.mockStorage
      pure
        $ counterexample (show save) (save === NoChange)
        .&&. counterexample (show transcript) (transcript == Seq.empty)

  specify "may create a session"
    $ forAll (genMockInit id)
    $ \mockInit -> ioProperty $ do
      mock@Mock {sessionManager} <- newMock id mockInit
      now <- readTVarIO mock.currentTime
      load <- loadNothing sessionManager
      let newData = loadedData load & Map.insert "a" "b"
      session <- assertSaved =<< saveSession sessionManager load newData
      transcript <- takeTranscript mock.mockStorage
      pure
        $ counterexample
          (show session)
          ( (session.map == newData)
              .&&. (session.time.created == now)
              .&&. (session.time.accessed == now)
          )
        .&&. counterexample
          (show transcript)
          (transcript == Seq.fromList [StorageOperation' $ InsertSession session])

  specify "may update a loaded session"
    $ forAll (genMockInit noTimeoutResolution)
    $ \mockInit -> ioProperty $ do
      mock@Mock {sessionManager} <- newMock id mockInit
      time1 <- readTVarIO mock.currentTime
      session1 <- do
        load <- loadNothing sessionManager
        let newData = loadedData load & Map.insert "a" "b"
        assertSaved =<< saveSession sessionManager load newData
      advanceTimeBriefly mock
      time2 <- readTVarIO mock.currentTime
      void $ takeTranscript mock.mockStorage
      load <- loadSession sessionManager session1.key
      let newData = loadedData load & Map.insert "c" "d"
      session2 <- assertSaved =<< saveSession sessionManager load newData
      transcript <- takeTranscript mock.mockStorage
      pure
        $ counterexample
          (show session2)
          ( (session2.map == Map.fromList [("a", "b"), ("c", "d")])
              .&&. (session2.time.created == time1)
              .&&. (session2.time.accessed == time2)
          )
        .&&. counterexample
          (show transcript)
          ( transcript
              == Seq.fromList
                [ StorageOperation' $ GetSession session2.key
                , StorageOperation' $ ReplaceSession session2
                ]
          )

  specify "changes the session key when we rotate"
    $ forAll (genMockInit noTimeoutResolution)
    $ \mockInit -> ioProperty $ do
      mock@Mock {sessionManager} <- newMock id mockInit
      let SessionManager {options} = sessionManager
      session1 <- do
        load <- loadNothing sessionManager
        let newData = loadedData load & Map.insert "a" "b"
        assertSaved =<< saveSession sessionManager load newData
      advanceTimeBriefly mock
      (session2, transcript) <- do
        load <- loadSession sessionManager session1.key
        void $ takeTranscript mock.mockStorage
        let newData =
              loadedData load
                & setSessionKeyRotation options (Just RotateSessionKey)
                & Map.insert "c" "d"
        session <- assertSaved =<< saveSession sessionManager load newData
        transcript <- takeTranscript mock.mockStorage
        pure (session, transcript)
      loadForOldSession <- loadSession sessionManager session1.key
      loadForNewSession <- loadSession sessionManager session2.key
      pure
        $ counterexample (show (session1, session2)) (session1.key /= session2.key)
        .&&. counterexample (show loadForOldSession) (not $ didSessionLoad loadForOldSession)
        .&&. counterexample
          (show loadForNewSession)
          (loadedData loadForNewSession == Map.fromList [("a", "b"), ("c", "d")])
        .&&. counterexample
          (show transcript)
          ( transcript
              == Seq.fromList
                [ StorageOperation' $ DeleteSession session1.key
                , StorageOperation' $ InsertSession session2
                ]
          )

setSessionKeyRotation
  :: Options tx m -> Maybe KeyRotation -> SessionMap -> SessionMap
setSessionKeyRotation options =
  execState . embed options.embedding.keyRotation

assertSaved :: Show a => SaveResult a -> IO a
assertSaved = \case
  Saved x -> pure x
  x -> fail $ "Expected Saved, but got: " <> show x