packages feed

eventium-memory-0.2.1: tests/Eventium/ReadModelSpec.hs

module Eventium.ReadModelSpec (spec) where

import Control.Concurrent.STM
import Eventium
import Eventium.Store.Memory
import Eventium.Testkit
import Test.Hspec

spec :: Spec
spec = do
  describe "rebuildReadModel" $ do
    it "should process all events from the beginning" $ do
      eventTVar <- eventMapTVar
      let writer = runEventStoreWriterUsing atomically (tvarEventStoreWriter eventTVar)
          globalReader = runEventStoreReaderUsing atomically (tvarGlobalEventStoreReader eventTVar)

      -- Write some events
      _ <- writer.storeEvents (uuidFromInteger 1) NoStream [Added 1, Added 2]
      _ <- writer.storeEvents (uuidFromInteger 2) NoStream [Added 10]

      -- Create a simple in-memory read model that sums all Added amounts
      sumRef <- newTVarIO (0 :: Int)
      checkpointRef <- newTVarIO (0 :: SequenceNumber)
      initCount <- newTVarIO (0 :: Int)
      resetCount <- newTVarIO (0 :: Int)

      let rm =
            ReadModel
              { initialize = atomically $ modifyTVar' initCount (+ 1),
                eventHandler = EventHandler $ \globalEvent -> do
                  let innerEvent = globalEvent.payload
                  case innerEvent.payload of
                    Added n -> atomically $ modifyTVar' sumRef (+ n)
                    _ -> return (),
                checkpointStore =
                  CheckpointStore
                    { getCheckpoint = readTVarIO checkpointRef,
                      saveCheckpoint = atomically . writeTVar checkpointRef
                    },
                reset = atomically $ do
                  writeTVar sumRef 0
                  writeTVar checkpointRef 0
                  modifyTVar' resetCount (+ 1)
              }

      rebuildReadModel globalReader rm

      result <- readTVarIO sumRef
      result `shouldBe` 13

      checkpoint <- readTVarIO checkpointRef
      checkpoint `shouldBe` 3

      inits <- readTVarIO initCount
      inits `shouldBe` 1

      resets <- readTVarIO resetCount
      resets `shouldBe` 1

    it "should reset state before replaying" $ do
      eventTVar <- eventMapTVar
      let writer = runEventStoreWriterUsing atomically (tvarEventStoreWriter eventTVar)
          globalReader = runEventStoreReaderUsing atomically (tvarGlobalEventStoreReader eventTVar)

      _ <- writer.storeEvents (uuidFromInteger 1) NoStream [Added 5]

      sumRef <- newTVarIO (0 :: Int)
      checkpointRef <- newTVarIO (0 :: SequenceNumber)

      let rm =
            ReadModel
              { initialize = return (),
                eventHandler = EventHandler $ \globalEvent -> do
                  let innerEvent = globalEvent.payload
                  case innerEvent.payload of
                    Added n -> atomically $ modifyTVar' sumRef (+ n)
                    _ -> return (),
                checkpointStore =
                  CheckpointStore
                    { getCheckpoint = readTVarIO checkpointRef,
                      saveCheckpoint = atomically . writeTVar checkpointRef
                    },
                reset = atomically $ do
                  writeTVar sumRef 0
                  writeTVar checkpointRef 0
              }

      -- Rebuild twice — should get same result, not double
      rebuildReadModel globalReader rm
      rebuildReadModel globalReader rm

      result <- readTVarIO sumRef
      result `shouldBe` 5

  describe "combineReadModels" $ do
    it "should fan out events to multiple handlers" $ do
      eventTVar <- eventMapTVar
      let writer = runEventStoreWriterUsing atomically (tvarEventStoreWriter eventTVar)
          globalReader = runEventStoreReaderUsing atomically (tvarGlobalEventStoreReader eventTVar)

      _ <- writer.storeEvents (uuidFromInteger 1) NoStream [Added 3, Added 7]

      sumRef <- newTVarIO (0 :: Int)
      countRef <- newTVarIO (0 :: Int)
      checkpointRef <- newTVarIO (0 :: SequenceNumber)

      let sumRM =
            ReadModel
              { initialize = return (),
                eventHandler = EventHandler $ \globalEvent -> do
                  case globalEvent.payload.payload of
                    Added n -> atomically $ modifyTVar' sumRef (+ n)
                    _ -> return (),
                checkpointStore =
                  CheckpointStore
                    { getCheckpoint = readTVarIO checkpointRef,
                      saveCheckpoint = atomically . writeTVar checkpointRef
                    },
                reset = atomically $ do
                  writeTVar sumRef 0
                  writeTVar checkpointRef 0
              }

      let countRM =
            ReadModel
              { initialize = return (),
                eventHandler = EventHandler $ \_ ->
                  atomically $ modifyTVar' countRef (+ 1),
                checkpointStore =
                  CheckpointStore
                    { getCheckpoint = readTVarIO checkpointRef,
                      saveCheckpoint = atomically . writeTVar checkpointRef
                    },
                reset = atomically $ do
                  writeTVar countRef 0
                  writeTVar checkpointRef 0
              }

      let combined = combineReadModels [sumRM, countRM]

      rebuildReadModel globalReader combined

      s <- readTVarIO sumRef
      s `shouldBe` 10

      c <- readTVarIO countRef
      c `shouldBe` 2

    it "should initialize and reset all sub-models" $ do
      initA <- newTVarIO False
      initB <- newTVarIO False
      resetA <- newTVarIO False
      resetB <- newTVarIO False
      checkpointRef <- newTVarIO (0 :: SequenceNumber)

      let mkRM initRef resetRef =
            ReadModel
              { initialize = atomically $ writeTVar initRef True,
                eventHandler = mempty,
                checkpointStore =
                  CheckpointStore
                    { getCheckpoint = readTVarIO checkpointRef,
                      saveCheckpoint = atomically . writeTVar checkpointRef
                    },
                reset = atomically $ writeTVar resetRef True
              }

      let combined = combineReadModels [mkRM initA resetA, mkRM initB resetB]
          runInit = combined.initialize
          runReset = combined.reset

      runInit
      iA <- readTVarIO initA
      iB <- readTVarIO initB
      iA `shouldBe` True
      iB `shouldBe` True

      runReset
      rA <- readTVarIO resetA
      rB <- readTVarIO resetB
      rA `shouldBe` True
      rB `shouldBe` True