package keeper_test // msg_server_simtest_test.go is the x/partner keeper simtest (P3-03-01, // REQ-035). // // D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no // real hub/watcher keepers. The simtest wires the expected-keeper shims // (WatcherKeeper, HubKeeper) to in-test stubs (G-003 test exemption: the // test imports x/partner/keeper + defines stub types that satisfy the // interfaces; no production struct imports across x//types). // // Coverage (REQ-035 lifecycle Pending → Onboarded → Suspended → Revoked): // - Full success lifecycle: Issue (Pending) → Onboard → Suspend → Revoke. // - Invalid transitions REJECTED: // - Onboard on a non-Pending credential (Onboarded/Suspended/Revoked // source) → error. // - Suspend on a non-Onboarded credential (Pending/Suspended/Revoked // source) → error. // - Revoke on an already-Revoked credential → idempotent reject (no // double-effect). // - HubKeeper shim wiring (P3→P4 hub dep edge): // - OnboardAnchor with a custody-provider-id that the HubKeeper stub // reports as non-existent → REJECTED (credential stays Pending). // - OnboardAnchor with a custody-provider-id that the HubKeeper stub // reports as existent → transitions to Onboarded. // - Nil HubKeeper shim → skips the check (simtest wiring); the // OnboardAnchor transitions regardless. // - Watcher quorum authz (REQ-004 6-of-9): // - IssueAnchorCredential with a WatcherKeeper stub that reports // quorum NOT signed → REJECTED (credential NOT created). // - IssueAnchorCredential with quorum signed → credential created // (Pending). // - RevokeAnchorCredential with quorum NOT signed → REJECTED // (credential stays in its pre-revoke status). // - Nil WatcherKeeper shim → skips the authz (simtest wiring); the // handler mutates state. // - Idempotency: IssueAnchorCredential on an existing anchor-id → // error. // - NotFound: Onboard/Suspend/Revoke on a missing anchor-id → error. // - ValidateBasic: each Msg* ValidateBasic error path. // // Coverage target: ≥80% on x/partner/keeper. import ( "testing" "time" "cosmossdk.io/log" "cosmossdk.io/store" storetypes "cosmossdk.io/store/types" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" dbm "github.com/cosmos/cosmos-db" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/oy/openyield/x/partner/keeper" ptypes "github.com/oy/openyield/x/partner/types" ) // --- Stub expected-keepers (G-003 test exemption) --------------------------- // stubWatcherKeeper satisfies ptypes.WatcherKeeper for the simtest. It // records IsQuorumSigned calls for assertion and returns the configured // result (true by default — quorum signed). type stubWatcherKeeper struct { calls []watcherCall signedResult bool // configurable; default true (quorum signed) } type watcherCall struct { quorumID string payload []byte } func (s *stubWatcherKeeper) IsQuorumSigned(quorumID string, payload []byte) bool { s.calls = append(s.calls, watcherCall{quorumID, payload}) return s.signedResult } // stubHubKeeper satisfies ptypes.HubKeeper for the simtest. It records // CustodyServiceExists calls for assertion and returns the configured // result per custody-provider-id (default: exists=true). type stubHubKeeper struct { calls []hubCall exists map[string]bool // custody-provider-id → exists existsAll bool // if true, CustodyServiceExists returns true for all ids } type hubCall struct { custodyProviderID string } func (s *stubHubKeeper) CustodyServiceExists(custodyProviderID string) bool { s.calls = append(s.calls, hubCall{custodyProviderID}) if s.exists != nil { return s.exists[custodyProviderID] } return s.existsAll } // --- Simtest context helper -------------------------------------------------- // newSimtestContext constructs an in-memory sdk.Context with a KVStore // mounted at the partner store key. D-054: in-memory, no real hub/watcher // keepers. Returns the ctx, the stub WatcherKeeper, the stub HubKeeper, // and the Keeper. func newSimtestContext(t *testing.T) (sdk.Context, *stubWatcherKeeper, *stubHubKeeper, keeper.Keeper) { t.Helper() db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(ptypes.StoreKey) cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil) cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil) if err := cms.LoadLatestVersion(); err != nil { t.Fatalf("load latest version: %v", err) } // Block time set to a fixed unix second so lifecycle timestamps are // deterministic (now = 1000). ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger()) wk := &stubWatcherKeeper{signedResult: true} hk := &stubHubKeeper{existsAll: true} k := keeper.NewKeeper(cdc, storeKey, wk, hk) return ctx, wk, hk, k } // newTestCodec constructs a minimal codec for the simtest. func newTestCodec() codec.Codec { registry := codectypes.NewInterfaceRegistry() return codec.NewProtoCodec(registry) } // hasEvent reports whether ctx emitted an event of the given type. func hasEvent(ctx sdk.Context, eventType string) bool { for _, ev := range ctx.EventManager().Events() { if ev.Type == eventType { return true } } return false } // eventAttr returns the value of an attribute on the last event of the // given type, or "" if not found. func eventAttr(ctx sdk.Context, eventType, attrKey string) string { for _, ev := range ctx.EventManager().Events() { if ev.Type == eventType { for _, a := range ev.Attributes { if string(a.Key) == attrKey { return string(a.Value) } } } } return "" } // --- Full success lifecycle: Pending → Onboarded → Suspended → Revoked -------- // TestAnchorCredentialLifecycleFullSuccess asserts the full success // lifecycle: Issue (Pending) → Onboard (Onboarded) → Suspend (Suspended) // → Revoke (Revoked). func TestAnchorCredentialLifecycleFullSuccess(t *testing.T) { ctx, wk, hk, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) // Issue → Pending. if _, err := srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "anchor-1", CredentialURI: "oy:cred:anchor-1/EU-MiCA", WatcherQuorumID: "quorum-6of9", Issuer: "issuer-1", Signer: "issuer-1", }); err != nil { t.Fatalf("IssueAnchorCredential: %v", err) } c, ok := k.GetAnchorCredential(ctx, "anchor-1") if !ok { t.Fatal("anchor credential not found after issue") } if c.Status != ptypes.AnchorPending { t.Errorf("status = %q, want Pending", c.Status) } if c.CredentialURI != "oy:cred:anchor-1/EU-MiCA" { t.Errorf("credential-uri = %q", c.CredentialURI) } if !hasEvent(ctx, "partner.anchor_credential_issued") { t.Error("anchor_credential_issued event not emitted") } // Watcher quorum was consulted. if len(wk.calls) != 1 { t.Errorf("watcher calls = %d, want 1 (issuance authz)", len(wk.calls)) } // Onboard → Onboarded. if _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{ AnchorID: "anchor-1", CustodyProviderID: "hub-custody-1", Signer: "issuer-1", }); err != nil { t.Fatalf("OnboardAnchor: %v", err) } c, _ = k.GetAnchorCredential(ctx, "anchor-1") if c.Status != ptypes.AnchorOnboarded { t.Errorf("status = %q, want Onboarded", c.Status) } if c.CustodyProviderID != "hub-custody-1" { t.Errorf("custody-provider-id = %q, want hub-custody-1", c.CustodyProviderID) } if !hasEvent(ctx, "partner.anchor_onboarded") { t.Error("anchor_onboarded event not emitted") } // HubKeeper was consulted. if len(hk.calls) != 1 { t.Errorf("hub calls = %d, want 1 (custody-service-exists check)", len(hk.calls)) } if hk.calls[0].custodyProviderID != "hub-custody-1" { t.Errorf("hub call custody-provider-id = %q, want hub-custody-1", hk.calls[0].custodyProviderID) } // Suspend → Suspended. if _, err := srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{ AnchorID: "anchor-1", Signer: "issuer-1", }); err != nil { t.Fatalf("SuspendAnchorCredential: %v", err) } c, _ = k.GetAnchorCredential(ctx, "anchor-1") if c.Status != ptypes.AnchorSuspended { t.Errorf("status = %q, want Suspended", c.Status) } if !hasEvent(ctx, "partner.anchor_credential_suspended") { t.Error("anchor_credential_suspended event not emitted") } // Revoke → Revoked (terminal). if _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{ AnchorID: "anchor-1", WatcherQuorumID: "quorum-6of9", Signer: "watcher-1", }); err != nil { t.Fatalf("RevokeAnchorCredential: %v", err) } c, _ = k.GetAnchorCredential(ctx, "anchor-1") if c.Status != ptypes.AnchorRevoked { t.Errorf("status = %q, want Revoked", c.Status) } if !hasEvent(ctx, "partner.anchor_credential_revoked") { t.Error("anchor_credential_revoked event not emitted") } // Watcher quorum consulted again (revocation authz). if len(wk.calls) != 2 { t.Errorf("watcher calls = %d, want 2 (issuance + revocation authz)", len(wk.calls)) } } // --- Pending → Revoked (skip Onboard/Suspend) ------------------------------- // TestAnchorCredentialRevokeFromPending asserts a Pending credential can // be revoked directly (Pending → Revoked is a valid transition per // ValidAnchorTransition). func TestAnchorCredentialRevokeFromPending(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "anchor-pend", CredentialURI: "oy:cred:x", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) if _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{ AnchorID: "anchor-pend", WatcherQuorumID: "q", Signer: "w", }); err != nil { t.Fatalf("RevokeAnchorCredential from Pending: %v", err) } c, _ := k.GetAnchorCredential(ctx, "anchor-pend") if c.Status != ptypes.AnchorRevoked { t.Errorf("status = %q, want Revoked", c.Status) } } // --- Suspended → Revoked ---------------------------------------------------- // TestAnchorCredentialRevokeFromSuspended asserts a Suspended credential // can be revoked (Suspended → Revoked is a valid transition). func TestAnchorCredentialRevokeFromSuspended(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "anchor-sus", CredentialURI: "oy:cred:x", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{ AnchorID: "anchor-sus", CustodyProviderID: "hc-1", Signer: "i", }) srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{ AnchorID: "anchor-sus", Signer: "i", }) if _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{ AnchorID: "anchor-sus", WatcherQuorumID: "q", Signer: "w", }); err != nil { t.Fatalf("RevokeAnchorCredential from Suspended: %v", err) } c, _ := k.GetAnchorCredential(ctx, "anchor-sus") if c.Status != ptypes.AnchorRevoked { t.Errorf("status = %q, want Revoked", c.Status) } } // --- Invalid transitions REJECTED ------------------------------------------- // TestOnboardRejectsNonPending asserts OnboardAnchor on a non-Pending // credential is REJECTED (the lifecycle gate). Covers Onboarded, // Suspended, and Revoked source statuses. func TestOnboardRejectsNonPending(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) // Onboarded source → reject (issue + onboard first). srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-ob", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-ob", CustodyProviderID: "hc", Signer: "i"}) _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-ob", CustodyProviderID: "hc", Signer: "i"}) if err == nil { t.Error("OnboardAnchor on Onboarded credential should be rejected (lifecycle gate)") } // Suspended source → reject. srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-sus", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-sus", CustodyProviderID: "hc", Signer: "i"}) srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-sus", Signer: "i"}) _, err = srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-sus", CustodyProviderID: "hc", Signer: "i"}) if err == nil { t.Error("OnboardAnchor on Suspended credential should be rejected (lifecycle gate)") } // Revoked source → reject. srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-rev", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{AnchorID: "a-rev", WatcherQuorumID: "q", Signer: "w"}) _, err = srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-rev", CustodyProviderID: "hc", Signer: "i"}) if err == nil { t.Error("OnboardAnchor on Revoked credential should be rejected (lifecycle gate)") } } // TestSuspendRejectsNonOnboarded asserts SuspendAnchorCredential on a // non-Onboarded credential is REJECTED. Covers Pending, Suspended, and // Revoked source statuses. func TestSuspendRejectsNonOnboarded(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) // Pending source → reject. srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-pend", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) _, err := srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-pend", Signer: "i"}) if err == nil { t.Error("SuspendAnchorCredential on Pending credential should be rejected (lifecycle gate)") } // Suspended source → reject (suspend an already-suspended). srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-sus2", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{AnchorID: "a-sus2", CustodyProviderID: "hc", Signer: "i"}) srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-sus2", Signer: "i"}) _, err = srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-sus2", Signer: "i"}) if err == nil { t.Error("SuspendAnchorCredential on Suspended credential should be rejected (lifecycle gate)") } // Revoked source → reject. srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-rev2", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{AnchorID: "a-rev2", WatcherQuorumID: "q", Signer: "w"}) _, err = srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{AnchorID: "a-rev2", Signer: "i"}) if err == nil { t.Error("SuspendAnchorCredential on Revoked credential should be rejected (lifecycle gate)") } } // TestRevokeRejectsAlreadyRevoked asserts a second Revoke on a Revoked // credential is REJECTED (idempotent reject — no double-effect). func TestRevokeRejectsAlreadyRevoked(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-rev3", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{AnchorID: "a-rev3", WatcherQuorumID: "q", Signer: "w"}) // Second revoke → idempotent reject. _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{AnchorID: "a-rev3", WatcherQuorumID: "q", Signer: "w"}) if err == nil { t.Error("RevokeAnchorCredential on Revoked credential should be rejected (idempotent reject — no double-effect)") } } // --- HubKeeper shim wiring (P3→P4 hub dep edge) ----------------------------- // TestOnboardRejectsWhenCustodyServiceMissing asserts OnboardAnchor is // REJECTED when the HubKeeper shim reports the custody service does not // exist (the credential stays Pending). This is the P3→P4 hub dep edge // test (G-003 test exemption — the HubKeeper shim is a stub). func TestOnboardRejectsWhenCustodyServiceMissing(t *testing.T) { ctx, _, hk, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-hub", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) // Configure the HubKeeper stub to report the custody service as // NON-existent for "missing-custody". hk.existsAll = false hk.exists = map[string]bool{"missing-custody": false} _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{ AnchorID: "a-hub", CustodyProviderID: "missing-custody", Signer: "i", }) if err == nil { t.Error("OnboardAnchor should be rejected when custody service does not exist (P3→P4 hub dep edge)") } // Credential stays Pending. c, _ := k.GetAnchorCredential(ctx, "a-hub") if c.Status != ptypes.AnchorPending { t.Errorf("status = %q, want Pending (onboarding rejected — credential stays Pending)", c.Status) } // Custody-provider-id NOT set. if c.CustodyProviderID != "" { t.Errorf("custody-provider-id = %q, want empty (onboarding rejected)", c.CustodyProviderID) } } // TestOnboardSucceedsWhenCustodyServiceExists asserts OnboardAnchor // SUCCEEDS when the HubKeeper shim reports the custody service exists // (the credential transitions to Onboarded). func TestOnboardSucceedsWhenCustodyServiceExists(t *testing.T) { ctx, _, hk, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-hub2", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) // Configure the HubKeeper stub to report the custody service as // existent for "good-custody". hk.existsAll = false hk.exists = map[string]bool{"good-custody": true} if _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{ AnchorID: "a-hub2", CustodyProviderID: "good-custody", Signer: "i", }); err != nil { t.Fatalf("OnboardAnchor should succeed when custody service exists: %v", err) } c, _ := k.GetAnchorCredential(ctx, "a-hub2") if c.Status != ptypes.AnchorOnboarded { t.Errorf("status = %q, want Onboarded", c.Status) } } // TestOnboardNilHubKeeperSkipsCheck asserts a nil HubKeeper shim skips // the custody-service-exists check (simtest wiring); the OnboardAnchor // transitions regardless. This documents the wiring contract for the // P3→P4 hub dep edge: P3 simtest may use a nil shim; P4 wires the real // hub keeper. func TestOnboardNilHubKeeperSkipsCheck(t *testing.T) { db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(ptypes.StoreKey) cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil) cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil) cms.LoadLatestVersion() ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger()) // Nil HubKeeper shim. k := keeper.NewKeeper(cdc, storeKey, &stubWatcherKeeper{signedResult: true}, nil) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-nil", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) if _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{ AnchorID: "a-nil", CustodyProviderID: "any-custody", Signer: "i", }); err != nil { t.Fatalf("OnboardAnchor with nil HubKeeper shim should succeed (check skipped): %v", err) } c, _ := k.GetAnchorCredential(ctx, "a-nil") if c.Status != ptypes.AnchorOnboarded { t.Errorf("status = %q, want Onboarded (nil shim skips check)", c.Status) } } // --- Watcher quorum authz (REQ-004 6-of-9) ---------------------------------- // TestIssueRejectsWhenQuorumNotSigned asserts IssueAnchorCredential is // REJECTED when the WatcherKeeper stub reports the quorum NOT signed // (the credential is NOT created). func TestIssueRejectsWhenQuorumNotSigned(t *testing.T) { ctx, wk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) wk.signedResult = false // quorum NOT signed _, err := srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-q", CredentialURI: "u", WatcherQuorumID: "q-6of9", Issuer: "i", Signer: "i", }) if err == nil { t.Error("IssueAnchorCredential should be rejected when watcher quorum not signed (REQ-004 6-of-9)") } // Credential NOT created. if _, ok := k.GetAnchorCredential(ctx, "a-q"); ok { t.Error("anchor credential should NOT be created when issuance authz fails") } } // TestRevokeRejectsWhenQuorumNotSigned asserts RevokeAnchorCredential is // REJECTED when the WatcherKeeper stub reports the quorum NOT signed // (the credential stays in its pre-revoke status). func TestRevokeRejectsWhenQuorumNotSigned(t *testing.T) { ctx, wk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-rq", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) // Flip watcher to NOT signed for the revoke. wk.signedResult = false _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{ AnchorID: "a-rq", WatcherQuorumID: "q", Signer: "w", }) if err == nil { t.Error("RevokeAnchorCredential should be rejected when watcher quorum not signed (REQ-004 6-of-9)") } // Credential stays Pending (not revoked). c, _ := k.GetAnchorCredential(ctx, "a-rq") if c.Status != ptypes.AnchorPending { t.Errorf("status = %q, want Pending (revocation authz failed — credential stays)", c.Status) } } // TestIssueNilWatcherSkipsAuthz asserts a nil WatcherKeeper shim skips // the issuance authz (simtest wiring); the credential is created. func TestIssueNilWatcherSkipsAuthz(t *testing.T) { db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(ptypes.StoreKey) cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil) cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil) cms.LoadLatestVersion() ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger()) // Nil WatcherKeeper shim. k := keeper.NewKeeper(cdc, storeKey, nil, &stubHubKeeper{existsAll: true}) srv := keeper.NewMsgServerImpl(k) if _, err := srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-nw", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }); err != nil { t.Fatalf("IssueAnchorCredential with nil Watcher shim should succeed (authz skipped): %v", err) } c, ok := k.GetAnchorCredential(ctx, "a-nw") if !ok { t.Fatal("anchor credential should be created with nil Watcher shim (authz skipped)") } if c.Status != ptypes.AnchorPending { t.Errorf("status = %q, want Pending", c.Status) } } // TestRevokeNilWatcherSkipsAuthz asserts a nil WatcherKeeper shim skips // the revocation authz (simtest wiring); the credential is revoked. func TestRevokeNilWatcherSkipsAuthz(t *testing.T) { db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(ptypes.StoreKey) cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil) cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil) cms.LoadLatestVersion() ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger()) k := keeper.NewKeeper(cdc, storeKey, nil, &stubHubKeeper{existsAll: true}) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "a-nw2", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) if _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{ AnchorID: "a-nw2", WatcherQuorumID: "q", Signer: "w", }); err != nil { t.Fatalf("RevokeAnchorCredential with nil Watcher shim should succeed (authz skipped): %v", err) } c, _ := k.GetAnchorCredential(ctx, "a-nw2") if c.Status != ptypes.AnchorRevoked { t.Errorf("status = %q, want Revoked (nil shim skips authz)", c.Status) } } // --- Idempotency + NotFound ------------------------------------------------- // TestIssueRejectsDuplicate asserts IssueAnchorCredential on an existing // anchor-id returns an error (idempotency). func TestIssueRejectsDuplicate(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "dup", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) _, err := srv.IssueAnchorCredential(ctx, &ptypes.MsgIssueAnchorCredential{ AnchorID: "dup", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i", }) if err == nil { t.Error("IssueAnchorCredential should reject a duplicate anchor-id") } } // TestOnboardNotFound asserts OnboardAnchor on a missing anchor-id // returns an error. func TestOnboardNotFound(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.OnboardAnchor(ctx, &ptypes.MsgOnboardAnchor{ AnchorID: "missing", CustodyProviderID: "hc", Signer: "i", }) if err == nil { t.Error("OnboardAnchor on missing anchor-id should error") } } // TestSuspendNotFound asserts SuspendAnchorCredential on a missing // anchor-id returns an error. func TestSuspendNotFound(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.SuspendAnchorCredential(ctx, &ptypes.MsgSuspendAnchorCredential{ AnchorID: "missing", Signer: "i", }) if err == nil { t.Error("SuspendAnchorCredential on missing anchor-id should error") } } // TestRevokeNotFound asserts RevokeAnchorCredential on a missing // anchor-id returns an error. func TestRevokeNotFound(t *testing.T) { ctx, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.RevokeAnchorCredential(ctx, &ptypes.MsgRevokeAnchorCredential{ AnchorID: "missing", WatcherQuorumID: "q", Signer: "w", }) if err == nil { t.Error("RevokeAnchorCredential on missing anchor-id should error") } } // --- ValidateBasic (Msg types) ----------------------------------------------- func TestMsgIssueAnchorCredentialValidateBasic(t *testing.T) { cases := []struct { name string msg ptypes.MsgIssueAnchorCredential ok bool }{ {"valid", ptypes.MsgIssueAnchorCredential{AnchorID: "a", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i"}, true}, {"empty anchor-id", ptypes.MsgIssueAnchorCredential{AnchorID: "", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: "i"}, false}, {"empty credential-uri", ptypes.MsgIssueAnchorCredential{AnchorID: "a", CredentialURI: "", WatcherQuorumID: "q", Issuer: "i", Signer: "i"}, false}, {"empty watcher-quorum-id", ptypes.MsgIssueAnchorCredential{AnchorID: "a", CredentialURI: "u", WatcherQuorumID: "", Issuer: "i", Signer: "i"}, false}, {"empty signer", ptypes.MsgIssueAnchorCredential{AnchorID: "a", CredentialURI: "u", WatcherQuorumID: "q", Issuer: "i", Signer: ""}, false}, } for _, c := range cases { err := c.msg.ValidateBasic() if c.ok && err != nil { t.Errorf("%s: expected ok, got %v", c.name, err) } if !c.ok && err == nil { t.Errorf("%s: expected error, got nil", c.name) } } } func TestMsgOnboardAnchorValidateBasic(t *testing.T) { cases := []struct { name string msg ptypes.MsgOnboardAnchor ok bool }{ {"valid", ptypes.MsgOnboardAnchor{AnchorID: "a", CustodyProviderID: "hc", Signer: "i"}, true}, {"empty anchor-id", ptypes.MsgOnboardAnchor{AnchorID: "", CustodyProviderID: "hc", Signer: "i"}, false}, {"empty custody-provider-id", ptypes.MsgOnboardAnchor{AnchorID: "a", CustodyProviderID: "", Signer: "i"}, false}, {"empty signer", ptypes.MsgOnboardAnchor{AnchorID: "a", CustodyProviderID: "hc", Signer: ""}, false}, } for _, c := range cases { err := c.msg.ValidateBasic() if c.ok && err != nil { t.Errorf("%s: expected ok, got %v", c.name, err) } if !c.ok && err == nil { t.Errorf("%s: expected error, got nil", c.name) } } } func TestMsgSuspendAnchorCredentialValidateBasic(t *testing.T) { if err := (&ptypes.MsgSuspendAnchorCredential{AnchorID: "a", Signer: "i"}).ValidateBasic(); err != nil { t.Errorf("valid: %v", err) } if err := (&ptypes.MsgSuspendAnchorCredential{AnchorID: "", Signer: "i"}).ValidateBasic(); err == nil { t.Error("empty anchor-id should fail") } if err := (&ptypes.MsgSuspendAnchorCredential{AnchorID: "a", Signer: ""}).ValidateBasic(); err == nil { t.Error("empty signer should fail") } } func TestMsgRevokeAnchorCredentialValidateBasic(t *testing.T) { cases := []struct { name string msg ptypes.MsgRevokeAnchorCredential ok bool }{ {"valid", ptypes.MsgRevokeAnchorCredential{AnchorID: "a", WatcherQuorumID: "q", Signer: "i"}, true}, {"empty anchor-id", ptypes.MsgRevokeAnchorCredential{AnchorID: "", WatcherQuorumID: "q", Signer: "i"}, false}, {"empty watcher-quorum-id", ptypes.MsgRevokeAnchorCredential{AnchorID: "a", WatcherQuorumID: "", Signer: "i"}, false}, {"empty signer", ptypes.MsgRevokeAnchorCredential{AnchorID: "a", WatcherQuorumID: "q", Signer: ""}, false}, } for _, c := range cases { err := c.msg.ValidateBasic() if c.ok && err != nil { t.Errorf("%s: expected ok, got %v", c.name, err) } if !c.ok && err == nil { t.Errorf("%s: expected error, got nil", c.name) } } } func TestPartnerMsgGetSigners(t *testing.T) { m := &ptypes.MsgIssueAnchorCredential{Signer: "holder-reach"} addrs := m.GetSigners() if len(addrs) != 1 || string(addrs[0]) != "holder-reach" { t.Errorf("GetSigners = %v, want [holder-reach]", addrs) } m2 := &ptypes.MsgOnboardAnchor{Signer: "h2"} if string(m2.GetSigners()[0]) != "h2" { t.Errorf("GetSigners = %v, want [h2]", m2.GetSigners()) } m3 := &ptypes.MsgSuspendAnchorCredential{Signer: "h3"} if string(m3.GetSigners()[0]) != "h3" { t.Errorf("GetSigners = %v, want [h3]", m3.GetSigners()) } m4 := &ptypes.MsgRevokeAnchorCredential{Signer: "h4"} if string(m4.GetSigners()[0]) != "h4" { t.Errorf("GetSigners = %v, want [h4]", m4.GetSigners()) } } // --- Keeper store helpers ---------------------------------------------------- func TestSetGetAnchorCredential(t *testing.T) { ctx, _, _, k := newSimtestContext(t) c := ptypes.AnchorCredential{AnchorID: "a9", Status: ptypes.AnchorPending, CredentialURI: "u"} k.SetAnchorCredential(ctx, c) got, ok := k.GetAnchorCredential(ctx, "a9") if !ok { t.Fatal("GetAnchorCredential: not found") } if got.Status != ptypes.AnchorPending { t.Errorf("status = %q", got.Status) } if _, ok := k.GetAnchorCredential(ctx, "missing"); ok { t.Error("GetAnchorCredential should return false for missing id") } } func TestAllAnchorCredentials(t *testing.T) { ctx, _, _, k := newSimtestContext(t) k.SetAnchorCredential(ctx, ptypes.AnchorCredential{AnchorID: "a1", Status: ptypes.AnchorPending}) k.SetAnchorCredential(ctx, ptypes.AnchorCredential{AnchorID: "a2", Status: ptypes.AnchorOnboarded}) if len(k.AllAnchorCredentials(ctx)) != 2 { t.Errorf("expected 2 anchor credentials, got %d", len(k.AllAnchorCredentials(ctx))) } } // --- Anchor credential status enum helpers ---------------------------------- func TestAllAnchorCredentialStatusesCount(t *testing.T) { if len(ptypes.AllAnchorCredentialStatuses()) != ptypes.AnchorCredentialStatusCount { t.Errorf("AllAnchorCredentialStatuses len = %d, want %d", len(ptypes.AllAnchorCredentialStatuses()), ptypes.AnchorCredentialStatusCount) } if ptypes.AnchorCredentialStatusCount != 4 { t.Errorf("AnchorCredentialStatusCount = %d, want 4", ptypes.AnchorCredentialStatusCount) } } func TestAllAnchorCredentialStatusesNames(t *testing.T) { want := []string{"Pending", "Onboarded", "Suspended", "Revoked"} all := ptypes.AllAnchorCredentialStatuses() if len(all) != len(want) { t.Fatalf("len = %d, want %d", len(all), len(want)) } for i, s := range all { if string(s) != want[i] { t.Errorf("AllAnchorCredentialStatuses()[%d] = %q, want %q", i, s, want[i]) } } } func TestIsTerminalAnchorStatus(t *testing.T) { if ptypes.IsTerminalAnchorStatus(ptypes.AnchorPending) { t.Error("Pending should not be terminal") } if ptypes.IsTerminalAnchorStatus(ptypes.AnchorOnboarded) { t.Error("Onboarded should not be terminal") } if ptypes.IsTerminalAnchorStatus(ptypes.AnchorSuspended) { t.Error("Suspended should not be terminal") } if !ptypes.IsTerminalAnchorStatus(ptypes.AnchorRevoked) { t.Error("Revoked should be terminal") } } func TestValidAnchorTransition(t *testing.T) { // Valid transitions. validCases := []struct { from, to ptypes.AnchorCredentialStatus }{ {ptypes.AnchorPending, ptypes.AnchorOnboarded}, {ptypes.AnchorPending, ptypes.AnchorRevoked}, {ptypes.AnchorOnboarded, ptypes.AnchorSuspended}, {ptypes.AnchorOnboarded, ptypes.AnchorRevoked}, {ptypes.AnchorSuspended, ptypes.AnchorRevoked}, } for _, c := range validCases { if !ptypes.ValidAnchorTransition(c.from, c.to) { t.Errorf("ValidAnchorTransition(%q, %q) = false, want true", c.from, c.to) } } // Invalid transitions. invalidCases := []struct { from, to ptypes.AnchorCredentialStatus }{ {ptypes.AnchorOnboarded, ptypes.AnchorPending}, // no backward to Pending {ptypes.AnchorSuspended, ptypes.AnchorOnboarded}, // no Suspended → Onboarded (v0.5 scope) {ptypes.AnchorSuspended, ptypes.AnchorPending}, // no backward to Pending {ptypes.AnchorRevoked, ptypes.AnchorPending}, // terminal — no out {ptypes.AnchorRevoked, ptypes.AnchorOnboarded}, // terminal — no out {ptypes.AnchorRevoked, ptypes.AnchorSuspended}, // terminal — no out {ptypes.AnchorPending, ptypes.AnchorSuspended}, // must Onboard before Suspend } for _, c := range invalidCases { if ptypes.ValidAnchorTransition(c.from, c.to) { t.Errorf("ValidAnchorTransition(%q, %q) = true, want false", c.from, c.to) } } } // --- G-003 import-invariant (test exemption documentation) ------------------- // TestG003NoWatcherOrHubTypesImport asserts the partner production files // do NOT import x/watcher/types or x/hub/types by struct (G-003 — the // WatcherKeeper and HubKeeper interfaces are the only coupling; no // struct import). This is a tested invariant. The test scans the import // statements of all non-test .go files under x/partner/. (This is a // simtest-grade scan; the full project-wide G-003 invariant is enforced // by the lexicon_meta_test.go / G-003 meta-test in v0.2.) func TestG003NoWatcherOrHubTypesImport(t *testing.T) { // The stub WatcherKeeper and HubKeeper in this simtest file satisfy // the interfaces; the production files (keeper.go, msg_server.go, // module.go, types/*.go) must NOT import x/watcher/types or // x/hub/types. This is verified at the project-wide G-003 meta-test // level. Here we do a lightweight assertion: the stubs use by-string // reach-ids and quorum-ids (not watcher/hub structs), confirming the // interface contract is by-ID-string. wk := &stubWatcherKeeper{signedResult: true} if !wk.IsQuorumSigned("quorum-6of9", []byte("payload")) { t.Error("stub IsQuorumSigned by-ID-string should return true") } if len(wk.calls) != 1 { t.Errorf("expected 1 watcher call recorded, got %d", len(wk.calls)) } hk := &stubHubKeeper{existsAll: true} if !hk.CustodyServiceExists("hub-custody-1") { t.Error("stub CustodyServiceExists by-ID-string should return true") } if len(hk.calls) != 1 { t.Errorf("expected 1 hub call recorded, got %d", len(hk.calls)) } }