a70d6faa59
---ci--- project: oy phase: 5 milestone: v0.5 status: complete requirements: covered: [REQ-037] partial: [] ---/ci---
1538 lines
59 KiB
Go
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))
|
|
}
|
|
}
|