Files
openyield/x/services/keeper/msg_server_simtest_test.go
T
cloudinit-bot a70d6faa59
docs-build / go test ./... (lexicon firewall + all x/* tests) (push) Has been cancelled
docs-build / mkdocs build (docs site artifact) (push) Has been cancelled
Merge phase/05 into milestone/v0.5-bearers-runtime (P5 complete → v0.4.5)
---ci---
project: oy
phase: 5
milestone: v0.5
status: complete
requirements:
  covered: [REQ-037]
  partial: []
---/ci---
2026-08-18 00:57:25 +00:00

1538 lines
59 KiB
Go

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/<module>/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))
}
}