kiroku-store-0.6.0.0: test/Test/VisibleGlobalHeadPosition.hs
{-# LANGUAGE TypeApplications #-}
module Test.VisibleGlobalHeadPosition (spec) where
import Control.Lens ((&), (.~), (^.))
import Data.Aeson qualified as Aeson
import Data.Generics.Labels ()
import Data.IORef (modifyIORef', newIORef, readIORef)
import Effectful (runEff)
import Effectful.Error.Static (runErrorNoCallStack)
import Kiroku.Store
import Test.Helpers (makeEvent, withTestStore, withTestStoreSettings)
import Test.Hspec
spec :: Spec
spec = describe "visible global head position" $ do
it "returns zero for an empty migrated store through both public runners" $
withTestStore $ \store -> do
runStoreIO store visibleGlobalHeadPosition
`shouldReturn` Right (GlobalPosition 0)
resourceResult <-
runEff
. runErrorNoCallStack @StoreError
. runKirokuStoreWith store
. runStoreResource
$ visibleGlobalHeadPosition
resourceResult `shouldBe` Right (GlobalPosition 0)
it "returns the greatest appended position" $
withTestStore $ \store -> do
appendEvents store (StreamName "visible-head-populated") 3
runStoreIO store visibleGlobalHeadPosition
`shouldReturn` Right (GlobalPosition 3)
it "falls back across hard-deleted tails while the append frontier stays monotonic" $
withTestStore $ \store -> do
let first = StreamName "visible-head-first"
middle = StreamName "visible-head-middle"
lastStream = StreamName "visible-head-last"
appendEvents store first 1
appendEvents store middle 1
appendEvents store lastStream 1
Right (Just _) <- runStoreIO store $ hardDeleteStream middle
assertHeadAndFrontier store 3 3
Right (Just _) <- runStoreIO store $ hardDeleteStream lastStream
assertHeadAndFrontier store 1 3
Right (Just _) <- runStoreIO store $ hardDeleteStream first
assertHeadAndFrontier store 0 3
it "keeps logically truncated and soft-deleted events visible in $all" $
withTestStore $ \store -> do
let name = StreamName "visible-head-logical-lifecycle"
appendEvents store name 3
Right (Just _) <-
runStoreIO store $
setStreamTruncateBefore name (StreamVersion 3)
runStoreIO store visibleGlobalHeadPosition
`shouldReturn` Right (GlobalPosition 3)
Right (Just _) <- runStoreIO store $ softDeleteStream name
runStoreIO store visibleGlobalHeadPosition
`shouldReturn` Right (GlobalPosition 3)
it "does not invoke the event decode hook" $ do
decodeCalls <- newIORef (0 :: Int)
let failingHook event = do
modifyIORef' decodeCalls (+ 1)
ioError (userError ("unexpected decode of " <> show (event ^. #eventId)))
tweak settings =
settings
& #storeSettings
.~ defaultStoreSettings{decodeHook = Just failingHook}
withTestStoreSettings tweak $ \store -> do
appendEvents store (StreamName "visible-head-no-decode") 1
runStoreIO store visibleGlobalHeadPosition
`shouldReturn` Right (GlobalPosition 1)
readIORef decodeCalls `shouldReturn` 0
appendEvents :: KirokuStore -> StreamName -> Int -> IO ()
appendEvents store name count = do
let events =
[ makeEvent "VisibleHeadEvent" (Aeson.object [("ordinal", Aeson.Number (fromIntegral ordinal))])
| ordinal <- [1 .. count]
]
result <- runStoreIO store $ appendToStream name NoStream events
case result of
Left err -> expectationFailure ("append failed: " <> show err)
Right _ -> pure ()
assertHeadAndFrontier :: KirokuStore -> Integer -> Integer -> IO ()
assertHeadAndFrontier store expectedHead expectedFrontier = do
runStoreIO store visibleGlobalHeadPosition
`shouldReturn` Right (GlobalPosition (fromIntegral expectedHead))
result <- runStoreIO store subscriptionCheckpointInventory
case result of
Left err -> expectationFailure ("inventory read failed: " <> show err)
Right inventory ->
inventory ^. #storePosition
`shouldBe` GlobalPosition (fromIntegral expectedFrontier)