packages feed

eventium-test-helpers (empty) → 0.1.0

raw patch · 3 files changed

+436/−0 lines, 3 filesdep +aesondep +aeson-casingdep +base

Dependencies added: aeson, aeson-casing, base, eventium-core, extra, hspec, monad-logger, text

Files

+ 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-test-helpers.cabal view
@@ -0,0 +1,44 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.37.0.+--+-- see: https://github.com/sol/hpack++name:           eventium-test-helpers+version:        0.1.0+synopsis:       Common module used for eventium tests+description:    Eventium-test-helpers 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++library+  exposed-modules:+      Eventium.TestHelpers+  other-modules:+      Paths_eventium_test_helpers+  hs-source-dirs:+      src+  ghc-options: -Wall+  build-depends:+      aeson >=1.5 && <2.3+    , aeson-casing >=0.2 && <0.3+    , base >=4.9 && <5+    , eventium-core+    , extra >=1.7 && <1.8+    , hspec >=2.8 && <2.12+    , monad-logger >=0.3 && <0.4+    , text >=1.2 && <2.2+  default-language: Haskell2010
+ src/Eventium/TestHelpers.hs view
@@ -0,0 +1,372 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Common test functionality+module Eventium.TestHelpers+  ( Counter (..),+    CounterProjection,+    counterProjection,+    CounterCommandHandler,+    counterCommandHandler,+    CounterEvent (..),+    CounterCommand (..),+    EventStoreRunner (..),+    GlobalStreamEventStoreRunner (..),+    eventStoreSpec,+    globalStreamEventStoreSpec,+    VersionedProjectionCacheRunner (..),+    versionedProjectionCacheSpec,+    GlobalStreamProjectionCacheRunner (..),+    globalStreamProjectionCacheSpec,+    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.Casing+import Data.Aeson.TH+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+      { _counterEventAmount :: Int+      }+  | CounterFailedOutOfBounds+  deriving (Eq, Show)++type CounterProjection = Projection Counter CounterEvent++counterProjection :: CounterProjection+counterProjection =+  Projection+    (Counter 0)+    (\(Counter k) (Added x) -> Counter (k + x))++counterGlobalProjection :: Projection Counter (VersionedStreamEvent CounterEvent)+counterGlobalProjection =+  Projection+    (Counter 0)+    (\(Counter k) (StreamEvent _ _ (Added x)) -> Counter (k + x))++data CounterCommand+  = Increment+      { _counterCommandAmount :: Int+      }+  | Decrement+      { _counterCommandAmount :: Int+      }+  deriving (Eq, Show)++type CounterCommandHandler = CommandHandler Counter CounterEvent CounterCommand++counterCommandHandler :: CounterCommandHandler+counterCommandHandler = CommandHandler counterCommand counterProjection++counterCommand :: Counter -> CounterCommand -> [CounterEvent]+counterCommand (Counter k) (Increment n) =+  if k + n <= 100+    then [Added n]+    else [CounterFailedOutOfBounds]+counterCommand (Counter k) (Decrement n) =+  if k - n >= 0+    then [Added (-n)]+    else [CounterFailedOutOfBounds]++deriveJSON (aesonPrefix camelCase) ''CounterEvent+deriveJSON (aesonPrefix camelCase) ''CounterCommand++-- 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+          _ <- storeEvents writer nil NoStream sampleEvents+          action writer reader++    it "should return events" $ do+      events' <- withStore' $ \_ reader -> getEvents reader (allEvents nil)+      (streamEventEvent <$> events') `shouldBe` sampleEvents++    it "should return correct event versions" $ do+      events <- withStore' $ \_ reader -> getEvents reader (allEvents nil)+      (streamEventPosition <$> events) `shouldBe` [0, 1, 2, 3]++    it "should return correct events with queries" $ do+      (firstEvents, middleEvents, laterEvents, maxEvents) <- withStore' $ \_ reader ->+        (,,,)+          <$> getEvents reader (eventsUntil nil 1)+          <*> getEvents reader (eventsStartingAtUntil nil 1 2)+          <*> getEvents reader (eventsStartingAt nil 2)+          <*> getEvents reader (eventsStartingAtTakeLimit nil 0 2)+      (streamEventEvent <$> firstEvents) `shouldBe` take 2 sampleEvents+      (streamEventEvent <$> middleEvents) `shouldBe` take 2 (drop 1 sampleEvents)+      (streamEventEvent <$> laterEvents) `shouldBe` drop 2 sampleEvents+      (streamEventEvent <$> maxEvents) `shouldBe` take 2 sampleEvents++    it "should return the latest projection" $ do+      projection <- withStore' $ \_ reader ->+        getLatestStreamProjection reader (versionedStreamProjection nil counterProjection)+      streamProjectionState projection `shouldBe` Counter 7+      streamProjectionPosition projection `shouldBe` 3+      streamProjectionKey projection `shouldBe` nil++    it "should return the latest projection with some starting StreamProjection" $ do+      projection <- withStore' $ \_ reader -> do+        initialEvents <- getEvents reader (eventsUntil nil 1)+        let initialProjection = latestProjection counterProjection (streamEventEvent <$> initialEvents)+        getLatestStreamProjection reader (StreamProjection nil 1 counterProjection initialProjection)+      streamProjectionState projection `shouldBe` Counter 7+      streamProjectionPosition projection `shouldBe` 3+      streamProjectionKey projection `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 ->+        (,) <$> getEvents reader (allEvents uuid1) <*> getEvents reader (allEvents uuid2)+      (streamEventEvent <$> events1) `shouldBe` Added <$> [1, 4]+      (streamEventEvent <$> events2) `shouldBe` Added <$> [2, 3, 5]+      (streamEventKey <$> events1) `shouldBe` [uuid1, uuid1]+      (streamEventKey <$> events2) `shouldBe` [uuid2, uuid2, uuid2]+      (streamEventPosition <$> events1) `shouldBe` [0, 1]+      (streamEventPosition <$> events2) `shouldBe` [0, 1, 2]++    it "should return correct event versions" $ do+      (events1, events2) <- withStoreExampleEvents $ \_ reader ->+        (,)+          <$> getEvents reader (allEvents uuid1)+          <*> getEvents reader (allEvents uuid2)+      streamEventEvent <$> events1 `shouldBe` [Added 1, Added 4]+      streamEventEvent <$> events2 `shouldBe` [Added 2, Added 3, Added 5]++    it "should return correct events with queries" $ do+      (firstEvents, middleEvents, laterEvents, maxEvents) <- withStoreExampleEvents $ \_ reader ->+        (,,,)+          <$> getEvents reader (eventsUntil uuid1 1)+          <*> getEvents reader (eventsStartingAtUntil uuid2 1 2)+          <*> getEvents reader (eventsStartingAt uuid2 2)+          <*> getEvents reader (eventsStartingAtTakeLimit uuid1 1 1)+      (streamEventEvent <$> firstEvents) `shouldBe` [Added 1, Added 4]+      (streamEventEvent <$> middleEvents) `shouldBe` [Added 3, Added 5]+      (streamEventEvent <$> laterEvents) `shouldBe` [Added 5]+      (streamEventEvent <$> 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)+      (streamProjectionState proj1, streamProjectionPosition proj1) `shouldBe` (Counter 5, 1)+      (streamProjectionState proj2, streamProjectionPosition proj2) `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 _ ->+        (,)+          <$> storeEvents writer nil StreamExists [Added 1]+          <*> storeEvents writer 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 _ -> storeEvents writer nil NoStream [Added 1]) `shouldReturn` Right 0++    it "should reject storing events sometimes with a stream" $ do+      (err1, err2, err3) <- withStore $ \writer _ ->+        (,,)+          <$> storeEvents writer nil NoStream [Added 1]+          <*> storeEvents writer nil NoStream [Added 1]+          <*> storeEvents writer 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+          [ storeEvents writer nil NoStream [Added 1],+            storeEvents writer nil AnyPosition [Added 1],+            storeEvents writer nil (ExactPosition 1) [Added 1],+            storeEvents writer 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 -> getEvents globalReader (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+        getEvents globalReader (allEvents ())+      (streamEventEvent . streamEventEvent <$> events) `shouldBe` Added <$> [1 .. 5]+      (streamEventKey . streamEventEvent <$> events) `shouldBe` [uuid1, uuid2, uuid2, uuid1, uuid2]+      (streamEventPosition . streamEventEvent <$> events) `shouldBe` [0, 0, 1, 1, 2]+      (streamEventPosition <$> events) `shouldBe` [1 .. 5]++    it "should work with global projections" $ do+      (proj1, proj2) <- withStore $ \writer globalReader -> do+        insertExampleEvents writer+        p1 <- getLatestStreamProjection globalReader (globalStreamProjection counterGlobalProjection)+        _ <- storeEvents writer uuid1 AnyPosition [Added 10, Added 20]+        p2 <- getLatestStreamProjection globalReader p1+        return (p1, p2)++      streamProjectionPosition proj1 `shouldBe` 5+      streamProjectionPosition proj2 `shouldBe` 7++    it "should handle queries" $ do+      (firstEvents, middleEvents, laterEvents, maxEvents) <- withStore $ \writer globalReader -> do+        insertExampleEvents writer+        (,,,)+          <$> getEvents globalReader (eventsUntil () 2)+          <*> getEvents globalReader (eventsStartingAtUntil () 2 3)+          <*> getEvents globalReader (eventsStartingAt () 3)+          <*> getEvents globalReader (eventsStartingAtTakeLimit () 2 3)++      (streamEventEvent . streamEventEvent <$> firstEvents) `shouldBe` Added <$> [1 .. 2]+      (streamEventPosition <$> firstEvents) `shouldBe` [1 .. 2]+      (streamEventEvent . streamEventEvent <$> middleEvents) `shouldBe` Added <$> [2 .. 3]+      (streamEventPosition <$> middleEvents) `shouldBe` [2 .. 3]+      (streamEventEvent . streamEventEvent <$> laterEvents) `shouldBe` Added <$> [3 .. 5]+      (streamEventPosition <$> laterEvents) `shouldBe` [3 .. 5]+      (streamEventEvent . streamEventEvent <$> maxEvents) `shouldBe` Added <$> [2 .. 4]+      (streamEventPosition <$> maxEvents) `shouldBe` [2 .. 4]++insertExampleEvents ::+  (Monad m) =>+  VersionedEventStoreWriter m CounterEvent ->+  m ()+insertExampleEvents store = do+  void $ storeEvents store uuid1 NoStream [Added 1]+  void $ storeEvents store uuid2 NoStream [Added 2, Added 3]+  void $ storeEvents store uuid1 (ExactPosition 0) [Added 4]+  void $ storeEvents store 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+        storeProjectionSnapshot cache nil 4 (Counter 100)+        loadProjectionSnapshot cache 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+        _ <- storeEvents writer nil AnyPosition [Added 1, Added 2]+        getLatestVersionedProjectionWithCache reader cache (versionedStreamProjection nil counterProjection)+      streamProjectionPosition snapshot `shouldBe` 1+      streamProjectionState snapshot `shouldBe` Counter 3++    it "should work with updateProjectionCache" $ do+      snapshot <- withStoreAndCache $ \writer reader cache -> do+        _ <- storeEvents writer nil AnyPosition [Added 1, Added 2, Added 3]+        updateProjectionCache reader cache (versionedStreamProjection nil counterProjection)+        getLatestVersionedProjectionWithCache reader cache (versionedStreamProjection nil counterProjection)+      streamProjectionKey snapshot `shouldBe` nil+      streamProjectionPosition snapshot `shouldBe` 2+      streamProjectionState snapshot `shouldBe` Counter 6++newtype GlobalStreamProjectionCacheRunner m+  = GlobalStreamProjectionCacheRunner+      ( forall a.+        ( VersionedEventStoreWriter m CounterEvent ->+          GlobalEventStoreReader m CounterEvent ->+          GlobalStreamProjectionCache Text Counter m ->+          m a+        ) ->+        IO a+      )++globalStreamProjectionCacheSpec ::+  (Monad m) =>+  GlobalStreamProjectionCacheRunner m ->+  Spec+globalStreamProjectionCacheSpec (GlobalStreamProjectionCacheRunner withStoreAndCache) = do+  context "when the store is empty" $ do+    it "should be able to store and load simple projections" $ do+      snapshot <- withStoreAndCache $ \_ _ cache -> do+        storeProjectionSnapshot cache "key" 4 (Counter 100)+        loadProjectionSnapshot cache "key"+      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+        _ <- storeEvents writer nil AnyPosition [Added 1, Added 2]+        getLatestGlobalProjectionWithCache globalReader cache (globalStreamProjection counterGlobalProjection) "key"+      streamProjectionPosition snapshot `shouldBe` 2+      streamProjectionState snapshot `shouldBe` Counter 3++    it "should work with updateGlobalProjectionCache" $ do+      snapshot <- withStoreAndCache $ \writer globalReader cache -> do+        _ <- storeEvents writer nil AnyPosition [Added 1, Added 2, Added 3]+        updateGlobalProjectionCache globalReader cache (globalStreamProjection counterGlobalProjection) "key"+        getLatestGlobalProjectionWithCache globalReader cache (globalStreamProjection counterGlobalProjection) "key"+      streamProjectionPosition snapshot `shouldBe` 3+      streamProjectionState snapshot `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) "key"+        getLatestGlobalProjectionWithCache globalReader cache (globalStreamProjection counterGlobalProjection) "key"+      streamProjectionPosition snapshot `shouldBe` 5+      streamProjectionState snapshot `shouldBe` Counter 15