packages feed

eventium-core-0.2.1: tests/Eventium/CommandDispatcherSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Eventium.CommandDispatcherSpec (spec) where

import Data.IORef
import Eventium.CommandDispatcher
import Eventium.CommandHandler
import Eventium.ProcessManager (CommandDispatchResult (..), CommandDispatcher (..), RejectionReason (..))
import Eventium.Projection
import Eventium.Store.Class
import Eventium.UUID
import Test.Hspec

-- Minimal test domain
data CounterEvent = Incremented | Decremented deriving (Show, Eq)

data CounterCommand = Increment | Decrement | Unknown deriving (Show, Eq)

data CounterError = AlreadyZero deriving (Show, Eq)

type Counter = Int

counterProjection :: Projection Counter CounterEvent
counterProjection =
  Projection 0 $ \s e -> case e of
    Incremented -> s + 1
    Decremented -> s - 1

counterHandler :: CommandHandler Counter CounterEvent CounterCommand CounterError
counterHandler = CommandHandler decide' counterProjection
  where
    decide' _ Increment = Right [Incremented]
    decide' s Decrement
      | s <= 0 = Left AlreadyZero
      | otherwise = Right [Decremented]
    decide' _ Unknown = Right []

-- | Simple IORef-based event store for testing.
mkTestStore :: IO (VersionedEventStoreWriter IO CounterEvent, VersionedEventStoreReader IO CounterEvent)
mkTestStore = do
  eventsRef <- newIORef ([] :: [VersionedStreamEvent CounterEvent])
  let writer = EventStoreWriter $ \uuid _expected events -> do
        existing <- readIORef eventsRef
        let startVersion = fromIntegral (length existing)
            versioned = zipWith (\i e -> StreamEvent uuid i (emptyMetadata "") e) [startVersion ..] events
        modifyIORef eventsRef (++ versioned)
        pure (Right (startVersion + fromIntegral (length events) - 1))
      reader = EventStoreReader $ \query -> do
        allEvts <- readIORef eventsRef
        pure $ filterByQuery query allEvts
  pure (writer, reader)
  where
    filterByQuery (QueryRange uuid _ _) =
      filter (\(StreamEvent k _ _ _) -> k == uuid)

spec :: Spec
spec = describe "CommandDispatcher" $ do
  describe "commandHandlerDispatcher" $ do
    it "routes command to matching handler and reports success" $ do
      (writer, reader) <- mkTestStore

      let handlers = [mkAggregateHandler counterHandler]
          dispatcher = commandHandlerDispatcher writer reader handlers

      result <- dispatcher.dispatchCommand (uuidFromInteger 1) Increment
      result `shouldBe` CommandSucceeded

    it "reports failure when command is rejected" $ do
      (writer, reader) <- mkTestStore

      let handlers = [mkAggregateHandler counterHandler]
          dispatcher = commandHandlerDispatcher writer reader handlers

      -- Counter starts at 0, Decrement should fail
      result <- dispatcher.dispatchCommand (uuidFromInteger 1) Decrement
      result `shouldBe` CommandFailed (RejectionReason "AlreadyZero")

    it "returns CommandSucceeded when no handler matches" $ do
      (writer, reader) <- mkTestStore

      let handlers = [mkAggregateHandler counterHandler]
          dispatcher = commandHandlerDispatcher writer reader handlers

      -- Unknown returns Right [], so no handler "matches" (produces events)
      result <- dispatcher.dispatchCommand (uuidFromInteger 1) Unknown
      result `shouldBe` CommandSucceeded