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]