packages feed

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

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Eventium.EventPublisherSpec (spec) where

import Control.Concurrent.STM
import Control.Monad
import Data.IORef
import Eventium.EventHandler
import Eventium.EventPublisher
import Eventium.Store.Class
import Eventium.Store.Memory
import Eventium.UUID
import Test.Hspec

-- Helper to create IO-compatible stores from a TVar
makeIOStore :: TVar (EventMap event) -> (VersionedEventStoreWriter IO event, VersionedEventStoreReader IO event, GlobalEventStoreReader IO event)
makeIOStore tvar =
  ( runEventStoreWriterUsing atomically (tvarEventStoreWriter tvar),
    runEventStoreReaderUsing atomically (tvarEventStoreReader tvar),
    runEventStoreReaderUsing atomically (tvarGlobalEventStoreReader tvar)
  )

spec :: Spec
spec = do
  describe "synchronousPublisher" $ do
    it "should deliver each event to the handler" $ do
      ref <- newIORef ([] :: [Int])
      let handler = EventHandler $ \(StreamEvent _ _ _ e) -> modifyIORef ref (++ [e])
          publisher = synchronousPublisher handler
      publisher.publishEvents
        nil
        [ StreamEvent nil 0 (emptyMetadata "") (1 :: Int),
          StreamEvent nil 1 (emptyMetadata "") 2,
          StreamEvent nil 2 (emptyMetadata "") 3
        ]
      readIORef ref `shouldReturn` [1, 2, 3]

  describe "publishingEventStoreWriter" $ do
    it "should publish events after a successful write" $ do
      tvar <- eventMapTVar
      let (writer, reader, _) = makeIOStore tvar

      ref <- newIORef ([] :: [Int])
      let handler = EventHandler $ \(StreamEvent _ _ _ e) -> modifyIORef ref (++ [e])
          publisher = synchronousPublisher handler
          publishingWriter = publishingEventStoreWriter writer publisher

      _ <- publishingWriter.storeEvents (uuidFromInteger 1) NoStream [10, 20, 30 :: Int]

      readIORef ref `shouldReturn` [10, 20, 30]

      -- Verify events are also stored
      events <- reader.getEvents (allEvents (uuidFromInteger 1))
      map (.payload) events `shouldBe` [10, 20, 30]

    it "should NOT publish events when the write fails" $ do
      tvar <- eventMapTVar
      let (writer, _, _) = makeIOStore tvar

      ref <- newIORef ([] :: [Int])
      let handler = EventHandler $ \(StreamEvent _ _ _ e) -> modifyIORef ref (++ [e])
          publisher = synchronousPublisher handler
          publishingWriter = publishingEventStoreWriter writer publisher

      -- Write should fail due to ExactPosition mismatch
      result <- publishingWriter.storeEvents (uuidFromInteger 1) (ExactPosition 5) [10 :: Int]
      result `shouldSatisfy` isLeft'

      readIORef ref `shouldReturn` []

    it "should tag events with correct stream key and version positions" $ do
      tvar <- eventMapTVar
      let (writer, _, _) = makeIOStore tvar

      ref <- newIORef ([] :: [(UUID, EventVersion, Int)])
      let handler = EventHandler $ \(StreamEvent uuid pos _ e) -> modifyIORef ref (++ [(uuid, pos, e)])
          publisher = synchronousPublisher handler
          publishingWriter = publishingEventStoreWriter writer publisher

      let uuid1 = uuidFromInteger 1
      _ <- publishingWriter.storeEvents uuid1 NoStream [100, 200 :: Int]

      readIORef ref
        `shouldReturn` [ (uuid1, 0, 100),
                         (uuid1, 1, 200)
                       ]

  describe "breadth-first dispatch (regression test for depth-first bug)" $ do
    it "should deliver all original events before handler-generated events" $ do
      -- This is the critical test: the old EventBus had depth-first dispatch
      -- where if handler A generated new events, those were dispatched to
      -- handler B before handler B saw the rest of the original batch.
      --
      -- With the new EventPublisher, the publish happens AFTER all events
      -- in the batch are written. The handler sees all events in the batch
      -- sequentially. Any events written by a handler are stored but do NOT
      -- trigger re-entrant publishing (because the handler writes to the
      -- raw writer, not the publishing writer).

      tvar <- eventMapTVar
      let (rawWriter, _, _) = makeIOStore tvar

      -- Track the order in which handler B sees events
      orderRef <- newIORef ([] :: [String])

      -- Handler A: when it sees event "trigger", writes new events to the store
      let handlerA = EventHandler $ \(StreamEvent _ _ _ (e :: String)) ->
            when (e == "trigger") $ do
              _ <-
                rawWriter.storeEvents
                  (uuidFromInteger 99)
                  AnyPosition
                  ["handler-a-generated"]
              return ()

          -- Handler B: records every event it receives, in order
          handlerB = EventHandler $ \(StreamEvent _ _ _ e) ->
            modifyIORef orderRef (++ [e])

          publisher = synchronousPublisher (handlerA <> handlerB)
          publishingWriter = publishingEventStoreWriter rawWriter publisher

      -- Write two events. "trigger" will cause handler A to write to the store.
      -- Handler B should see BOTH original events ("trigger" and "normal")
      -- sequentially. Handler A's written events go to the raw writer and
      -- are NOT re-published through the handler chain.
      _ <- publishingWriter.storeEvents (uuidFromInteger 1) NoStream ["trigger", "normal" :: String]

      observed <- readIORef orderRef

      -- Handler B should see both original events in order
      observed `shouldBe` ["trigger", "normal"]

isLeft' :: Either a b -> Bool
isLeft' (Left _) = True
isLeft' (Right _) = False