packages feed

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

module Eventium.EventHandlerSpec (spec) where

import Data.Functor.Contravariant
import Data.IORef
import Eventium.Codec (Codec (..), DecodeError (..))
import Eventium.EventHandler
import Test.Hspec

-- Helper: create an EventHandler that appends events to an IORef
recordingHandler :: IORef [a] -> EventHandler IO a
recordingHandler ref = EventHandler $ \e -> modifyIORef ref (++ [e])

spec :: Spec
spec = do
  describe "handleEvent" $ do
    it "should process a single event" $ do
      ref <- newIORef ([] :: [Int])
      (recordingHandler ref).handleEvent 42
      readIORef ref `shouldReturn` [42]

  describe "handleEvents" $ do
    it "should process events in order" $ do
      ref <- newIORef ([] :: [Int])
      handleEvents (recordingHandler ref) [1, 2, 3]
      readIORef ref `shouldReturn` [1, 2, 3]

    it "should do nothing for empty list" $ do
      ref <- newIORef ([] :: [Int])
      handleEvents (recordingHandler ref) []
      readIORef ref `shouldReturn` []

  describe "Contravariant instance" $ do
    it "should adapt the event type via contramap" $ do
      ref <- newIORef ([] :: [Int])
      let handler = contramap length (recordingHandler ref)
      handler.handleEvent "hello"
      handler.handleEvent "hi"
      readIORef ref `shouldReturn` [5, 2]

  describe "Semigroup instance" $ do
    it "should run both handlers for each event" $ do
      ref1 <- newIORef ([] :: [Int])
      ref2 <- newIORef ([] :: [Int])
      let combined = recordingHandler ref1 <> recordingHandler ref2
      combined.handleEvent 10
      combined.handleEvent 20
      readIORef ref1 `shouldReturn` [10, 20]
      readIORef ref2 `shouldReturn` [10, 20]

  describe "Monoid instance" $ do
    it "mempty should be a no-op" $ do
      (mempty :: EventHandler IO Int).handleEvent 42
    -- No crash, no observable effect

    it "mconcat should compose multiple handlers" $ do
      ref1 <- newIORef ([] :: [Int])
      ref2 <- newIORef ([] :: [Int])
      ref3 <- newIORef ([] :: [Int])
      let combined = mconcat [recordingHandler ref1, recordingHandler ref2, recordingHandler ref3]
      combined.handleEvent 99
      readIORef ref1 `shouldReturn` [99]
      readIORef ref2 `shouldReturn` [99]
      readIORef ref3 `shouldReturn` [99]

  describe "eventHandlerMapMaybe" $ do
    it "should only handle events matching the filter" $ do
      ref <- newIORef ([] :: [Int])
      let evenOnly n = if even n then Just n else Nothing
          handler = eventHandlerMapMaybe evenOnly (recordingHandler ref)
      handleEvents handler [1, 2, 3, 4, 5, 6]
      readIORef ref `shouldReturn` [2, 4, 6]

    it "should handle no events when filter matches nothing" $ do
      ref <- newIORef ([] :: [Int])
      let noneMatch _ = Nothing :: Maybe Int
          handler = eventHandlerMapMaybe noneMatch (recordingHandler ref)
      handleEvents handler [1, 2, 3]
      readIORef ref `shouldReturn` []

  describe "codecEventHandler" $ do
    it "should deserialize events before handling" $ do
      ref <- newIORef ([] :: [Int])
      let codec = Codec show (\s -> Just (read s :: Int))
          handler = codecEventHandler codec (recordingHandler ref)
      handleEvents handler ["1", "2", "3"]
      readIORef ref `shouldReturn` [1, 2, 3]

    it "should error on deserialization failure" $ do
      ref <- newIORef ([] :: [Int])
      let codec = Codec show (\s -> if s == "bad" then Nothing else Just (read s :: Int))
          handler = codecEventHandler codec (recordingHandler ref)
      handleEvents handler ["1", "bad", "3"] `shouldThrow` (\(DecodeError ctx _) -> ctx == "codecEventHandler")

  describe "lenientCodecEventHandler" $ do
    it "should silently drop events that fail to deserialize" $ do
      ref <- newIORef ([] :: [Int])
      let codec = Codec show (\s -> if s == "bad" then Nothing else Just (read s :: Int))
          handler = lenientCodecEventHandler codec (recordingHandler ref)
      handleEvents handler ["1", "bad", "3"]
      readIORef ref `shouldReturn` [1, 3]