package keeper_test // msg_server_simtest_test.go is the x/services keeper simtest (P5-03-01, // REQ-037). // // D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no // real window or vault keepers. The simtest wires the expected-keeper // shims (WindowKeeper, VaultKeeper) to in-test stubs (G-003 test // exemption: the test imports x/services/keeper + defines stub types // that satisfy the interfaces; no production struct imports across // x//types). G-022: the stub WindowKeeper + VaultKeeper are // STUBS returning sentinels, NOT real implementations of x/window or // x/vault (the v0.1 baseline keepers remain empty stubs; v0.5 does not // promote them). // // Coverage (REQ-037): // Lifecycle (kind-agnostic) — Pending -> Active -> Suspended -> Revoked: // - Full success lifecycle: Register (Pending) -> Activate (Active) // -> Suspend (Suspended) -> Revoke (Revoked). // - Pending -> Revoked (skip Activate/Suspend) is a valid transition. // - Suspended -> Revoked is a valid transition. // - Invalid transitions REJECTED: // - Activate on a non-Pending service (Active/Suspended/Revoked // source) -> error. // - Suspend on a non-Active service (Pending/Suspended/Revoked // source) -> error. // - Revoke on an already-Revoked service -> idempotent reject // (no double-effect). // Per-kind typed dispatch (A-551): // - IssueCareGrant on a Care service -> CareService metadata updated. // - IssueCareGrant on a non-Care service -> REJECTED (kind mismatch). // - ActivateSIM on a SIM service -> SIMService metadata updated. // - ActivateSIM on a non-SIM service -> REJECTED. // - ProvisionVault on a Vault service -> VaultService metadata updated // (via the VaultKeeper stub — A-553; G-003 test exemption). // - ProvisionVault on a non-Vault service -> REJECTED. // - ProvisionVault with a nil VaultKeeper shim -> REJECTED (wiring // error). // - ProvisionVault with a VaultKeeper shim returning an error -> // REJECTED. // - BindMailbox on a Mail service -> MailService metadata updated. // - BindMailbox on a non-Mail service -> REJECTED. // Window-grant-on-every-op (A-552): // - RegisterService against a Revoked Window -> REJECTED. // - RegisterService against an Expired Window -> REJECTED. // - RegisterService against an unknown Window -> REJECTED. // - ActivateService on a service whose window-id went Revoked AFTER // registration -> REJECTED (window-grant-on-every-op — the check // is NOT just at registration). // - IssueCareGrant on a Care service whose window-id went Revoked // AFTER registration + activation -> REJECTED. // - ProvisionVault on a Vault service whose window-id went Revoked // AFTER registration -> REJECTED. // - Nil WindowKeeper shim -> skips the A-552 check (simtest wiring). // Idempotency + NotFound: // - RegisterService on an existing service-id -> REJECTED. // - ActivateService / SuspendService / RevokeService / per-kind // handlers on a missing service-id -> REJECTED. // ValidateBasic: each Msg* ValidateBasic error path. // // Coverage target: >=80% on x/services/keeper. import ( "fmt" "strings" "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/services/keeper" stypes "github.com/oy/openyield/x/services/types" ) // --- Stub expected-keepers (G-003 test exemption, G-022 stubs) ------------- // stubWindowKeeper satisfies stypes.WindowKeeper for the simtest. It // records GetWindowStatus calls and returns the configured status per // window-id (default: Active). G-022: this is a STUB returning a sentinel // status, NOT a real x/window keeper implementation. type stubWindowKeeper struct { calls []string // recorded window-ids status map[stypes.WindowStatus]bool // status-set to return for all (single-value) defaultSet bool singleStatus stypes.WindowStatus perWindow map[string]stypes.WindowStatus // window-id -> status err error } func (s *stubWindowKeeper) GetWindowStatus(windowID string) (stypes.WindowStatus, error) { s.calls = append(s.calls, windowID) if s.err != nil { return stypes.WindowStatusUnknown, s.err } if s.perWindow != nil { if st, ok := s.perWindow[windowID]; ok { return st, nil } } if s.defaultSet { return s.singleStatus, nil } return stypes.WindowStatusActive, nil } // setWindowStatus configures the stub to return the given status for the // named window-id (overrides the default Active). func (s *stubWindowKeeper) setWindowStatus(windowID string, status stypes.WindowStatus) { if s.perWindow == nil { s.perWindow = map[string]stypes.WindowStatus{} } s.perWindow[windowID] = status } // setDefaultStatus configures the stub to return the given status for // any window-id (default fallback when no per-window override). func (s *stubWindowKeeper) setDefaultStatus(status stypes.WindowStatus) { s.defaultSet = true s.singleStatus = status } // stubVaultKeeper satisfies stypes.VaultKeeper for the simtest. It // records ProvisionVault calls and returns the configured error (default // nil = success). G-022: this is a STUB, NOT a real x/vault keeper // implementation. type stubVaultKeeper struct { calls []vaultCall err error provisioned map[string]int64 // service-id -> storage-quota-grain } type vaultCall struct { serviceID string quotaGrain int64 } func (s *stubVaultKeeper) ProvisionVault(serviceID string, quotaGrain int64) error { s.calls = append(s.calls, vaultCall{serviceID, quotaGrain}) if s.err != nil { return s.err } if s.provisioned == nil { s.provisioned = map[string]int64{} } s.provisioned[serviceID] = quotaGrain return nil } // --- Simtest context helper -------------------------------------------------- // newSimtestContext constructs an in-memory sdk.Context with a KVStore // mounted at the services store key. D-054: in-memory, no real window or // vault keepers. Returns the ctx, the stub WindowKeeper, the stub // VaultKeeper, the store key, and the Keeper. func newSimtestContext(t *testing.T) (sdk.Context, *stubWindowKeeper, *stubVaultKeeper, storetypes.StoreKey, keeper.Keeper) { t.Helper() db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(stypes.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) } ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger()) wk := &stubWindowKeeper{} vk := &stubVaultKeeper{} k := keeper.NewKeeper(cdc, storeKey, wk, vk) return ctx, wk, vk, storeKey, 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 -> Active -> Suspended -> Revoked ------- // TestServiceLifecycleFullSuccess asserts the full success lifecycle for a // Care service: Register (Pending) -> Activate (Active) -> Suspend // (Suspended) -> Revoke (Revoked). func TestServiceLifecycleFullSuccess(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) // Register -> Pending. if _, err := srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-care-1", Kind: stypes.KindCare, OperatorReachID: "reach-op-1", Name: "Care Service 1", WindowID: "window-1", Signer: "reach-op-1", }); err != nil { t.Fatalf("RegisterService: %v", err) } info, ok := k.GetService(ctx, "svc-care-1") if !ok { t.Fatal("service not found after register") } if info.Status != stypes.ServicePending { t.Errorf("status = %q, want Pending", info.Status) } if info.Kind != stypes.KindCare { t.Errorf("kind = %q, want Care", info.Kind) } if info.WindowID != "window-1" { t.Errorf("window-id = %q, want window-1", info.WindowID) } if !hasEvent(ctx, "services.service_registered") { t.Error("service_registered event not emitted") } // Activate -> Active. if _, err := srv.ActivateService(ctx, &stypes.MsgActivateService{ ServiceID: "svc-care-1", Signer: "reach-op-1", }); err != nil { t.Fatalf("ActivateService: %v", err) } info, _ = k.GetService(ctx, "svc-care-1") if info.Status != stypes.ServiceActive { t.Errorf("status = %q, want Active", info.Status) } if !hasEvent(ctx, "services.service_activated") { t.Error("service_activated event not emitted") } // Suspend -> Suspended. if _, err := srv.SuspendService(ctx, &stypes.MsgSuspendService{ ServiceID: "svc-care-1", Signer: "reach-op-1", }); err != nil { t.Fatalf("SuspendService: %v", err) } info, _ = k.GetService(ctx, "svc-care-1") if info.Status != stypes.ServiceSuspended { t.Errorf("status = %q, want Suspended", info.Status) } if !hasEvent(ctx, "services.service_suspended") { t.Error("service_suspended event not emitted") } // Revoke -> Revoked (terminal). if _, err := srv.RevokeService(ctx, &stypes.MsgRevokeService{ ServiceID: "svc-care-1", Signer: "reach-op-1", }); err != nil { t.Fatalf("RevokeService: %v", err) } info, _ = k.GetService(ctx, "svc-care-1") if info.Status != stypes.ServiceRevoked { t.Errorf("status = %q, want Revoked", info.Status) } if !hasEvent(ctx, "services.service_revoked") { t.Error("service_revoked event not emitted") } } // --- Pending -> Revoked (skip Activate/Suspend) ----------------------------- // TestServiceRevokeFromPending asserts a Pending service can be revoked // directly (Pending -> Revoked is a valid transition). func TestServiceRevokeFromPending(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-pend", Kind: stypes.KindSIM, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, err := srv.RevokeService(ctx, &stypes.MsgRevokeService{ ServiceID: "svc-pend", Signer: "r", }); err != nil { t.Fatalf("RevokeService from Pending: %v", err) } info, _ := k.GetService(ctx, "svc-pend") if info.Status != stypes.ServiceRevoked { t.Errorf("status = %q, want Revoked", info.Status) } } // --- Suspended -> Revoked ---------------------------------------------------- // TestServiceRevokeFromSuspended asserts a Suspended service can be // revoked (Suspended -> Revoked is a valid transition). func TestServiceRevokeFromSuspended(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-sus", Kind: stypes.KindMail, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "svc-sus", Signer: "r"}) srv.SuspendService(ctx, &stypes.MsgSuspendService{ServiceID: "svc-sus", Signer: "r"}) if _, err := srv.RevokeService(ctx, &stypes.MsgRevokeService{ ServiceID: "svc-sus", Signer: "r", }); err != nil { t.Fatalf("RevokeService from Suspended: %v", err) } info, _ := k.GetService(ctx, "svc-sus") if info.Status != stypes.ServiceRevoked { t.Errorf("status = %q, want Revoked", info.Status) } } // --- Invalid transitions REJECTED ------------------------------------------- // TestActivateRejectsNonPending asserts ActivateService on a non-Pending // service is REJECTED (lifecycle gate). Covers Active, Suspended, and // Revoked source statuses. func TestActivateRejectsNonPending(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) // Active source -> reject (register + activate first). srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "a-act", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "a-act", Signer: "r"}) _, err := srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "a-act", Signer: "r"}) if err == nil { t.Error("ActivateService on Active service should be rejected (lifecycle gate)") } // Suspended source -> reject. srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "a-sus", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "a-sus", Signer: "r"}) srv.SuspendService(ctx, &stypes.MsgSuspendService{ServiceID: "a-sus", Signer: "r"}) _, err = srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "a-sus", Signer: "r"}) if err == nil { t.Error("ActivateService on Suspended service should be rejected (lifecycle gate)") } // Revoked source -> reject. srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "a-rev", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.RevokeService(ctx, &stypes.MsgRevokeService{ServiceID: "a-rev", Signer: "r"}) _, err = srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "a-rev", Signer: "r"}) if err == nil { t.Error("ActivateService on Revoked service should be rejected (lifecycle gate)") } } // TestSuspendRejectsNonActive asserts SuspendService on a non-Active // service is REJECTED. Covers Pending, Suspended, and Revoked source // statuses. func TestSuspendRejectsNonActive(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) // Pending source -> reject. srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "s-pend", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) _, err := srv.SuspendService(ctx, &stypes.MsgSuspendService{ServiceID: "s-pend", Signer: "r"}) if err == nil { t.Error("SuspendService on Pending service should be rejected (lifecycle gate)") } // Suspended source -> reject (suspend an already-suspended). srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "s-sus2", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "s-sus2", Signer: "r"}) srv.SuspendService(ctx, &stypes.MsgSuspendService{ServiceID: "s-sus2", Signer: "r"}) _, err = srv.SuspendService(ctx, &stypes.MsgSuspendService{ServiceID: "s-sus2", Signer: "r"}) if err == nil { t.Error("SuspendService on Suspended service should be rejected (lifecycle gate)") } // Revoked source -> reject. srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "s-rev2", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.RevokeService(ctx, &stypes.MsgRevokeService{ServiceID: "s-rev2", Signer: "r"}) _, err = srv.SuspendService(ctx, &stypes.MsgSuspendService{ServiceID: "s-rev2", Signer: "r"}) if err == nil { t.Error("SuspendService on Revoked service should be rejected (lifecycle gate)") } } // TestRevokeRejectsAlreadyRevoked asserts a second Revoke on a Revoked // service is REJECTED (idempotent reject — no double-effect). func TestRevokeRejectsAlreadyRevoked(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "r-rev3", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.RevokeService(ctx, &stypes.MsgRevokeService{ServiceID: "r-rev3", Signer: "r"}) _, err := srv.RevokeService(ctx, &stypes.MsgRevokeService{ServiceID: "r-rev3", Signer: "r"}) if err == nil { t.Error("RevokeService on Revoked service should be rejected (idempotent reject — no double-effect)") } } // --- Per-kind typed dispatch (A-551) ----------------------------------------- // TestIssueCareGrantOnCareService asserts IssueCareGrant on a Care // service SUCCEEDS and updates the CareService metadata. func TestIssueCareGrantOnCareService(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-care", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, err := srv.IssueCareGrant(ctx, &stypes.MsgIssueCareGrant{ ServiceID: "svc-care", CareKind: "mutual-aid", GrantRecipientReachID: "reach-recipient", Signer: "r", }); err != nil { t.Fatalf("IssueCareGrant on Care service: %v", err) } care, ok := k.GetCareService(ctx, "svc-care") if !ok { t.Fatal("CareService metadata not found") } if care.CareKind != "mutual-aid" { t.Errorf("care-kind = %q, want mutual-aid", care.CareKind) } if !hasEvent(ctx, "services.care_grant_issued") { t.Error("care_grant_issued event not emitted") } } // TestIssueCareGrantRejectsNonCare asserts IssueCareGrant on a non-Care // service is REJECTED (A-551 typed dispatch — kind mismatch). func TestIssueCareGrantRejectsNonCare(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-sim", Kind: stypes.KindSIM, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) _, err := srv.IssueCareGrant(ctx, &stypes.MsgIssueCareGrant{ ServiceID: "svc-sim", CareKind: "mutual-aid", GrantRecipientReachID: "x", Signer: "r", }) if err == nil { t.Error("IssueCareGrant on a SIM service should be rejected (A-551 kind mismatch)") } if !strings.Contains(err.Error(), "not Care") { t.Errorf("error = %q, want 'not Care'", err.Error()) } } // TestActivateSIMOnSIMService asserts ActivateSIM on a SIM service // SUCCEEDS and updates the SIMService metadata. func TestActivateSIMOnSIMService(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-sim2", Kind: stypes.KindSIM, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, err := srv.ActivateSIM(ctx, &stypes.MsgActivateSIM{ ServiceID: "svc-sim2", Carrier: "oy-mobile", RecipientReachID: "reach-recipient", Signer: "r", }); err != nil { t.Fatalf("ActivateSIM on SIM service: %v", err) } sim, ok := k.GetSIMService(ctx, "svc-sim2") if !ok { t.Fatal("SIMService metadata not found") } if sim.Carrier != "oy-mobile" { t.Errorf("carrier = %q, want oy-mobile", sim.Carrier) } if !hasEvent(ctx, "services.sim_activated") { t.Error("sim_activated event not emitted") } } // TestActivateSIMRejectsNonSIM asserts ActivateSIM on a non-SIM service is // REJECTED. func TestActivateSIMRejectsNonSIM(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-care2", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) _, err := srv.ActivateSIM(ctx, &stypes.MsgActivateSIM{ ServiceID: "svc-care2", Carrier: "c", RecipientReachID: "x", Signer: "r", }) if err == nil { t.Error("ActivateSIM on a Care service should be rejected (A-551 kind mismatch)") } if !strings.Contains(err.Error(), "not SIM") { t.Errorf("error = %q, want 'not SIM'", err.Error()) } } // TestProvisionVaultOnVaultService asserts ProvisionVault on a Vault // service SUCCEEDS, delegates to the VaultKeeper stub (A-553), and // updates the VaultService metadata. func TestProvisionVaultOnVaultService(t *testing.T) { ctx, _, vk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-vault", Kind: stypes.KindVault, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, err := srv.ProvisionVault(ctx, &stypes.MsgProvisionVault{ ServiceID: "svc-vault", StorageQuotaGrain: 1_000_000, Signer: "r", }); err != nil { t.Fatalf("ProvisionVault on Vault service: %v", err) } // The VaultKeeper stub was called with the service-id + quota-grain. if len(vk.calls) != 1 { t.Fatalf("vault keeper calls = %d, want 1", len(vk.calls)) } if vk.calls[0].serviceID != "svc-vault" { t.Errorf("vault call service-id = %q, want svc-vault", vk.calls[0].serviceID) } if vk.calls[0].quotaGrain != 1_000_000 { t.Errorf("vault call quota = %d, want 1000000", vk.calls[0].quotaGrain) } // The VaultService metadata is updated. vault, ok := k.GetVaultService(ctx, "svc-vault") if !ok { t.Fatal("VaultService metadata not found") } if vault.StorageQuotaGrain != 1_000_000 { t.Errorf("storage-quota-grain = %d, want 1000000", vault.StorageQuotaGrain) } if !hasEvent(ctx, "services.vault_provisioned") { t.Error("vault_provisioned event not emitted") } } // TestProvisionVaultRejectsNonVault asserts ProvisionVault on a non-Vault // service is REJECTED. func TestProvisionVaultRejectsNonVault(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-mail", Kind: stypes.KindMail, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) _, err := srv.ProvisionVault(ctx, &stypes.MsgProvisionVault{ ServiceID: "svc-mail", StorageQuotaGrain: 1000, Signer: "r", }) if err == nil { t.Error("ProvisionVault on a Mail service should be rejected (A-551 kind mismatch)") } if !strings.Contains(err.Error(), "not Vault") { t.Errorf("error = %q, want 'not Vault'", err.Error()) } } // TestProvisionVaultRejectsNilVaultKeeper asserts ProvisionVault with a // nil VaultKeeper shim is REJECTED (wiring error — A-553). func TestProvisionVaultRejectsNilVaultKeeper(t *testing.T) { ctx, wk, _, sk, _ := newSimtestContext(t) // Construct a keeper with a nil VaultKeeper, reusing the mounted store key. k := keeper.NewKeeper(newTestCodec(), sk, wk, nil) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-vault-nil", Kind: stypes.KindVault, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) _, err := srv.ProvisionVault(ctx, &stypes.MsgProvisionVault{ ServiceID: "svc-vault-nil", StorageQuotaGrain: 1000, Signer: "r", }) if err == nil { t.Error("ProvisionVault with nil VaultKeeper shim should be rejected (wiring error)") } if !strings.Contains(err.Error(), "vault keeper not wired") { t.Errorf("error = %q, want 'vault keeper not wired'", err.Error()) } } // TestProvisionVaultRejectsVaultKeeperError asserts ProvisionVault with a // VaultKeeper shim returning an error is REJECTED (the VaultService // metadata is NOT updated). func TestProvisionVaultRejectsVaultKeeperError(t *testing.T) { ctx, _, vk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-vault-err", Kind: stypes.KindVault, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) // Configure the VaultKeeper stub to return an error. vk.err = fmt.Errorf("vault quota exceeds capacity") _, err := srv.ProvisionVault(ctx, &stypes.MsgProvisionVault{ ServiceID: "svc-vault-err", StorageQuotaGrain: 1_000_000_000, Signer: "r", }) if err == nil { t.Error("ProvisionVault should be rejected when VaultKeeper returns an error") } if !strings.Contains(err.Error(), "vault quota exceeds capacity") { t.Errorf("error = %q, want 'vault quota exceeds capacity'", err.Error()) } // The VaultService metadata is NOT updated (storage-quota-grain stays 0). vault, _ := k.GetVaultService(ctx, "svc-vault-err") if vault.StorageQuotaGrain != 0 { t.Errorf("storage-quota-grain = %d, want 0 (provisioning rejected)", vault.StorageQuotaGrain) } } // TestBindMailboxOnMailService asserts BindMailbox on a Mail service // SUCCEEDS and updates the MailService metadata. func TestBindMailboxOnMailService(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-mail2", Kind: stypes.KindMail, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, err := srv.BindMailbox(ctx, &stypes.MsgBindMailbox{ ServiceID: "svc-mail2", MailboxID: "mbox-1", HolderReachID: "reach-holder", Signer: "r", }); err != nil { t.Fatalf("BindMailbox on Mail service: %v", err) } mail, ok := k.GetMailService(ctx, "svc-mail2") if !ok { t.Fatal("MailService metadata not found") } if mail.MailboxID != "mbox-1" { t.Errorf("mailbox-id = %q, want mbox-1", mail.MailboxID) } if mail.HolderReachID != "reach-holder" { t.Errorf("holder-reach-id = %q, want reach-holder", mail.HolderReachID) } if !hasEvent(ctx, "services.mailbox_bound") { t.Error("mailbox_bound event not emitted") } } // TestBindMailboxRejectsNonMail asserts BindMailbox on a non-Mail service // is REJECTED. func TestBindMailboxRejectsNonMail(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-vault2", Kind: stypes.KindVault, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) _, err := srv.BindMailbox(ctx, &stypes.MsgBindMailbox{ ServiceID: "svc-vault2", MailboxID: "m", HolderReachID: "h", Signer: "r", }) if err == nil { t.Error("BindMailbox on a Vault service should be rejected (A-551 kind mismatch)") } if !strings.Contains(err.Error(), "not Mail") { t.Errorf("error = %q, want 'not Mail'", err.Error()) } } // --- Window-grant-on-every-op (A-552) --------------------------------------- // TestRegisterServiceRejectsRevokedWindow asserts RegisterService against // a Revoked Window is REJECTED (the service is NOT created). func TestRegisterServiceRejectsRevokedWindow(t *testing.T) { ctx, wk, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) wk.setWindowStatus("window-revoked", stypes.WindowStatusRevoked) _, err := srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-x", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "window-revoked", Signer: "r", }) if err == nil { t.Error("RegisterService against a Revoked Window should be rejected (A-552)") } if !strings.Contains(err.Error(), "window") { t.Errorf("error = %q, want 'window'", err.Error()) } // The service is NOT created. if _, ok := k.GetService(ctx, "svc-x"); ok { t.Error("service should NOT be created when Window is Revoked") } } // TestRegisterServiceRejectsExpiredWindow asserts RegisterService against // an Expired Window is REJECTED (A-552). func TestRegisterServiceRejectsExpiredWindow(t *testing.T) { ctx, wk, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) wk.setWindowStatus("window-exp", stypes.WindowStatusExpired) _, err := srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-y", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "window-exp", Signer: "r", }) if err == nil { t.Error("RegisterService against an Expired Window should be rejected (A-552)") } } // TestRegisterServiceRejectsUnknownWindow asserts RegisterService against // an unknown Window (status=Unknown) is REJECTED (A-552). func TestRegisterServiceRejectsUnknownWindow(t *testing.T) { ctx, wk, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) wk.setWindowStatus("window-unk", stypes.WindowStatusUnknown) _, err := srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-z", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "window-unk", Signer: "r", }) if err == nil { t.Error("RegisterService against an unknown Window should be rejected (A-552)") } } // TestActivateServiceRejectsWhenWindowRevokedAfterRegistration asserts // A-552 window-grant-on-every-op: a service whose window-id went Revoked // AFTER registration is REJECTED at ActivateService (the check is NOT // just at registration). func TestActivateServiceRejectsWhenWindowRevokedAfterRegistration(t *testing.T) { ctx, wk, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) // Register against an Active Window -> Pending. if _, err := srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-window-flip", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "window-flip", Signer: "r", }); err != nil { t.Fatalf("RegisterService against Active Window: %v", err) } // The window-id goes Revoked AFTER registration (the Window was // Active at RegisterService time; the next op must re-check A-552). wk.setWindowStatus("window-flip", stypes.WindowStatusRevoked) _, err := srv.ActivateService(ctx, &stypes.MsgActivateService{ ServiceID: "svc-window-flip", Signer: "r", }) if err == nil { t.Error("ActivateService should be rejected when Window went Revoked after registration (A-552 window-grant-on-every-op)") } if !strings.Contains(err.Error(), "window") { t.Errorf("error = %q, want 'window'", err.Error()) } // The service stays Pending (the rejected activation did not mutate). info, _ := k.GetService(ctx, "svc-window-flip") if info.Status != stypes.ServicePending { t.Errorf("status = %q, want Pending (rejected activation did not mutate)", info.Status) } } // TestIssueCareGrantRejectsWhenWindowRevokedAfterRegistration asserts // A-552 window-grant-on-every-op on a per-kind op: IssueCareGrant on a // Care service whose window-id went Revoked AFTER registration + // activation is REJECTED. func TestIssueCareGrantRejectsWhenWindowRevokedAfterRegistration(t *testing.T) { ctx, wk, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-care-flip", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "window-care-flip", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "svc-care-flip", Signer: "r"}) // Window goes Revoked AFTER registration + activation. wk.setWindowStatus("window-care-flip", stypes.WindowStatusRevoked) _, err := srv.IssueCareGrant(ctx, &stypes.MsgIssueCareGrant{ ServiceID: "svc-care-flip", CareKind: "mutual-aid", GrantRecipientReachID: "x", Signer: "r", }) if err == nil { t.Error("IssueCareGrant should be rejected when Window went Revoked (A-552 window-grant-on-every-op)") } // The CareService metadata is NOT updated. care, _ := k.GetCareService(ctx, "svc-care-flip") if care.CareKind != "" { t.Errorf("care-kind = %q, want empty (per-kind op rejected — A-552)", care.CareKind) } } // TestProvisionVaultRejectsWhenWindowRevokedAfterRegistration asserts // A-552 window-grant-on-every-op on ProvisionVault: the window-grant // check runs BEFORE the VaultKeeper shim delegation (the provisioning is // NOT delegated when the Window is Revoked). func TestProvisionVaultRejectsWhenWindowRevokedAfterRegistration(t *testing.T) { ctx, wk, vk, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-vault-flip", Kind: stypes.KindVault, OperatorReachID: "r", Name: "n", WindowID: "window-vault-flip", Signer: "r", }) // Window goes Revoked AFTER registration. wk.setWindowStatus("window-vault-flip", stypes.WindowStatusRevoked) _, err := srv.ProvisionVault(ctx, &stypes.MsgProvisionVault{ ServiceID: "svc-vault-flip", StorageQuotaGrain: 1000, Signer: "r", }) if err == nil { t.Error("ProvisionVault should be rejected when Window went Revoked (A-552 window-grant-on-every-op)") } // The VaultKeeper shim was NOT called (the A-552 check ran before the // A-553 delegation). if len(vk.calls) != 0 { t.Errorf("vault keeper calls = %d, want 0 (A-552 check ran BEFORE A-553 delegation)", len(vk.calls)) } } // TestNilWindowKeeperSkipsA552Check asserts a nil WindowKeeper shim skips // the A-552 window-grant Active check (simtest wiring); the handler // mutates state regardless. This documents the wiring contract for the // A-552 shim: a real x/window keeper is wired in the live chain; the // simtest may use a nil shim. func TestNilWindowKeeperSkipsA552Check(t *testing.T) { db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(stypes.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 WindowKeeper shim (VaultKeeper stub provided so ProvisionVault // would not fail on a nil VaultKeeper — but this test only registers // + activates). k := keeper.NewKeeper(cdc, storeKey, nil, &stubVaultKeeper{}) srv := keeper.NewMsgServerImpl(k) if _, err := srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-nil-wk", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "any-window", Signer: "r", }); err != nil { t.Fatalf("RegisterService with nil WindowKeeper should succeed (A-552 check skipped): %v", err) } info, ok := k.GetService(ctx, "svc-nil-wk") if !ok { t.Fatal("service should be registered (nil shim skips A-552)") } if info.Status != stypes.ServicePending { t.Errorf("status = %q, want Pending", info.Status) } } // --- Idempotency + NotFound ------------------------------------------------- // TestRegisterServiceRejectsDuplicate asserts RegisterService on an // existing service-id returns an error (idempotency). func TestRegisterServiceRejectsDuplicate(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "dup", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) _, err := srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "dup", Kind: stypes.KindSIM, OperatorReachID: "r2", Name: "n2", WindowID: "w2", Signer: "r2", }) if err == nil { t.Error("RegisterService should reject a duplicate service-id") } } // TestActivateNotFound asserts ActivateService on a missing service-id // returns an error. func TestActivateNotFound(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "missing", Signer: "r"}) if err == nil { t.Error("ActivateService on missing service-id should error") } } // TestSuspendNotFound asserts SuspendService on a missing service-id // returns an error. func TestSuspendNotFound(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.SuspendService(ctx, &stypes.MsgSuspendService{ServiceID: "missing", Signer: "r"}) if err == nil { t.Error("SuspendService on missing service-id should error") } } // TestRevokeNotFound asserts RevokeService on a missing service-id // returns an error. func TestRevokeNotFound(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.RevokeService(ctx, &stypes.MsgRevokeService{ServiceID: "missing", Signer: "r"}) if err == nil { t.Error("RevokeService on missing service-id should error") } } // TestPerKindHandlersNotFound asserts each per-kind handler on a missing // service-id returns an error. func TestPerKindHandlersNotFound(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) if _, err := srv.IssueCareGrant(ctx, &stypes.MsgIssueCareGrant{ ServiceID: "missing", CareKind: "k", GrantRecipientReachID: "g", Signer: "r", }); err == nil { t.Error("IssueCareGrant on missing service-id should error") } if _, err := srv.ActivateSIM(ctx, &stypes.MsgActivateSIM{ ServiceID: "missing", Carrier: "c", RecipientReachID: "r", Signer: "r", }); err == nil { t.Error("ActivateSIM on missing service-id should error") } if _, err := srv.ProvisionVault(ctx, &stypes.MsgProvisionVault{ ServiceID: "missing", StorageQuotaGrain: 1000, Signer: "r", }); err == nil { t.Error("ProvisionVault on missing service-id should error") } if _, err := srv.BindMailbox(ctx, &stypes.MsgBindMailbox{ ServiceID: "missing", MailboxID: "m", HolderReachID: "h", Signer: "r", }); err == nil { t.Error("BindMailbox on missing service-id should error") } } // --- Per-kind round-trip (one per ServiceKind — A-551 typed dispatch) ----- // TestPerKindRoundTripCare asserts a full Care round-trip: register // (Care) -> activate -> IssueCareGrant. func TestPerKindRoundTripCare(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "rt-care", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "rt-care", Signer: "r"}) if _, err := srv.IssueCareGrant(ctx, &stypes.MsgIssueCareGrant{ ServiceID: "rt-care", CareKind: "mutual-aid", GrantRecipientReachID: "recipient", Signer: "r", }); err != nil { t.Fatalf("IssueCareGrant round-trip: %v", err) } care, _ := k.GetCareService(ctx, "rt-care") if care.CareKind != "mutual-aid" { t.Errorf("care-kind = %q", care.CareKind) } } // TestPerKindRoundTripSIM asserts a full SIM round-trip. func TestPerKindRoundTripSIM(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "rt-sim", Kind: stypes.KindSIM, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "rt-sim", Signer: "r"}) if _, err := srv.ActivateSIM(ctx, &stypes.MsgActivateSIM{ ServiceID: "rt-sim", Carrier: "carrier-x", RecipientReachID: "recipient", Signer: "r", }); err != nil { t.Fatalf("ActivateSIM round-trip: %v", err) } sim, _ := k.GetSIMService(ctx, "rt-sim") if sim.Carrier != "carrier-x" { t.Errorf("carrier = %q", sim.Carrier) } } // TestPerKindRoundTripVault asserts a full Vault round-trip (incl. the // A-553 VaultKeeper stub delegation). func TestPerKindRoundTripVault(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "rt-vault", Kind: stypes.KindVault, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "rt-vault", Signer: "r"}) if _, err := srv.ProvisionVault(ctx, &stypes.MsgProvisionVault{ ServiceID: "rt-vault", StorageQuotaGrain: 5_000_000, Signer: "r", }); err != nil { t.Fatalf("ProvisionVault round-trip: %v", err) } vault, _ := k.GetVaultService(ctx, "rt-vault") if vault.StorageQuotaGrain != 5_000_000 { t.Errorf("storage-quota-grain = %d", vault.StorageQuotaGrain) } } // TestPerKindRoundTripMail asserts a full Mail round-trip. func TestPerKindRoundTripMail(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "rt-mail", Kind: stypes.KindMail, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) srv.ActivateService(ctx, &stypes.MsgActivateService{ServiceID: "rt-mail", Signer: "r"}) if _, err := srv.BindMailbox(ctx, &stypes.MsgBindMailbox{ ServiceID: "rt-mail", MailboxID: "mbox-rt", HolderReachID: "holder-rt", Signer: "r", }); err != nil { t.Fatalf("BindMailbox round-trip: %v", err) } mail, _ := k.GetMailService(ctx, "rt-mail") if mail.MailboxID != "mbox-rt" { t.Errorf("mailbox-id = %q", mail.MailboxID) } if mail.HolderReachID != "holder-rt" { t.Errorf("holder-reach-id = %q", mail.HolderReachID) } } // --- ValidateBasic (Msg types) ----------------------------------------------- func TestMsgRegisterServiceValidateBasic(t *testing.T) { cases := []struct { name string msg stypes.MsgRegisterService ok bool }{ {"valid", stypes.MsgRegisterService{ServiceID: "s", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r"}, true}, {"empty service-id", stypes.MsgRegisterService{ServiceID: "", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r"}, false}, {"unknown kind", stypes.MsgRegisterService{ServiceID: "s", Kind: stypes.ServiceKind("Bogus"), OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r"}, false}, {"empty operator-reach-id", stypes.MsgRegisterService{ServiceID: "s", Kind: stypes.KindCare, OperatorReachID: "", Name: "n", WindowID: "w", Signer: "r"}, false}, {"empty name", stypes.MsgRegisterService{ServiceID: "s", Kind: stypes.KindCare, OperatorReachID: "r", Name: "", WindowID: "w", Signer: "r"}, false}, {"empty window-id", stypes.MsgRegisterService{ServiceID: "s", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "", Signer: "r"}, false}, {"empty signer", stypes.MsgRegisterService{ServiceID: "s", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", 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 TestMsgActivateServiceValidateBasic(t *testing.T) { if err := (&stypes.MsgActivateService{ServiceID: "s", Signer: "r"}).ValidateBasic(); err != nil { t.Errorf("valid: %v", err) } if err := (&stypes.MsgActivateService{ServiceID: "", Signer: "r"}).ValidateBasic(); err == nil { t.Error("empty service-id should fail") } if err := (&stypes.MsgActivateService{ServiceID: "s", Signer: ""}).ValidateBasic(); err == nil { t.Error("empty signer should fail") } } func TestMsgSuspendServiceValidateBasic(t *testing.T) { if err := (&stypes.MsgSuspendService{ServiceID: "s", Signer: "r"}).ValidateBasic(); err != nil { t.Errorf("valid: %v", err) } if err := (&stypes.MsgSuspendService{ServiceID: "", Signer: "r"}).ValidateBasic(); err == nil { t.Error("empty service-id should fail") } if err := (&stypes.MsgSuspendService{ServiceID: "s", Signer: ""}).ValidateBasic(); err == nil { t.Error("empty signer should fail") } } func TestMsgRevokeServiceValidateBasic(t *testing.T) { if err := (&stypes.MsgRevokeService{ServiceID: "s", Signer: "r"}).ValidateBasic(); err != nil { t.Errorf("valid: %v", err) } if err := (&stypes.MsgRevokeService{ServiceID: "", Signer: "r"}).ValidateBasic(); err == nil { t.Error("empty service-id should fail") } if err := (&stypes.MsgRevokeService{ServiceID: "s", Signer: ""}).ValidateBasic(); err == nil { t.Error("empty signer should fail") } } func TestMsgIssueCareGrantValidateBasic(t *testing.T) { cases := []struct { name string msg stypes.MsgIssueCareGrant ok bool }{ {"valid", stypes.MsgIssueCareGrant{ServiceID: "s", CareKind: "k", GrantRecipientReachID: "g", Signer: "r"}, true}, {"empty service-id", stypes.MsgIssueCareGrant{ServiceID: "", CareKind: "k", GrantRecipientReachID: "g", Signer: "r"}, false}, {"empty care-kind", stypes.MsgIssueCareGrant{ServiceID: "s", CareKind: "", GrantRecipientReachID: "g", Signer: "r"}, false}, {"empty grant-recipient", stypes.MsgIssueCareGrant{ServiceID: "s", CareKind: "k", GrantRecipientReachID: "", Signer: "r"}, false}, {"empty signer", stypes.MsgIssueCareGrant{ServiceID: "s", CareKind: "k", GrantRecipientReachID: "g", 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 TestMsgActivateSIMValidateBasic(t *testing.T) { cases := []struct { name string msg stypes.MsgActivateSIM ok bool }{ {"valid", stypes.MsgActivateSIM{ServiceID: "s", Carrier: "c", RecipientReachID: "r", Signer: "r"}, true}, {"empty service-id", stypes.MsgActivateSIM{ServiceID: "", Carrier: "c", RecipientReachID: "r", Signer: "r"}, false}, {"empty carrier", stypes.MsgActivateSIM{ServiceID: "s", Carrier: "", RecipientReachID: "r", Signer: "r"}, false}, {"empty recipient", stypes.MsgActivateSIM{ServiceID: "s", Carrier: "c", RecipientReachID: "", Signer: "r"}, false}, {"empty signer", stypes.MsgActivateSIM{ServiceID: "s", Carrier: "c", RecipientReachID: "r", 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 TestMsgProvisionVaultValidateBasic(t *testing.T) { cases := []struct { name string msg stypes.MsgProvisionVault ok bool }{ {"valid", stypes.MsgProvisionVault{ServiceID: "s", StorageQuotaGrain: 1000, Signer: "r"}, true}, {"empty service-id", stypes.MsgProvisionVault{ServiceID: "", StorageQuotaGrain: 1000, Signer: "r"}, false}, {"zero quota", stypes.MsgProvisionVault{ServiceID: "s", StorageQuotaGrain: 0, Signer: "r"}, false}, {"negative quota", stypes.MsgProvisionVault{ServiceID: "s", StorageQuotaGrain: -1, Signer: "r"}, false}, {"empty signer", stypes.MsgProvisionVault{ServiceID: "s", StorageQuotaGrain: 1000, 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 TestMsgBindMailboxValidateBasic(t *testing.T) { cases := []struct { name string msg stypes.MsgBindMailbox ok bool }{ {"valid", stypes.MsgBindMailbox{ServiceID: "s", MailboxID: "m", HolderReachID: "h", Signer: "r"}, true}, {"empty service-id", stypes.MsgBindMailbox{ServiceID: "", MailboxID: "m", HolderReachID: "h", Signer: "r"}, false}, {"empty mailbox-id", stypes.MsgBindMailbox{ServiceID: "s", MailboxID: "", HolderReachID: "h", Signer: "r"}, false}, {"empty holder-reach-id", stypes.MsgBindMailbox{ServiceID: "s", MailboxID: "m", HolderReachID: "", Signer: "r"}, false}, {"empty signer", stypes.MsgBindMailbox{ServiceID: "s", MailboxID: "m", HolderReachID: "h", 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) } } } // TestServicesMsgGetSigners asserts each Msg* GetSigners returns the // signer as sdk.AccAddress bytes. func TestServicesMsgGetSigners(t *testing.T) { m1 := &stypes.MsgRegisterService{Signer: "reach-op"} if got := m1.GetSigners(); len(got) != 1 || string(got[0]) != "reach-op" { t.Errorf("MsgRegisterService GetSigners = %v, want [reach-op]", got) } m2 := &stypes.MsgActivateService{Signer: "h2"} if string(m2.GetSigners()[0]) != "h2" { t.Errorf("MsgActivateService GetSigners = %v", m2.GetSigners()) } m3 := &stypes.MsgSuspendService{Signer: "h3"} if string(m3.GetSigners()[0]) != "h3" { t.Errorf("MsgSuspendService GetSigners = %v", m3.GetSigners()) } m4 := &stypes.MsgRevokeService{Signer: "h4"} if string(m4.GetSigners()[0]) != "h4" { t.Errorf("MsgRevokeService GetSigners = %v", m4.GetSigners()) } m5 := &stypes.MsgIssueCareGrant{Signer: "h5"} if string(m5.GetSigners()[0]) != "h5" { t.Errorf("MsgIssueCareGrant GetSigners = %v", m5.GetSigners()) } m6 := &stypes.MsgActivateSIM{Signer: "h6"} if string(m6.GetSigners()[0]) != "h6" { t.Errorf("MsgActivateSIM GetSigners = %v", m6.GetSigners()) } m7 := &stypes.MsgProvisionVault{Signer: "h7"} if string(m7.GetSigners()[0]) != "h7" { t.Errorf("MsgProvisionVault GetSigners = %v", m7.GetSigners()) } m8 := &stypes.MsgBindMailbox{Signer: "h8"} if string(m8.GetSigners()[0]) != "h8" { t.Errorf("MsgBindMailbox GetSigners = %v", m8.GetSigners()) } } // --- Keeper store helpers ---------------------------------------------------- // TestSetGetService exercises the exported Keeper accessors that the // simtest above does not directly hit (SetService direct, AllServices, // per-kind stores round-trip, marshal-error paths) to push coverage // >=80%. func TestSetGetService(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) // Empty-store accessor. if got := k.AllServices(ctx); len(got) != 0 { t.Errorf("AllServices empty = %d, want 0", len(got)) } // Direct SetService + read back. k.SetService(ctx, stypes.ServiceInfo{ServiceID: "direct-1", Kind: stypes.KindCare, Status: stypes.ServiceActive}) if s, ok := k.GetService(ctx, "direct-1"); !ok || s.Kind != stypes.KindCare { t.Errorf("GetService = %+v ok=%v", s, ok) } if got := k.AllServices(ctx); len(got) != 1 { t.Errorf("AllServices = %d, want 1", len(got)) } // Missing id. if _, ok := k.GetService(ctx, "missing"); ok { t.Error("GetService should return false for missing id") } } // TestPerKindStoreRoundTrip exercises the per-kind store Set/Get accessors. func TestPerKindStoreRoundTrip(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) // Care. k.SetCareService(ctx, stypes.CareService{CareID: "c1", CareKind: "k"}) if c, ok := k.GetCareService(ctx, "c1"); !ok || c.CareKind != "k" { t.Errorf("GetCareService = %+v ok=%v", c, ok) } if _, ok := k.GetCareService(ctx, "missing"); ok { t.Error("GetCareService should return false for missing id") } // SIM. k.SetSIMService(ctx, stypes.SIMService{SIMID: "s1", Carrier: "carrier"}) if s, ok := k.GetSIMService(ctx, "s1"); !ok || s.Carrier != "carrier" { t.Errorf("GetSIMService = %+v ok=%v", s, ok) } if _, ok := k.GetSIMService(ctx, "missing"); ok { t.Error("GetSIMService should return false for missing id") } // Vault. k.SetVaultService(ctx, stypes.VaultService{VaultID: "v1", StorageQuotaGrain: 1000}) if v, ok := k.GetVaultService(ctx, "v1"); !ok || v.StorageQuotaGrain != 1000 { t.Errorf("GetVaultService = %+v ok=%v", v, ok) } if _, ok := k.GetVaultService(ctx, "missing"); ok { t.Error("GetVaultService should return false for missing id") } // Mail. k.SetMailService(ctx, stypes.MailService{MailID: "m1", MailboxID: "mb", HolderReachID: "h"}) if m, ok := k.GetMailService(ctx, "m1"); !ok || m.MailboxID != "mb" { t.Errorf("GetMailService = %+v ok=%v", m, ok) } if _, ok := k.GetMailService(ctx, "missing"); ok { t.Error("GetMailService should return false for missing id") } } // TestStoreMarshalErrorPaths exercises the marshal-error branches on // each store's Get accessor (corrupt bytes in store). func TestStoreMarshalErrorPaths(t *testing.T) { ctx, _, _, sk, k := newSimtestContext(t) store := ctx.KVStore(sk) // Corrupt ServiceInfo bytes. store.Set([]byte("svc/corrupt-svc"), []byte("not-json")) if _, ok := k.GetService(ctx, "corrupt-svc"); ok { t.Error("GetService on corrupt bytes should return false") } // Corrupt CareService bytes. store.Set([]byte("kind/care/corrupt-care"), []byte("not-json")) if _, ok := k.GetCareService(ctx, "corrupt-care"); ok { t.Error("GetCareService on corrupt bytes should return false") } // Corrupt SIMService bytes. store.Set([]byte("kind/sim/corrupt-sim"), []byte("not-json")) if _, ok := k.GetSIMService(ctx, "corrupt-sim"); ok { t.Error("GetSIMService on corrupt bytes should return false") } // Corrupt VaultService bytes. store.Set([]byte("kind/vault/corrupt-vault"), []byte("not-json")) if _, ok := k.GetVaultService(ctx, "corrupt-vault"); ok { t.Error("GetVaultService on corrupt bytes should return false") } // Corrupt MailService bytes. store.Set([]byte("kind/mail/corrupt-mail"), []byte("not-json")) if _, ok := k.GetMailService(ctx, "corrupt-mail"); ok { t.Error("GetMailService on corrupt bytes should return false") } } // TestSetWindowKeeperPostConstruction exercises the SetWindowKeeper // post-construction wiring setter. func TestSetWindowKeeperPostConstruction(t *testing.T) { _, _, _, sk, _ := newSimtestContext(t) // Construct with nil WindowKeeper. k := keeper.NewKeeper(newTestCodec(), sk, nil, &stubVaultKeeper{}) // Re-wire post-construction. wk := &stubWindowKeeper{} k.SetWindowKeeper(wk) if k.WindowKeeper() == nil { t.Error("SetWindowKeeper should wire the shim") } } // TestSetVaultKeeperPostConstruction exercises the SetVaultKeeper // post-construction wiring setter. func TestSetVaultKeeperPostConstruction(t *testing.T) { _, _, _, sk, _ := newSimtestContext(t) k := keeper.NewKeeper(newTestCodec(), sk, &stubWindowKeeper{}, nil) vk := &stubVaultKeeper{} k.SetVaultKeeper(vk) if k.VaultKeeper() == nil { t.Error("SetVaultKeeper should wire the shim") } } // TestUnwrapCtxPanic asserts unwrapCtx panics on a non-sdk.Context value. func TestUnwrapCtxPanic(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("unwrapCtx on non-sdk.Context should panic") } }() _, _ = keeper.NewMsgServerImpl(keeper.Keeper{}).RevokeService("not-a-ctx", &stypes.MsgRevokeService{ServiceID: "s", Signer: "r"}) } // TestRegisterServicePerKindMetadataCreated asserts RegisterService // creates the per-kind metadata record for each ServiceKind (the kind is // fixed at registration; the per-kind handlers later populate the // operational fields). func TestRegisterServicePerKindMetadataCreated(t *testing.T) { ctx, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) // Care. srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "meta-care", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, ok := k.GetCareService(ctx, "meta-care"); !ok { t.Error("CareService metadata should be created on RegisterService(Kind=Care)") } // SIM. srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "meta-sim", Kind: stypes.KindSIM, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, ok := k.GetSIMService(ctx, "meta-sim"); !ok { t.Error("SIMService metadata should be created on RegisterService(Kind=SIM)") } // Vault. srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "meta-vault", Kind: stypes.KindVault, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, ok := k.GetVaultService(ctx, "meta-vault"); !ok { t.Error("VaultService metadata should be created on RegisterService(Kind=Vault)") } // Mail. srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "meta-mail", Kind: stypes.KindMail, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if _, ok := k.GetMailService(ctx, "meta-mail"); !ok { t.Error("MailService metadata should be created on RegisterService(Kind=Mail)") } } // TestWindowKeeperErrorRejected asserts the WindowKeeper shim returning // an error is treated as not-Active (the op is REJECTED — A-552). func TestWindowKeeperErrorRejected(t *testing.T) { ctx, wk, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) wk.err = fmt.Errorf("window keeper unavailable") _, err := srv.RegisterService(ctx, &stypes.MsgRegisterService{ ServiceID: "svc-err", Kind: stypes.KindCare, OperatorReachID: "r", Name: "n", WindowID: "w", Signer: "r", }) if err == nil { t.Error("RegisterService should be rejected when WindowKeeper returns an error (A-552)") } if !strings.Contains(err.Error(), "window-grant check") { t.Errorf("error = %q, want 'window-grant check'", err.Error()) } } // --- ServiceStatus enum helpers (regression firewall — ServiceStatusCount=4) -- func TestAllServiceStatusesCount(t *testing.T) { if len(stypes.AllServiceStatuses()) != stypes.ServiceStatusCount { t.Errorf("AllServiceStatuses len = %d, want %d", len(stypes.AllServiceStatuses()), stypes.ServiceStatusCount) } if stypes.ServiceStatusCount != 4 { t.Errorf("ServiceStatusCount = %d, want 4 (REQ-025 LOCKED)", stypes.ServiceStatusCount) } } func TestAllServiceStatusesNames(t *testing.T) { want := []string{"Pending", "Active", "Suspended", "Revoked"} all := stypes.AllServiceStatuses() 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("AllServiceStatuses()[%d] = %q, want %q", i, s, want[i]) } } } func TestIsTerminalServiceStatus(t *testing.T) { if stypes.IsTerminalServiceStatus(stypes.ServicePending) { t.Error("Pending should not be terminal") } if stypes.IsTerminalServiceStatus(stypes.ServiceActive) { t.Error("Active should not be terminal") } if stypes.IsTerminalServiceStatus(stypes.ServiceSuspended) { t.Error("Suspended should not be terminal") } if !stypes.IsTerminalServiceStatus(stypes.ServiceRevoked) { t.Error("Revoked should be terminal") } } func TestValidServiceTransition(t *testing.T) { // Valid transitions. validCases := []struct { from, to stypes.ServiceStatus }{ {stypes.ServicePending, stypes.ServiceActive}, {stypes.ServicePending, stypes.ServiceRevoked}, {stypes.ServiceActive, stypes.ServiceSuspended}, {stypes.ServiceActive, stypes.ServiceRevoked}, {stypes.ServiceSuspended, stypes.ServiceRevoked}, } for _, c := range validCases { if !stypes.ValidServiceTransition(c.from, c.to) { t.Errorf("ValidServiceTransition(%q, %q) = false, want true", c.from, c.to) } } // Invalid transitions. invalidCases := []struct { from, to stypes.ServiceStatus }{ {stypes.ServiceActive, stypes.ServicePending}, // no backward to Pending {stypes.ServiceSuspended, stypes.ServiceActive}, // no Suspended -> Active (v0.5 scope) {stypes.ServiceSuspended, stypes.ServicePending}, // no backward to Pending {stypes.ServiceRevoked, stypes.ServicePending}, // terminal — no out {stypes.ServiceRevoked, stypes.ServiceActive}, // terminal — no out {stypes.ServiceRevoked, stypes.ServiceSuspended}, // terminal — no out {stypes.ServicePending, stypes.ServiceSuspended}, // must Activate before Suspend } for _, c := range invalidCases { if stypes.ValidServiceTransition(c.from, c.to) { t.Errorf("ValidServiceTransition(%q, %q) = true, want false", c.from, c.to) } } } // --- G-003 import-invariant (test exemption documentation) ----------------- // TestG003NoWindowOrVaultTypesImport asserts the services production // files do NOT import x/window/types or x/vault/types by struct (G-003 // — the WindowKeeper and VaultKeeper interfaces are the only coupling; // no struct import). This is a tested invariant. The full project-wide // G-003 invariant is enforced by the x/window/types G-003 meta-test // (scans all x/**/*.go including the new x/services files); here we do // a lightweight assertion: the stub WindowKeeper and VaultKeeper in // this simtest file satisfy the interfaces by-ID-string (not by struct // import). func TestG003NoWindowOrVaultTypesImport(t *testing.T) { wk := &stubWindowKeeper{} if st, err := wk.GetWindowStatus("window-by-id"); err != nil || st != stypes.WindowStatusActive { t.Errorf("stub GetWindowStatus by-ID-string should return Active; got %q err=%v", st, err) } if len(wk.calls) != 1 { t.Errorf("expected 1 window call recorded, got %d", len(wk.calls)) } vk := &stubVaultKeeper{} if err := vk.ProvisionVault("svc-by-id", 1000); err != nil { t.Errorf("stub ProvisionVault by-ID-string should succeed: %v", err) } if len(vk.calls) != 1 { t.Errorf("expected 1 vault call recorded, got %d", len(vk.calls)) } }