packages feed

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

{-# LANGUAGE OverloadedStrings #-}

module Eventium.ProjectionSpec (spec) where

import Control.Exception (evaluate)
import Data.Functor.Contravariant
import Eventium.Codec (Codec (..), DecodeError (..))
import Eventium.Projection
import Eventium.Store.Types
import Test.Hspec

-- Simple test projection: counter that sums integers
counterProjection :: Projection Int Int
counterProjection = Projection 0 (+)

spec :: Spec
spec = do
  describe "latestProjection" $ do
    it "should compute the correct final state" $ do
      latestProjection counterProjection [1, 2, 3, 4] `shouldBe` 10

    it "should return the seed for an empty event list" $ do
      latestProjection counterProjection [] `shouldBe` 0

  describe "allProjections" $ do
    it "should return all intermediate states" $ do
      allProjections counterProjection [1, 2, 3] `shouldBe` [0, 1, 3, 6]

    it "should return just the seed for an empty event list" $ do
      allProjections counterProjection [] `shouldBe` [0]

  describe "Contravariant instance" $ do
    it "should adapt the event type via contramap" $ do
      let stringProjection = contramap (length :: String -> Int) counterProjection
      latestProjection stringProjection ["hi", "hello", "a"] `shouldBe` 8

  describe "projectionMapMaybe" $ do
    it "should filter events and apply matching ones" $ do
      let evenOnly n = if even n then Just n else Nothing
          evenProjection = projectionMapMaybe evenOnly counterProjection
      latestProjection evenProjection [1, 2, 3, 4, 5, 6] `shouldBe` 12

    it "should return seed when no events match" $ do
      let noneMatch _ = Nothing
          emptyProjection = projectionMapMaybe noneMatch counterProjection
      latestProjection emptyProjection [1, 2, 3] `shouldBe` 0

  describe "codecProjection" $ do
    it "should deserialize and apply events" $ do
      let codec = Codec show (Just . (read :: String -> Int))
          codecProj = codecProjection codec counterProjection
      latestProjection codecProj ["1", "2", "3"] `shouldBe` 6

    it "should error on deserialization failure" $ do
      let codec = Codec show (\s -> if s == "bad" then Nothing else Just (read s :: Int))
          codecProj = codecProjection codec counterProjection
      evaluate (latestProjection codecProj ["1", "bad"]) `shouldThrow` (\(DecodeError ctx _) -> ctx == "codecProjection")

  describe "lenientCodecProjection" $ do
    it "should apply deserialized events and skip failures" $ do
      let codec = Codec show (\s -> if s == "bad" then Nothing else Just (read s :: Int))
          codecProj = lenientCodecProjection codec counterProjection
      latestProjection codecProj ["1", "2", "bad", "3"] `shouldBe` 6

  describe "streamProjectionEventHandler" $ do
    it "should update state and position" $ do
      let sp = streamProjection "key" (0 :: Int) counterProjection
          event = StreamEvent "other" 5 (emptyMetadata "") (10 :: Int)
          sp' = streamProjectionEventHandler sp event
      sp'.state `shouldBe` 10
      sp'.position `shouldBe` 5
      sp'.key `shouldBe` "key"