packages feed

keiki-0.2.0.0: test/Keiki/AcceptorSpec.hs

module Keiki.AcceptorSpec (spec) where

import Data.Time (UTCTime (..), fromGregorian, secondsToDiffTime)
import Keiki.Acceptor
import Keiki.Core (InFlight (..), initialRegs, isFinal, reconstitute)
import Keiki.Fixtures.EmailDelivery
  ( EmailEvent (..),
    EmailSentData (..),
    emailDelivery,
  )
import Keiki.Fixtures.UserRegistration
  ( AccountConfirmedData (..),
    AccountDeletedData (..),
    ConfirmAccountData (..),
    ConfirmationEmailSentData (..),
    ConfirmationResentData (..),
    FulfillGDPRRequestData (..),
    RegistrationStartedData (..),
    StartRegistrationData (..),
    UserCmd (..),
    UserEvent (..),
    Vertex (..),
    userReg,
  )
import Test.Hspec

-- | A trivial UTC-time fixture: 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 canonical four-step command sequence on 'userReg' that lands
-- in the final 'Deleted' vertex.
--
--   PotentialCustomer    --StartRegistration->  Registering
--   Registering          --Continue->           RequiresConfirmation
--   RequiresConfirmation --ConfirmAccount->     Confirmed
--   Confirmed            --FulfillGDPRRequest-> Deleted
--
-- The 'ConfirmAccount' code matches the code stored at registration,
-- so the @PEq@ guard on the confirmation edge is satisfied.
-- EP-19 M7: 'Continue' retired with the collapsed entrance.
-- StartRegistration alone now drives PotentialCustomer →
-- RequiresConfirmation as one transition emitting both
-- RegistrationStarted and ConfirmationEmailSent.
canonicalUserCmds :: [UserCmd]
canonicalUserCmds =
  [ StartRegistration (StartRegistrationData "alice@x" "Z9F4" (t 0)),
    ConfirmAccount (ConfirmAccountData "Z9F4" (t 100)),
    FulfillGDPRRequest (FulfillGDPRRequestData (t 200))
  ]

-- | The canonical event log on 'emailDelivery' that lands in the
-- terminal 'EmailSentVertex'.
canonicalEmailLog :: [EmailEvent]
canonicalEmailLog =
  [EmailSent (EmailSentData "alice@x" "Welcome" (t 0))]

canonicalUserLog :: [UserEvent]
canonicalUserLog =
  [ 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))
  ]

spec :: Spec
spec = do
  describe "inputAcceptor userReg" $ do
    it "accepts the canonical command sequence" $
      accepts (inputAcceptor userReg) canonicalUserCmds
        `shouldBe` True

    it "rejects ConfirmAccount from PotentialCustomer" $
      accepts
        (inputAcceptor userReg)
        [ConfirmAccount (ConfirmAccountData "Z9F4" (t 0))]
        `shouldBe` False

  describe "outputAcceptor emailDelivery" $ do
    it "accepts the canonical event log" $
      accepts (outputAcceptor emailDelivery) canonicalEmailLog
        `shouldBe` True

    it "rejects an event that no edge from PotentialCustomer produces" $ do
      -- userReg's only outgoing edge from PotentialCustomer produces
      -- 'RegistrationStarted'; 'AccountConfirmed' has no matching
      -- inverse, so applyEvent returns Nothing on the first step.
      let badLog =
            [ AccountConfirmed
                (AccountConfirmedData "alice@x" "Z9F4" (t 0))
            ]
      accepts (outputAcceptor userReg) badLog `shouldBe` False

    it "agrees with reconstitute on the canonical log" $
      accepts (outputAcceptor emailDelivery) canonicalEmailLog
        `shouldBe` maybe
          False
          (isFinal emailDelivery . fst)
          (reconstitute emailDelivery canonicalEmailLog)

  describe "outputAcceptor userReg multi-event replay" $
    do
      it "accepts the canonical multi-event log" $
        accepts (outputAcceptor userReg) canonicalUserLog `shouldBe` True

      it "rejects a truncated chain while preserving its InFlight carrier" $ do
        accepts (outputAcceptor userReg) (take 1 canonicalUserLog)
          `shouldBe` False
        fmap fst (runAcceptor (outputAcceptor userReg) (take 1 canonicalUserLog))
          `shouldBe` Just
            ( InFlight
                RequiresConfirmation
                [ConfirmationEmailSent (ConfirmationEmailSentData "alice@x")]
            )

      it "agrees with reconstitute for complete, truncated, and foreign logs" $ do
        let foreignLog =
              [AccountConfirmed (AccountConfirmedData "alice@x" "Z9F4" (t 0))]
        accepts (outputAcceptor userReg) canonicalUserLog
          `shouldBe` maybe
            False
            (isFinal userReg . fst)
            (reconstitute userReg canonicalUserLog)
        accepts (outputAcceptor userReg) (take 1 canonicalUserLog)
          `shouldBe` maybe
            False
            (isFinal userReg . fst)
            (reconstitute userReg (take 1 canonicalUserLog))
        accepts (outputAcceptor userReg) foreignLog
          `shouldBe` maybe
            False
            (isFinal userReg . fst)
            (reconstitute userReg foreignLog)

  describe "aIsFinal" $ do
    it "matches isFinal on userReg under fst" $ do
      let a = inputAcceptor userReg
      aIsFinal a (Deleted, initialRegs userReg) `shouldBe` True
      aIsFinal a (Confirmed, initialRegs userReg) `shouldBe` False
      aIsFinal a (PotentialCustomer, initialRegs userReg) `shouldBe` False
      -- And the same predicate on the bare vertex.
      isFinal userReg Deleted `shouldBe` True
      isFinal userReg Confirmed `shouldBe` False