packages feed

keiki-0.1.0.0: test/Keiki/CoreApplyEventsSpec.hs

module Keiki.CoreApplyEventsSpec (spec) where

import Data.Time (UTCTime (..), fromGregorian, secondsToDiffTime)
import Keiki.Core (applyEvents, initial, initialRegs, (!))
import Keiki.Fixtures.UserRegistration
import Test.Hspec

-- | A trivial UTC-time fixture matching 'UserRegistrationSpec's
-- convention: every test moment is on the same day, offset by N
-- seconds.
t :: Integer -> UTCTime
t s = UTCTime (fromGregorian 2026 5 1) (secondsToDiffTime s)

-- | The synthesis §4 canonical event log, identical to the one
-- exercised by 'Keiki.Fixtures.UserRegistrationSpec'. Reproduced
-- here so the spec is self-contained.
canonicalLog :: [UserEvent]
canonicalLog =
  [ RegistrationStarted (RegistrationStartedData "alice@x" "Z9F4" (t 0)),
    ConfirmationEmailSent (ConfirmationEmailSentData "alice@x"),
    ConfirmationResent (ConfirmationResentData "alice@x" "K2P7" (t 100)),
    AccountConfirmed (AccountConfirmedData "alice@x" "K2P7" (t 200)),
    AccountDeleted (AccountDeletedData "alice@x" (t 300))
  ]

-- | The two events that 'StartRegistration' produces under state
-- refinement: the public emission and the synthetic 'Continue'-driven
-- emission. Together they take @PotentialCustomer@ to
-- @RequiresConfirmation@ via the intermediate @Registering@.
multiEventChunk :: [UserEvent]
multiEventChunk =
  [ RegistrationStarted (RegistrationStartedData "alice@x" "Z9F4" (t 0)),
    ConfirmationEmailSent (ConfirmationEmailSentData "alice@x")
  ]

spec :: Spec
spec = do
  describe "applyEvents folds applyEvent over a chunk" $ do
    it "round-trips the canonical 5-event log from the initial state" $
      case applyEvents userReg (initial userReg, initialRegs userReg) canonicalLog of
        Just (s, regs) ->
          (s, regs ! #email, regs ! #confirmCode)
            `shouldBe` (Deleted, "alice@x", "K2P7")
        Nothing -> expectationFailure "applyEvents returned Nothing"

    it "replays a 2-event chunk for one logical command from PotentialCustomer" $
      -- StartRegistration's two events: RegistrationStarted +
      -- ConfirmationEmailSent. Under state refinement the chunk passes
      -- through Registering and lands at RequiresConfirmation with the
      -- command's register writes applied.
      case applyEvents userReg (PotentialCustomer, emptyRegs) multiEventChunk of
        Just (s, regs) -> do
          s `shouldBe` RequiresConfirmation
          (regs ! #email, regs ! #confirmCode, regs ! #registeredAt)
            `shouldBe` ("alice@x", "Z9F4", t 0)
        Nothing -> expectationFailure "applyEvents returned Nothing"

    it "returns Nothing for an out-of-order event sequence" $
      -- ConfirmationEmailSent is not the prefix of any active edge's
      -- output at PotentialCustomer (the only outgoing letter edge
      -- emits RegistrationStarted). The fold short-circuits to
      -- Nothing. We pattern-match manually because 'RegFile
      -- UserRegRegs' has no 'Show' instance, so 'shouldBe' /
      -- 'shouldSatisfy' on the raw 'Maybe' do not type-check.
      case applyEvents
        userReg
        (PotentialCustomer, emptyRegs)
        [ConfirmationEmailSent (ConfirmationEmailSentData "alice@x")] of
        Nothing -> pure ()
        Just _ ->
          expectationFailure
            "applyEvents accepted an out-of-order event"