cqrs-testkit-0.10.0: src/Data/CQRS/Test/Internal/RepositoryTest.hs
{-# LANGUAGE OverloadedStrings #-}
module Data.CQRS.Test.Internal.RepositoryTest
( mkRepositorySpec
) where
import Control.DeepSeq (NFData)
import Control.Monad (forM_, liftM)
import Control.Monad.Trans.Reader (ask)
import Control.Monad.IO.Class (liftIO)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString as B
import Data.CQRS.Command (CommandT)
import qualified Data.CQRS.Command as C
import Data.CQRS.Query
import Data.CQRS.Repository
import Data.CQRS.Types.EventStore (EventStore)
import Data.CQRS.Types.SnapshotStore (nullSnapshotStore, SnapshotStore)
import Data.CQRS.Test.Internal.AggregateAction (byteStringAggregateAction)
import Data.CQRS.Test.Internal.Scope (ScopeM, verify, randomUUID, mkRunScope)
import Data.CQRS.Test.Internal.TestKitSettings
import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef')
import Data.Maybe (fromJust)
import Data.UUID.Types (UUID)
import qualified System.Random as R
import qualified Test.Hspec as Hspec
import Test.Hspec (Spec, shouldBe)
import Test.HUnit (assertBool)
-- Ambient data for test scope for each spec.
data Scope a e = Scope { scopeRepository :: Repository a e
, scopePublishedEvents :: IORef [(UUID, e, Int)]
}
-- Assert that the given list of events was published.
assertDidPublish :: (Show e, Eq e) => [(UUID, e, Int)] -> ScopeM (Scope a e) ()
assertDidPublish expectedEvents = do
publishedEventsRef <- fmap scopePublishedEvents $ ask
verify $ do
publishedEvents <- readIORef publishedEventsRef
length publishedEvents `shouldBe` length expectedEvents
forM_ (zip publishedEvents expectedEvents) $ uncurry shouldBe
-- Get the repository which is in scope.
getRepository :: ScopeM (Scope a e) (Repository a e)
getRepository = fmap scopeRepository ask
-- Run a command in scope.
runCommandT :: CommandT a e IO r -> ScopeM (Scope a e) r
runCommandT command = do
repository <- getRepository
liftIO $ C.runCommandT repository command
-- Create a new test scope runner from the test kit settings.
mkRunScope' :: Int -> TestKitSettings s (EventStore ByteString, SnapshotStore ByteString) -> (ScopeM (Scope ByteString ByteString) r -> IO r)
mkRunScope' snapshotFrequency testKitSettings = mkRunScope testKitSettings $ \a -> do
-- We collect all events published by the repository for verification
publishedEventsRef <- newIORef []
let publish (aggregateId, events') = atomicModifyIORef' publishedEventsRef $ \events ->
(events ++ map (\(PersistedEvent e s _) -> (aggregateId, e, s)) events', ())
-- Repository setup
(eventStore, snapshotStore) <- (tksMakeContext testKitSettings) a
let settings = setSnapshotFrequency snapshotFrequency $ defaultSettings
let repository = newRepository settings byteStringAggregateAction eventStore snapshotStore publish R.randomIO
-- Build the ambient state.
return $ Scope repository publishedEventsRef
-- Given test kit settings, create the full spec for testing the
-- repository implementation against those settings.
mkRepositorySpec :: TestKitSettings a (EventStore ByteString, SnapshotStore ByteString) -> Spec
mkRepositorySpec testKitSettings = do
-- We do each set of tests both *with* and *without* a snapshot
-- store and with varying snapshot frequency. This should hopefully
-- give us enough coverage against the handling of snapshots in the
-- Repository portion of the code.
forM_ [ 3, 5 ] $ \f -> do
let fs = "frequency " ++ show f
let s1 = "(snapshots; " ++ fs ++ ")"
let s2 = "(null snapshots; " ++ fs ++ ")"
mkSpec s1 (mkRunScope' f $ testKitSettings)
mkSpec s2 (mkRunScope' f $ disableSnapshots testKitSettings)
where
disableSnapshots settings =
settings { tksMakeContext = \a -> do
(eventStore, _) <- tksMakeContext settings a
return (eventStore, nullSnapshotStore)
}
mkSpec :: String -> (ScopeM (Scope ByteString ByteString) () -> IO ()) -> Spec
mkSpec suffix runScope = do
describe "Repository" $ do
it "should support creating an aggregate and returning its value" $ do
-- Exercise
(aggregateId, a) <- newAggregate ["3"]
-- Should have updated aggregate value
verify $ a `shouldBe` "3"
-- Should have published appropriate event
assertDidPublish [ (aggregateId, "3", 0) ]
it "should support creating an aggregate and loading it" $ do
-- Exercise
(aggregateId, _) <- newAggregate ["4"]
-- Should have an updated aggregate value
a <- loadAggregate aggregateId
verify $ a `shouldBe` "4"
-- Should have published appropriate event
assertDidPublish [ (aggregateId, "4", 0) ]
it "should support publishing >1 events to an aggregate (1 txn)" $ do
-- Exercise
(aggregateId, _) <- newAggregate ["7", "1"]
-- Should have an updated aggregate value
a <- loadAggregate aggregateId
verify $ a `shouldBe` "71"
-- Should have published two events
assertDidPublish [ (aggregateId, "7", 0)
, (aggregateId, "1", 1)
]
it "should support publishing >1 events to an aggregate (2 txns)" $ do
-- Exercise: 1st transaction
(aggregateId, _) <- newAggregate ["9"]
-- Exercise: 2nd transaction
_ <- runCommandT $ do
C.updateAggregate aggregateId $ \_ -> do
C.publishEvent $ "7"
-- Should have an updated aggregate value.
a <- loadAggregate aggregateId
verify $ a `shouldBe` "97"
-- Should have published two events.
assertDidPublish [ (aggregateId, "9", 0)
, (aggregateId, "7", 1)
]
it "should support publishing a large number of events to an aggregate" $ do
-- Setup
let events = map B8.pack $ map show ([1.. 100] :: [Int])
-- Exercise
(aggregateId, _) <- newAggregate events
-- Verify
a <- loadAggregate aggregateId
verify $ a `shouldBe` B.concat events
it "should be possible to find an existing aggregate" $ do
-- Setup
(aggregateId, _) <- newAggregate ["xyzzy"]
-- Exercise
a <- findAggregate aggregateId
-- Should have found it
verify $ a `shouldBe` Just "xyzzy"
it "should not be possible to find a non-existent aggregate" $ do
-- Exercise
aggregateId <- randomUUID
a <- findAggregate aggregateId
-- Should NOT have found anything
verify $ a `shouldBe` Nothing
it "should be possible to work with two different aggregates (serially) in a command" $ do
-- Setup
aggregateId0 <- randomUUID
aggregateId1 <- randomUUID
-- Exercise
_ <- runCommandT $ do
C.createAggregate aggregateId0 $ \_ -> do
C.publishEvent "34"
C.createAggregate aggregateId1 $ \_ -> do
C.publishEvent "1"
C.updateAggregate aggregateId0 $ \_ -> do
C.publishEvent "5"
-- Should have updated values for both aggregates
a0 <- loadAggregate aggregateId0
a1 <- loadAggregate aggregateId1
verify $ a0 `shouldBe` "345"
verify $ a1 `shouldBe` "1"
-- Should have published events in order of publishing
assertDidPublish [ (aggregateId0, "34", 0)
, (aggregateId1, "1", 0)
, (aggregateId0, "5", 1)
]
it "'getter' function returns up-to-date values when updating an aggregate" $ do
-- Setup
(aggregateId, _) <- newAggregate ["x"]
-- Exercise:
Just (a, a') <- runCommandT $ do -- We'll assume pattern match will work, otherwise test fails
C.updateAggregate aggregateId $ \get -> do
a <- get
C.publishEvent "y"
a' <- get
return (a, a')
-- Should have received original value in a'
verify $ a `shouldBe` "x"
-- Should have received an updated value in a''
verify $ a' `shouldBe` "xy"
-- Should have published events
assertDidPublish [ (aggregateId, "x", 0)
, (aggregateId, "y", 1)
]
it "'getter' function returns up-to-date values when creating an aggregate" $ do
-- Setup
aggregateId <- randomUUID
-- Exercise
(a, a') <- runCommandT $ do
C.createAggregate aggregateId $ \get -> do
a <- get -- Should return Nothing
C.publishEvent "x"
a' <- get -- Should reutrn (Just "x")
return (a, a')
-- Should have received Nothing in a' since aggregate didn't actually exist
verify $ a `shouldBe` Nothing
-- Should have received (Just "x") in a'' since we'd just published an "x" event
verify $ a' `shouldBe` (Just "x")
-- Should have published event
assertDidPublish [ (aggregateId, "x", 0) ]
where
-- Boilerplate avoidance
it msg scope = Hspec.it msg $ runScope scope
describe msg = Hspec.describe (msg ++ " " ++ suffix)
-- Create an aggregate with an initial series of events.
newAggregate :: (NFData a, NFData e) => [e] -> ScopeM (Scope a e) (UUID, a)
newAggregate es = do
-- Create new aggregate ID.
aggregateId <- randomUUID
-- Sanity check
liftIO $ assertBool "List of initial events must be non-emtpy" (length es > 0)
-- Create the aggregate with its initial list of events
runCommandT $ do
a <- C.createAggregate aggregateId $ \getAggregate -> do
forM_ es $ C.publishEvent
liftM fromJust $ getAggregate
return (aggregateId, a)
-- Load an aggregate value.
loadAggregate :: UUID -> ScopeM (Scope a e) a
loadAggregate aggregateId = liftM get $ runCommandT $ C.readAggregate aggregateId
where
get Nothing = error $ "loadAggregate: Missing expected aggregate"
get (Just a) = a
-- Get aggregate's value, if the aggregate exists
findAggregate :: UUID -> ScopeM (Scope a e) (Maybe a)
findAggregate = do
runCommandT . (flip C.updateAggregate) id