eventium-testkit (empty) → 0.2.1
raw patch · 3 files changed
+601/−0 lines, 3 filesdep +aesondep +basedep +eventium-core
Dependencies added: aeson, base, eventium-core, extra, hspec, monad-logger, text
Files
- LICENSE.md +20/−0
- eventium-testkit.cabal +54/−0
- src/Eventium/Testkit.hs +527/−0
+ LICENSE.md view
@@ -0,0 +1,20 @@+Copyright (c) 2025 Alexander Sidorenko+Copyright (c) 2016-2017 David Reaver++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies+of the Software, and to permit persons to whom the Software is furnished to do+so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ eventium-testkit.cabal view
@@ -0,0 +1,54 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.38.3.+--+-- see: https://github.com/sol/hpack++name: eventium-testkit+version: 0.2.1+synopsis: Testing utilities for eventium+description: Eventium-testkit provides common testing utilities and helper functions for testing Eventium-based+ applications. It includes utilities for event store testing, serialization testing, and common test fixtures+ that are shared across the Eventium test suite. This package helps reduce code duplication in tests and+ provides a consistent testing interface.+category: Database,Eventsourcing+stability: experimental+homepage: https://github.com/aleks-sidorenko/eventium#readme+bug-reports: https://github.com/aleks-sidorenko/eventium/issues+maintainer: Alexander Sidorenko+license: MIT+license-file: LICENSE.md+build-type: Simple++source-repository head+ type: git+ location: https://github.com/aleks-sidorenko/eventium++flag ci+ description: Enable -Werror for CI builds+ manual: True+ default: False++library+ exposed-modules:+ Eventium.Testkit+ other-modules:+ Paths_eventium_testkit+ hs-source-dirs:+ src+ default-extensions:+ NoFieldSelectors+ DuplicateRecordFields+ OverloadedRecordDot+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wredundant-constraints -Wno-type-defaults -Wno-incomplete-uni-patterns+ build-depends:+ aeson >=1.5 && <2.3+ , base >=4.9 && <5+ , eventium-core >=0.2.0 && <0.3.0+ , extra ==1.7.*+ , hspec >=2.8 && <2.12+ , monad-logger ==0.3.*+ , text >=1.2 && <2.2+ default-language: Haskell2010+ if flag(ci)+ ghc-options: -Werror
+ src/Eventium/Testkit.hs view
@@ -0,0 +1,527 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Common test functionality+module Eventium.Testkit+ ( Counter (..),+ CounterProjection,+ counterProjection,+ CounterCommandHandler,+ CounterCommandError (..),+ counterCommandHandler,+ counterGlobalProjection,+ CounterEvent (..),+ CounterCommand (..),+ allCommandHandlerStates,+ EventStoreRunner (..),+ GlobalStreamEventStoreRunner (..),+ eventStoreSpec,+ globalStreamEventStoreSpec,+ VersionedProjectionCacheRunner (..),+ versionedProjectionCacheSpec,+ GlobalProjectionCacheRunner (..),+ globalProjectionCacheSpec,+ CheckpointStoreRunner (..),+ checkpointStoreSpec,+ snapshotEventHandlerSpec,+ snapshotGlobalEventHandlerSpec,+ applyCommandHandlerWithCacheSpec,+ Text,+ module X,+ )+where++import Control.Monad as X+import Control.Monad.IO.Class as X+import Control.Monad.Logger as X+import Data.Aeson+import Data.Aeson.TH+import Data.List (scanl')+import Data.Text (Text)+import Eventium+import Test.Hspec++-- | Example Projection/CommandHandler+newtype Counter = Counter {unCounter :: Int}+ deriving (Eq, Show, FromJSON, ToJSON)++data CounterEvent+ = Added+ { amount :: Int+ }+ | CounterFailedOutOfBounds+ deriving (Eq, Show)++type CounterProjection = Projection Counter CounterEvent++counterProjection :: CounterProjection+counterProjection =+ Projection+ (Counter 0)+ ( \(Counter k) event -> case event of+ Added x -> Counter (k + x)+ CounterFailedOutOfBounds -> Counter k+ )++counterGlobalProjection :: Projection Counter (VersionedStreamEvent CounterEvent)+counterGlobalProjection =+ Projection+ (Counter 0)+ ( \(Counter k) streamEvent -> case streamEvent.payload of+ Added x -> Counter (k + x)+ CounterFailedOutOfBounds -> Counter k+ )++data CounterCommand+ = Increment+ { amount :: Int+ }+ | Decrement+ { amount :: Int+ }+ deriving (Eq, Show)++-- | Error type for counter command rejections.+data CounterCommandError = CounterOutOfBounds+ deriving (Eq, Show)++type CounterCommandHandler = CommandHandler Counter CounterEvent CounterCommand CounterCommandError++counterCommandHandler :: CounterCommandHandler+counterCommandHandler = CommandHandler counterCommand counterProjection++counterCommand :: Counter -> CounterCommand -> Either CounterCommandError [CounterEvent]+counterCommand (Counter k) (Increment n) =+ if k + n <= 100+ then Right [Added n]+ else Left CounterOutOfBounds+counterCommand (Counter k) (Decrement n) =+ if k - n >= 0+ then Right [Added (-n)]+ else Left CounterOutOfBounds++deriveJSON defaultOptions ''CounterEvent+deriveJSON defaultOptions ''CounterCommand++-- | Given a list of commands, produce all of the states the command handler's+-- projection sees. This is useful for unit testing a 'CommandHandler' by+-- verifying the intermediate states produced by a sequence of commands.+--+-- Commands that are rejected ('Left') leave the state unchanged.+-- The first element of the returned list is the projection's seed state.+--+-- This is the canonical location for this utility. It was moved here from+-- @eventium-core@ so that production code does not depend on test helpers.+--+-- @+-- allCommandHandlerStates handler [cmd1, cmd2, cmd3]+-- == [seed, stateAfterCmd1, stateAfterCmd2, stateAfterCmd3]+-- @+allCommandHandlerStates ::+ CommandHandler state event command err ->+ [command] ->+ [state]+allCommandHandlerStates (CommandHandler commandHandler (Projection seed' eventHandler)) =+ scanl' go seed'+ where+ go st command = case commandHandler st command of+ Left _ -> st+ Right events -> foldl' eventHandler st events++-- Test harness for stores++newtype EventStoreRunner m+ = EventStoreRunner (forall a. (VersionedEventStoreWriter m CounterEvent -> VersionedEventStoreReader m CounterEvent -> m a) -> IO a)++eventStoreSpec ::+ (Monad m) =>+ EventStoreRunner m ->+ Spec+eventStoreSpec (EventStoreRunner withStore) = do+ let withStoreExampleEvents action = withStore $ \writer reader -> do+ _ <- insertExampleEvents writer+ action writer reader++ context "when a few events are inserted" $ do+ let sampleEvents = [Added 1, Added 4, Added (-3), Added 5]+ withStore' action = withStore $ \writer reader -> do+ _ <- writer.storeEvents nil NoStream sampleEvents+ action writer reader++ it "should return events" $ do+ events' <- withStore' $ \_ reader -> reader.getEvents (allEvents nil)+ ((.payload) <$> events') `shouldBe` sampleEvents++ it "should return correct event versions" $ do+ events <- withStore' $ \_ reader -> reader.getEvents (allEvents nil)+ ((.position) <$> events) `shouldBe` [0, 1, 2, 3]++ it "should return correct events with queries" $ do+ (firstEvents, middleEvents, laterEvents, maxEvents) <- withStore' $ \_ reader ->+ (,,,)+ <$> reader.getEvents (eventsUntil nil 1)+ <*> reader.getEvents (eventsStartingAtUntil nil 1 2)+ <*> reader.getEvents (eventsStartingAt nil 2)+ <*> reader.getEvents (eventsStartingAtTakeLimit nil 0 2)+ ((.payload) <$> firstEvents) `shouldBe` take 2 sampleEvents+ ((.payload) <$> middleEvents) `shouldBe` take 2 (drop 1 sampleEvents)+ ((.payload) <$> laterEvents) `shouldBe` drop 2 sampleEvents+ ((.payload) <$> maxEvents) `shouldBe` take 2 sampleEvents++ it "should return the latest projection" $ do+ proj <- withStore' $ \_ reader ->+ getLatestStreamProjection reader (versionedStreamProjection nil counterProjection)+ proj.state `shouldBe` Counter 7+ proj.position `shouldBe` 3+ proj.key `shouldBe` nil++ it "should return the latest projection with some starting StreamProjection" $ do+ proj <- withStore' $ \_ reader -> do+ initialEvents <- reader.getEvents (eventsUntil nil 1)+ let initialProjection = latestProjection counterProjection ((.payload) <$> initialEvents)+ getLatestStreamProjection reader (StreamProjection nil 1 counterProjection initialProjection)+ proj.state `shouldBe` Counter 7+ proj.position `shouldBe` 3+ proj.key `shouldBe` nil++ context "when events from multiple UUIDs are inserted" $ do+ it "should have the correct events for each stream" $ do+ (events1, events2) <- withStoreExampleEvents $ \_ reader ->+ (,) <$> reader.getEvents (allEvents uuid1) <*> reader.getEvents (allEvents uuid2)+ ((.payload) <$> events1) `shouldBe` Added <$> [1, 4]+ ((.payload) <$> events2) `shouldBe` Added <$> [2, 3, 5]+ ((.key) <$> events1) `shouldBe` [uuid1, uuid1]+ ((.key) <$> events2) `shouldBe` [uuid2, uuid2, uuid2]+ ((.position) <$> events1) `shouldBe` [0, 1]+ ((.position) <$> events2) `shouldBe` [0, 1, 2]++ it "should return correct event versions" $ do+ (events1, events2) <- withStoreExampleEvents $ \_ reader ->+ (,)+ <$> reader.getEvents (allEvents uuid1)+ <*> reader.getEvents (allEvents uuid2)+ (.payload) <$> events1 `shouldBe` [Added 1, Added 4]+ (.payload) <$> events2 `shouldBe` [Added 2, Added 3, Added 5]++ it "should return correct events with queries" $ do+ (firstEvents, middleEvents, laterEvents, maxEvents) <- withStoreExampleEvents $ \_ reader ->+ (,,,)+ <$> reader.getEvents (eventsUntil uuid1 1)+ <*> reader.getEvents (eventsStartingAtUntil uuid2 1 2)+ <*> reader.getEvents (eventsStartingAt uuid2 2)+ <*> reader.getEvents (eventsStartingAtTakeLimit uuid1 1 1)+ ((.payload) <$> firstEvents) `shouldBe` [Added 1, Added 4]+ ((.payload) <$> middleEvents) `shouldBe` [Added 3, Added 5]+ ((.payload) <$> laterEvents) `shouldBe` [Added 5]+ ((.payload) <$> maxEvents) `shouldBe` [Added 4]++ it "should produce the correct projections" $ do+ (proj1, proj2) <- withStoreExampleEvents $ \_ reader ->+ (,)+ <$> getLatestStreamProjection reader (versionedStreamProjection uuid1 counterProjection)+ <*> getLatestStreamProjection reader (versionedStreamProjection uuid2 counterProjection)+ (proj1.state, proj1.position) `shouldBe` (Counter 5, 1)+ (proj2.state, proj2.position) `shouldBe` (Counter 10, 2)++ describe "can handle event storage errors" $ do+ it "rejects some writes when event store isn't created" $ do+ (err1, err2) <- withStore $ \writer _ ->+ (,)+ <$> writer.storeEvents nil StreamExists [Added 1]+ <*> writer.storeEvents nil (ExactPosition 0) [Added 1]+ err1 `shouldBe` Left (EventStreamNotAtExpectedVersion (-1))+ err2 `shouldBe` Left (EventStreamNotAtExpectedVersion (-1))++ it "should be able to store events starting with an empty stream" $ do+ withStore (\writer _ -> writer.storeEvents nil NoStream [Added 1]) `shouldReturn` Right 0++ it "should reject storing events sometimes with a stream" $ do+ (err1, err2, err3) <- withStore $ \writer _ ->+ (,,)+ <$> writer.storeEvents nil NoStream [Added 1]+ <*> writer.storeEvents nil NoStream [Added 1]+ <*> writer.storeEvents nil (ExactPosition 1) [Added 1]+ err1 `shouldBe` Right 0+ err2 `shouldBe` Left (EventStreamNotAtExpectedVersion 0)+ err3 `shouldBe` Left (EventStreamNotAtExpectedVersion 0)++ it "should accepts storing events sometimes with a stream" $ do+ errors <- withStore $ \writer _ ->+ sequence+ [ writer.storeEvents nil NoStream [Added 1],+ writer.storeEvents nil AnyPosition [Added 1],+ writer.storeEvents nil (ExactPosition 1) [Added 1],+ writer.storeEvents nil StreamExists [Added 1]+ ]+ errors `shouldBe` [Right 0, Right 1, Right 2, Right 3]++newtype GlobalStreamEventStoreRunner m+ = GlobalStreamEventStoreRunner+ (forall a. (VersionedEventStoreWriter m CounterEvent -> GlobalEventStoreReader m CounterEvent -> m a) -> IO a)++globalStreamEventStoreSpec ::+ (Monad m) =>+ GlobalStreamEventStoreRunner m ->+ Spec+globalStreamEventStoreSpec (GlobalStreamEventStoreRunner withStore) = do+ context "when the event store is empty" $ do+ it "shouldn't have any events" $ do+ events <- withStore (\_ globalReader -> globalReader.getEvents (allEvents ()))+ length events `shouldBe` 0++ context "when events from multiple UUIDs are inserted" $ do+ it "should have the correct events in global order" $ do+ events <- withStore $ \writer globalReader -> do+ insertExampleEvents writer+ globalReader.getEvents (allEvents ())+ ((\e -> e.payload.payload) <$> events) `shouldBe` Added <$> [1 .. 5]+ ((\e -> e.payload.key) <$> events) `shouldBe` [uuid1, uuid2, uuid2, uuid1, uuid2]+ ((\e -> e.payload.position) <$> events) `shouldBe` [0, 0, 1, 1, 2]+ ((.position) <$> events) `shouldBe` [1 .. 5]++ it "should work with global projections" $ do+ (proj1, proj2) <- withStore $ \writer globalReader -> do+ insertExampleEvents writer+ p1 <- getLatestStreamProjection globalReader (globalStreamProjection counterGlobalProjection)+ _ <- writer.storeEvents uuid1 AnyPosition [Added 10, Added 20]+ p2 <- getLatestStreamProjection globalReader p1+ return (p1, p2)++ proj1.position `shouldBe` 5+ proj2.position `shouldBe` 7++ it "should handle queries" $ do+ (firstEvents, middleEvents, laterEvents, maxEvents) <- withStore $ \writer globalReader -> do+ insertExampleEvents writer+ (,,,)+ <$> globalReader.getEvents (eventsUntil () 2)+ <*> globalReader.getEvents (eventsStartingAtUntil () 2 3)+ <*> globalReader.getEvents (eventsStartingAt () 3)+ <*> globalReader.getEvents (eventsStartingAtTakeLimit () 2 3)++ ((\e -> e.payload.payload) <$> firstEvents) `shouldBe` Added <$> [1 .. 2]+ ((.position) <$> firstEvents) `shouldBe` [1 .. 2]+ ((\e -> e.payload.payload) <$> middleEvents) `shouldBe` Added <$> [2 .. 3]+ ((.position) <$> middleEvents) `shouldBe` [2 .. 3]+ ((\e -> e.payload.payload) <$> laterEvents) `shouldBe` Added <$> [3 .. 5]+ ((.position) <$> laterEvents) `shouldBe` [3 .. 5]+ ((\e -> e.payload.payload) <$> maxEvents) `shouldBe` Added <$> [2 .. 4]+ ((.position) <$> maxEvents) `shouldBe` [2 .. 4]++insertExampleEvents ::+ (Monad m) =>+ VersionedEventStoreWriter m CounterEvent ->+ m ()+insertExampleEvents store = do+ void $ store.storeEvents uuid1 NoStream [Added 1]+ void $ store.storeEvents uuid2 NoStream [Added 2, Added 3]+ void $ store.storeEvents uuid1 (ExactPosition 0) [Added 4]+ void $ store.storeEvents uuid2 (ExactPosition 1) [Added 5]++uuid1 :: UUID+uuid1 = uuidFromInteger 1++uuid2 :: UUID+uuid2 = uuidFromInteger 2++newtype VersionedProjectionCacheRunner m+ = VersionedProjectionCacheRunner+ ( forall a.+ ( VersionedEventStoreWriter m CounterEvent ->+ VersionedEventStoreReader m CounterEvent ->+ VersionedProjectionCache Counter m ->+ m a+ ) ->+ IO a+ )++versionedProjectionCacheSpec ::+ (Monad m) =>+ VersionedProjectionCacheRunner m ->+ Spec+versionedProjectionCacheSpec (VersionedProjectionCacheRunner withStoreAndCache) = do+ context "when the store is empty" $ do+ it "should be able to store and load simple projections" $ do+ snapshot <- withStoreAndCache $ \_ _ cache -> do+ cache.storeSnapshot nil 4 (Counter 100)+ cache.loadSnapshot nil+ snapshot `shouldBe` Just (4, Counter 100)++ context "when the store has some events in one stream" $ do+ it "should load from a stream of events" $ do+ snapshot <- withStoreAndCache $ \writer reader cache -> do+ _ <- writer.storeEvents nil AnyPosition [Added 1, Added 2]+ getLatestVersionedProjectionWithCache reader cache (versionedStreamProjection nil counterProjection)+ snapshot.position `shouldBe` 1+ snapshot.state `shouldBe` Counter 3++ it "should work with updateVersionedProjectionCache" $ do+ snapshot <- withStoreAndCache $ \writer reader cache -> do+ _ <- writer.storeEvents nil AnyPosition [Added 1, Added 2, Added 3]+ updateVersionedProjectionCache reader cache (versionedStreamProjection nil counterProjection)+ getLatestVersionedProjectionWithCache reader cache (versionedStreamProjection nil counterProjection)+ snapshot.key `shouldBe` nil+ snapshot.position `shouldBe` 2+ snapshot.state `shouldBe` Counter 6++newtype GlobalProjectionCacheRunner m+ = GlobalProjectionCacheRunner+ ( forall a.+ ( VersionedEventStoreWriter m CounterEvent ->+ GlobalEventStoreReader m CounterEvent ->+ GlobalProjectionCache Counter m ->+ m a+ ) ->+ IO a+ )++globalProjectionCacheSpec ::+ (Monad m) =>+ GlobalProjectionCacheRunner m ->+ Spec+globalProjectionCacheSpec (GlobalProjectionCacheRunner withStoreAndCache) = do+ context "when the store is empty" $ do+ it "should be able to store and load simple projections" $ do+ snapshot <- withStoreAndCache $ \_ _ cache -> do+ cache.storeSnapshot () 4 (Counter 100)+ cache.loadSnapshot ()+ snapshot `shouldBe` Just (4, Counter 100)++ context "when the store has some events in one stream" $ do+ it "should load from a global stream of events" $ do+ snapshot <- withStoreAndCache $ \writer globalReader cache -> do+ _ <- writer.storeEvents nil AnyPosition [Added 1, Added 2]+ getLatestGlobalProjectionWithCache globalReader cache (globalStreamProjection counterGlobalProjection)+ snapshot.position `shouldBe` 2+ snapshot.state `shouldBe` Counter 3++ it "should work with updateGlobalProjectionCache" $ do+ snapshot <- withStoreAndCache $ \writer globalReader cache -> do+ _ <- writer.storeEvents nil AnyPosition [Added 1, Added 2, Added 3]+ updateGlobalProjectionCache globalReader cache (globalStreamProjection counterGlobalProjection)+ getLatestGlobalProjectionWithCache globalReader cache (globalStreamProjection counterGlobalProjection)+ snapshot.position `shouldBe` 3+ snapshot.state `shouldBe` Counter 6++ context "when events from multiple UUIDs are inserted" $ do+ it "should have the correct cached projection value" $ do+ snapshot <- withStoreAndCache $ \writer globalReader cache -> do+ insertExampleEvents writer+ updateGlobalProjectionCache globalReader cache (globalStreamProjection counterGlobalProjection)+ getLatestGlobalProjectionWithCache globalReader cache (globalStreamProjection counterGlobalProjection)+ snapshot.position `shouldBe` 5+ snapshot.state `shouldBe` Counter 15++newtype CheckpointStoreRunner m+ = CheckpointStoreRunner+ ( forall a.+ ( CheckpointStore m SequenceNumber ->+ m a+ ) ->+ IO a+ )++checkpointStoreSpec ::+ (Monad m) =>+ CheckpointStoreRunner m ->+ Spec+checkpointStoreSpec (CheckpointStoreRunner withCheckpoint) = do+ it "should return 0 when no checkpoint exists" $ do+ pos <- withCheckpoint $ \cs -> cs.getCheckpoint+ pos `shouldBe` 0++ it "should store and retrieve a checkpoint" $ do+ pos <- withCheckpoint $ \cs -> do+ cs.saveCheckpoint 42+ cs.getCheckpoint+ pos `shouldBe` 42++ it "should update an existing checkpoint" $ do+ pos <- withCheckpoint $ \cs -> do+ cs.saveCheckpoint 10+ cs.saveCheckpoint 20+ cs.getCheckpoint+ pos `shouldBe` 20++snapshotEventHandlerSpec ::+ (Monad m) =>+ VersionedProjectionCacheRunner m ->+ Spec+snapshotEventHandlerSpec (VersionedProjectionCacheRunner withStoreAndCache) = do+ it "should update cache when handling events via publisher" $ do+ snapshot <- withStoreAndCache $ \writer reader cache -> do+ let handler = snapshotEventHandler reader cache counterProjection+ publisher = synchronousPublisher handler+ pubWriter = publishingEventStoreWriter writer publisher+ _ <- pubWriter.storeEvents uuid1 NoStream [Added 1, Added 2]+ cache.loadSnapshot uuid1+ snapshot `shouldBe` Just (1, Counter 3)++ it "should handle events for multiple aggregates" $ do+ (snap1, snap2) <- withStoreAndCache $ \writer reader cache -> do+ let handler = snapshotEventHandler reader cache counterProjection+ publisher = synchronousPublisher handler+ pubWriter = publishingEventStoreWriter writer publisher+ _ <- pubWriter.storeEvents uuid1 NoStream [Added 1, Added 2]+ _ <- pubWriter.storeEvents uuid2 NoStream [Added 10]+ s1 <- cache.loadSnapshot uuid1+ s2 <- cache.loadSnapshot uuid2+ return (s1, s2)+ snap1 `shouldBe` Just (1, Counter 3)+ snap2 `shouldBe` Just (0, Counter 10)++snapshotGlobalEventHandlerSpec ::+ (Monad m) =>+ GlobalProjectionCacheRunner m ->+ Spec+snapshotGlobalEventHandlerSpec (GlobalProjectionCacheRunner withStoreAndCache) = do+ it "should update global cache when handling events" $ do+ snapshot <- withStoreAndCache $ \writer globalReader cache -> do+ let handler = snapshotGlobalEventHandler globalReader cache counterGlobalProjection+ _ <- writer.storeEvents uuid1 NoStream [Added 1, Added 2]+ events <- globalReader.getEvents (allEvents ())+ handleEvents handler events+ cache.loadSnapshot ()+ snapshot `shouldBe` Just (2, Counter 3)++ it "should accumulate state across multiple writes" $ do+ snapshot <- withStoreAndCache $ \writer globalReader cache -> do+ let handler = snapshotGlobalEventHandler globalReader cache counterGlobalProjection+ _ <- writer.storeEvents uuid1 NoStream [Added 1, Added 2]+ _ <- writer.storeEvents uuid2 NoStream [Added 10]+ events <- globalReader.getEvents (allEvents ())+ handleEvents handler events+ cache.loadSnapshot ()+ snapshot `shouldBe` Just (3, Counter 13)++applyCommandHandlerWithCacheSpec ::+ (Monad m) =>+ VersionedProjectionCacheRunner m ->+ Spec+applyCommandHandlerWithCacheSpec (VersionedProjectionCacheRunner withStoreAndCache) = do+ it "should apply a command and update the cache" $ do+ (result, snapshot) <- withStoreAndCache $ \writer reader cache -> do+ r <- applyCommandHandlerWithCache writer reader cache counterCommandHandler uuid1 (Increment 5)+ s <- cache.loadSnapshot uuid1+ return (r, s)+ result `shouldBe` Right [Added 5]+ snapshot `shouldBe` Just (0, Counter 5)++ it "should use cache for subsequent commands" $ do+ (result, snapshot) <- withStoreAndCache $ \writer reader cache -> do+ _ <- applyCommandHandlerWithCache writer reader cache counterCommandHandler uuid1 (Increment 5)+ r <- applyCommandHandlerWithCache writer reader cache counterCommandHandler uuid1 (Increment 3)+ s <- cache.loadSnapshot uuid1+ return (r, s)+ result `shouldBe` Right [Added 3]+ snapshot `shouldBe` Just (1, Counter 8)++ it "should reject commands and not update cache" $ do+ (result, snapshot) <- withStoreAndCache $ \writer reader cache -> do+ r <- applyCommandHandlerWithCache writer reader cache counterCommandHandler uuid1 (Increment 101)+ s <- cache.loadSnapshot uuid1+ return (r, s)+ result `shouldBe` Left (CommandRejected CounterOutOfBounds)+ snapshot `shouldBe` Nothing