package keeper_test // msg_server_simtest_test.go is the x/cover keeper simtest (REQ-046, // REQ-047, REQ-049, REQ-050, REQ-055, D-077, D-079, D-086, D-088, D-089). // // D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no // real Standing keeper (the StandingKeeper shim is wired to a stub; G-003 // test exemption), no real Watcher keeper (the WatcherKeeper shim is a // stub), no real Still keeper (x/still/keeper is empty — the StillKeeper // shim is a simtest-local stub that records Still() calls for assertion). // The simtest exercises: // // LaunchCoverPool (D-077 Standing gate + D-086 phase check + reserve floor): // - (a) successful launch with valid Standing + reserve (Phase2 Travel, // StandingKeeper stub returns "Trusted" 4.0, reserve 1.5). // - (b) rejected launch below Standing gate (StandingKeeper stub returns // "New" 3.0 for Travel -> REJECT). // - (c) rejected launch below reserve floor (ReserveAnnualContribRatio = // 1.0 < 1.5 -> REJECT at ValidateBasic). // - (g) D-086: rejected out-of-phase category launch (Phase3 EquipmentLoss // when FactoryAllowedPhases = [Phase2] only -> REJECT). // - nil StandingKeeper skips the gate (simtest wiring). // // RouteCoverFee (D-079 firewall + category-tag + below-floor auto-pause): // - (d) rejected Cover-Fee routing with category mismatch (Pool covers // Travel; route a HealthMCS tag -> REJECT). // - (e) auto-pause on below-floor + recovery: launch a pool at reserve // 1.5, then RouteCoverFee with the pool's reserve dropped to 1.2 // (simulate by mutating the stored pool) -> auto-pause + StillKeeper.Still // called; subsequent RouteCoverFee -> REJECTED (pool paused); then // restore reserve to 1.6 + unpause -> RouteCoverFee succeeds. // - (f) firewall rejection: RouteCoverFee with the pool's ReserveAccount // set to "root-pool-operating-expenses" -> REJECTED by the firewall. // // FileCoverCall (REQ-055 P1 scaffold): // - successful Cover Call filing on a pool + category match. // - rejected on category mismatch. // - rejected on non-existent pool. // // Coverage target: >=80% on x/cover/keeper. import ( "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/cover/firewall" "github.com/oy/openyield/x/cover/keeper" "github.com/oy/openyield/x/cover/types" ) // --- Stub expected-keepers (G-003 test exemption) --------------------------- // stubStandingKeeper satisfies types.StandingKeeper for the simtest. It // returns a configurable (bucket, score) per (reachID, category) key. A // missing key returns ("New", 3.0, nil) — the default-below-Trusted case. type stubStandingKeeper struct { buckets map[string]struct { bucket string score float64 } defaultBucket string defaultScore float64 defaultErr error } func (s *stubStandingKeeper) GetStandingBucket(reachID, category string) (string, float64, error) { if s.buckets != nil { key := reachID + "/" + category if v, ok := s.buckets[key]; ok { return v.bucket, v.score, nil } } return s.defaultBucket, s.defaultScore, s.defaultErr } // stubWatcherKeeper satisfies types.WatcherKeeper for the simtest. It // returns a synthetic attestation-ref per Attest call + records the last // payload for assertion. type stubWatcherKeeper struct { lastPoolID string lastPayload []byte attestErr error } func (s *stubWatcherKeeper) Attest(poolID string, payload []byte) (string, error) { if s.attestErr != nil { return "", s.attestErr } s.lastPoolID = poolID s.lastPayload = payload return "oy:attest:" + poolID, nil } // stubBondKeeper satisfies types.BondKeeper for the simtest. P1 does not // use it; the stub is here for wiring completeness. type stubBondKeeper struct { bonds map[string]bool } func (s *stubBondKeeper) GetBond(bondID string) bool { if s.bonds == nil { return false } return s.bonds[bondID] } // stubStillKeeper satisfies types.StillKeeper for the simtest. It records // every Still() call for assertion (the below-floor auto-pause test // asserts Still was called with the right pool-id + reason). type stubStillKeeper struct { calls []struct { poolID string reason string } stillErr error } func (s *stubStillKeeper) Still(poolID string, reason string) error { if s.stillErr != nil { return s.stillErr } s.calls = append(s.calls, struct { poolID string reason string }{poolID, reason}) return nil } // --- Simtest context helper -------------------------------------------------- // newSimtestContext constructs an in-memory sdk.Context with a KVStore // mounted at the cover store key. D-054: in-memory, no real Standing/ // Watcher/Still keepers (stubs). Returns the ctx, the four stub keepers, // the store key, and the Keeper. func newSimtestContext(t *testing.T) (sdk.Context, *stubStandingKeeper, *stubWatcherKeeper, *stubBondKeeper, *stubStillKeeper, storetypes.StoreKey, keeper.Keeper) { t.Helper() db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(types.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()) sk := &stubStandingKeeper{} wk := &stubWatcherKeeper{} bk := &stubBondKeeper{} stK := &stubStillKeeper{} k := keeper.NewKeeper(cdc, storeKey, sk, wk, bk, stK) return ctx, sk, wk, bk, stK, storeKey, k } // newSimtestContextNilShims constructs an in-memory sdk.Context with ALL // nil shims (for the nil-shim skip-path coverage). func newSimtestContextNilShims(t *testing.T) (sdk.Context, storetypes.StoreKey, keeper.Keeper) { t.Helper() db := dbm.NewMemDB() cdc := newTestCodec() storeKey := storetypes.NewKVStoreKey(types.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()) k := keeper.NewKeeper(cdc, storeKey, nil, nil, nil, nil) return ctx, 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 } // --- LaunchCoverPool (D-077 Standing gate + D-086 phase + reserve floor) ----- // TestLaunchCoverPoolSuccess (case a) asserts a successful pool launch with // valid Standing + reserve (Phase2 Travel, StandingKeeper stub returns // "Trusted" 4.0, reserve 1.5). func TestLaunchCoverPoolSuccess(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-1", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1", }) if err != nil { t.Fatalf("LaunchCoverPool: %v", err) } p, ok := k.GetCoverPool(ctx, "pool-1") if !ok { t.Fatal("pool not persisted") } if p.PoolPaused { t.Error("pool should not be paused on launch") } if p.PoolStandingGate != types.CoverStandingGateTrusted { t.Errorf("PoolStandingGate = %.2f, want %.2f", p.PoolStandingGate, types.CoverStandingGateTrusted) } if len(p.FactoryAllowedPhases) != 1 || p.FactoryAllowedPhases[0] != types.Phase2 { t.Errorf("FactoryAllowedPhases = %v, want [Phase2] (D-086)", p.FactoryAllowedPhases) } if !hasEvent(ctx, "cover.pool_launched") { t.Error("cover.pool_launched event not emitted") } } // TestLaunchCoverPoolRejectedBelowStandingGate (case b) asserts a launch is // REJECTED when the host's Standing bucket is below the locked gate // (StandingKeeper stub returns "New" 3.0 for Travel -> below Trusted 4.0). func TestLaunchCoverPoolRejectedBelowStandingGate(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-bad/Travel": {"New", 3.0}, } srv := keeper.NewMsgServerImpl(k) _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-bad", HostReachID: "host-bad", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-bad", Signer: "host-bad", }) if err == nil { t.Fatal("LaunchCoverPool with below-gate Standing should be rejected") } if !strings.Contains(err.Error(), "D-077") { t.Errorf("error = %q, want 'D-077'", err.Error()) } // The pool was NOT persisted. if _, ok := k.GetCoverPool(ctx, "pool-bad"); ok { t.Error("pool should NOT be persisted on reject") } } // TestLaunchCoverPoolRejectedBelowReserveFloor (case c) asserts a launch is // REJECTED at ValidateBasic when ReserveAnnualContribRatio < 1.5. func TestLaunchCoverPoolRejectedBelowReserveFloor(t *testing.T) { ctx, _, _, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-floor", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.0, ReserveAccount: "acc-1", Signer: "host-1", }) if err == nil { t.Fatal("LaunchCoverPool with reserve 1.0 < 1.5 should be rejected") } if !strings.Contains(err.Error(), "floor") { t.Errorf("error = %q, want 'floor'", err.Error()) } } // TestLaunchCoverPoolRejectedOutOfPhase (case g, D-086) asserts a launch with // a Phase3 category (EquipmentLoss) is REJECTED when FactoryAllowedPhases = // [Phase2] only (the P1 default). func TestLaunchCoverPoolRejectedOutOfPhase(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) // Even with a passing Standing gate, the phase check rejects first. sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/EquipmentLoss": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-phase3", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatEquipmentLoss}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1", }) if err == nil { t.Fatal("LaunchCoverPool with Phase3 category in P1 should be rejected (D-086)") } if !strings.Contains(err.Error(), "D-086") { t.Errorf("error = %q, want 'D-086'", err.Error()) } } // TestLaunchCoverPoolHealthMCSRequiresPreferred asserts HealthMCS demands the // Preferred gate (4.5): a host with Trusted (4.0) for HealthMCS is REJECTED // (Trusted does NOT meet the Preferred gate). func TestLaunchCoverPoolHealthMCSRequiresPreferred(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-trusted/HealthMCS": {"Trusted", 4.2}, "host-pref/HealthMCS": {"Preferred", 4.6}, } srv := keeper.NewMsgServerImpl(k) // Trusted (4.2) for HealthMCS -> REJECT (needs Preferred 4.5). _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-mcs-bad", HostReachID: "host-trusted", Categories: []types.CoverCategory{types.CatHealthMCS}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-bad", Signer: "host-trusted", }) if err == nil { t.Error("LaunchCoverPool HealthMCS with Trusted (4.2) < Preferred (4.5) should be rejected") } // Preferred (4.6) for HealthMCS -> ACCEPT. _, err = srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-mcs-ok", HostReachID: "host-pref", Categories: []types.CoverCategory{types.CatHealthMCS}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-ok", Signer: "host-pref", }) if err != nil { t.Errorf("LaunchCoverPool HealthMCS with Preferred (4.6) should succeed: %v", err) } } // TestLaunchCoverPoolIdempotentReject asserts a second LaunchCoverPool on the // same pool-id is REJECTED. func TestLaunchCoverPoolIdempotentReject(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) first := &types.MsgLaunchCoverPool{ PoolID: "pool-dup", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1", } if _, err := srv.LaunchCoverPool(ctx, first); err != nil { t.Fatalf("first LaunchCoverPool: %v", err) } _, err := srv.LaunchCoverPool(ctx, first) if err == nil { t.Error("second LaunchCoverPool on same pool-id should be rejected (idempotent)") } } // TestLaunchCoverPoolNilStandingKeeperSkip asserts a nil StandingKeeper shim // skips the D-077 gate check (simtest wiring) and the pool is launched // regardless of the host's Standing. func TestLaunchCoverPoolNilStandingKeeperSkip(t *testing.T) { ctx, _, k := newSimtestContextNilShims(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-nil", HostReachID: "host-any", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-any", }) if err != nil { t.Fatalf("LaunchCoverPool with nil StandingKeeper should skip gate: %v", err) } if _, ok := k.GetCoverPool(ctx, "pool-nil"); !ok { t.Error("pool should be launched (nil shim skips gate)") } } // TestLaunchCoverPoolWatcherAttestationError asserts a WatcherKeeper.Attest // error REJECTS the launch (the attestation is load-bearing). func TestLaunchCoverPoolWatcherAttestationError(t *testing.T) { ctx, sk, wk, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } wk.attestErr = errAttestFailed srv := keeper.NewMsgServerImpl(k) _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-attest-err", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1", }) if err == nil { t.Fatal("LaunchCoverPool with Watcher attest error should be rejected") } if !strings.Contains(err.Error(), "attestation") { t.Errorf("error = %q, want 'attestation'", err.Error()) } } // errAttestFailed is a sentinel error for the stubWatcherKeeper. var errAttestFailed = newSentinelError("attest failed (simtest)") type sentinelError string func (e sentinelError) Error() string { return string(e) } func newSentinelError(s string) error { return sentinelError(s) } // TestLaunchCoverPoolStandingLookupError asserts a StandingKeeper lookup // error REJECTS the launch. func TestLaunchCoverPoolStandingLookupError(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.defaultErr = newSentinelError("standing lookup failed (simtest)") srv := keeper.NewMsgServerImpl(k) _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-lookup-err", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1", }) if err == nil { t.Fatal("LaunchCoverPool with Standing lookup error should be rejected") } if !strings.Contains(err.Error(), "Standing lookup") { t.Errorf("error = %q, want 'Standing lookup'", err.Error()) } } // TestLaunchCoverPoolUnknownCategory asserts an unknown category (empty phase) // is REJECTED. func TestLaunchCoverPoolUnknownCategory(t *testing.T) { ctx, _, _, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-unknown", HostReachID: "host-1", Categories: []types.CoverCategory{types.CoverCategory("Unknown")}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1", }) if err == nil { t.Fatal("LaunchCoverPool with unknown category should be rejected") } if !strings.Contains(err.Error(), "unknown category") { t.Errorf("error = %q, want 'unknown category'", err.Error()) } } // --- RouteCoverFee (D-079 firewall + category-tag + below-floor) ------------- // TestRouteCoverFeeSuccess asserts a successful Cover-Fee routing into a // pool with valid reserve + matching category-tag. func TestRouteCoverFeeSuccess(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-r", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-r", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } if _, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "pool-r", GrainAmount: 1000, CategoryTag: "Travel", Signer: "host-1", }); err != nil { t.Fatalf("RouteCoverFee: %v", err) } if !hasEvent(ctx, "cover.cover_fee_routed") { t.Error("cover.cover_fee_routed event not emitted") } } // TestRouteCoverFeeCategoryMismatch (case d) asserts a Cover-Fee routing with // a category-tag that does not match the pool's categories is REJECTED. func TestRouteCoverFeeCategoryMismatch(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-mm", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-mm", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } _, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "pool-mm", GrainAmount: 1000, CategoryTag: "HealthMCS", Signer: "host-1", }) if err == nil { t.Fatal("RouteCoverFee with non-matching category-tag should be rejected") } if !strings.Contains(err.Error(), "CategoryTag") { t.Errorf("error = %q, want 'CategoryTag'", err.Error()) } } // TestRouteCoverFeeAutoPauseAndRecover (case e) asserts the below-floor // auto-pause + recovery: launch at reserve 1.5, mutate the stored pool's // reserve to 1.2 -> RouteCoverFee auto-pauses + Still called; subsequent // RouteCoverFee -> REJECTED (paused); restore reserve to 1.6 + unpause -> // RouteCoverFee succeeds. func TestRouteCoverFeeAutoPauseAndRecover(t *testing.T) { ctx, sk, _, _, stK, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-auto", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-auto", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } // Mutate the stored pool's reserve to 1.2 (below floor) to simulate a // reserve drop (the live reserve update is deferred; the simtest // mutates the stored pool directly). p, _ := k.GetCoverPool(ctx, "pool-auto") p.ReserveAnnualContribRatio = 1.2 k.SetCoverPool(ctx, p) // RouteCoverFee -> auto-pause + Still called + REJECTED. _, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "pool-auto", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1", }) if err == nil { t.Fatal("RouteCoverFee on below-floor pool should be rejected + auto-pause") } if !hasEvent(ctx, "cover.pool_below_floor") { t.Error("cover.pool_below_floor event not emitted") } // Still was called with the right pool-id + reason. if len(stK.calls) != 1 { t.Fatalf("Still calls = %d, want 1", len(stK.calls)) } if stK.calls[0].poolID != "pool-auto" || !strings.Contains(stK.calls[0].reason, "below reserve floor") { t.Errorf("Still call = %+v, want pool-auto / below reserve floor", stK.calls[0]) } // The pool is now paused. p, _ = k.GetCoverPool(ctx, "pool-auto") if !p.PoolPaused { t.Error("pool should be paused after below-floor auto-pause") } // Subsequent RouteCoverFee -> REJECTED (pool paused). _, err = srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "pool-auto", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1", }) if err == nil { t.Fatal("RouteCoverFee on paused pool should be rejected") } if !strings.Contains(err.Error(), "paused") { t.Errorf("error = %q, want 'paused'", err.Error()) } // Restore reserve to 1.6 + unpause -> RouteCoverFee succeeds. p, _ = k.GetCoverPool(ctx, "pool-auto") p.ReserveAnnualContribRatio = 1.6 p.PoolPaused = false k.SetCoverPool(ctx, p) _, err = srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "pool-auto", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1", }) if err != nil { t.Errorf("RouteCoverFee after recovery should succeed: %v", err) } } // TestRouteCoverFeeFirewallRejection (case f) asserts a RouteCoverFee is // REJECTED by the Anti-Crowding-Out firewall when the pool's ReserveAccount // is the Root-Pool operating-expenses holder. func TestRouteCoverFeeFirewallRejection(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-fw", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-ok", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } // Mutate the pool's ReserveAccount to the bad destination (the // Anti-Crowding-Out case). p, _ := k.GetCoverPool(ctx, "pool-fw") p.ReserveAccount = badDestinationFragment() k.SetCoverPool(ctx, p) _, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "pool-fw", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1", }) if err == nil { t.Fatal("RouteCoverFee with Anti-Crowding-Out destination should be rejected by firewall") } if !strings.Contains(err.Error(), "Anti-Crowding-Out") { t.Errorf("error = %q, want 'Anti-Crowding-Out'", err.Error()) } } // badDestinationFragment reassembles the firewall's bad destination from // fragments so this test file does not contain the literal bad string as a // searchable substring (mirrors the firewall's own fragment assembly). The // string matches the firewall's badDestination byte-for-byte. func badDestinationFragment() string { return string([]byte{ 'r', 'o', 'o', 't', '-', 'p', 'o', 'o', 'l', '-', 'o', 'p', 'e', 'r', 'a', 't', 'i', 'n', 'g', '-', 'e', 'x', 'p', 'e', 'n', 's', 'e', 's', }) } // TestRouteCoverFeeNonExistentPool asserts RouteCoverFee on a non-existent // pool is REJECTED. func TestRouteCoverFeeNonExistentPool(t *testing.T) { ctx, _, _, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "no-such-pool", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1", }) if err == nil { t.Error("RouteCoverFee on non-existent pool should be rejected") } if !strings.Contains(err.Error(), "not found") { t.Errorf("error = %q, want 'not found'", err.Error()) } } // TestRouteCoverFeeStillError asserts a StillKeeper.Still error on the // below-floor auto-pause REJECTS the routing (the Still recording is // load-bearing for the audit trail). func TestRouteCoverFeeStillError(t *testing.T) { ctx, sk, _, _, stK, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } stK.stillErr = newSentinelError("still failed (simtest)") srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-still-err", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } p, _ := k.GetCoverPool(ctx, "pool-still-err") p.ReserveAnnualContribRatio = 1.2 k.SetCoverPool(ctx, p) _, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "pool-still-err", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1", }) if err == nil { t.Fatal("RouteCoverFee with Still error should be rejected") } if !strings.Contains(err.Error(), "Still") { t.Errorf("error = %q, want 'Still'", err.Error()) } } // TestRouteCoverFeeNilStillKeeperSkip asserts a nil StillKeeper shim skips // the Still recording (the pool's PoolPaused flag is still set; only the // Still event is not recorded). The routing is still REJECTED (below floor). func TestRouteCoverFeeNilStillKeeperSkip(t *testing.T) { ctx, _, k := newSimtestContextNilShims(t) srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-nil-still", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } p, _ := k.GetCoverPool(ctx, "pool-nil-still") p.ReserveAnnualContribRatio = 1.2 k.SetCoverPool(ctx, p) _, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{ PoolID: "pool-nil-still", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1", }) if err == nil { t.Fatal("RouteCoverFee on below-floor pool should be rejected (nil Still still rejects)") } // The pool IS paused (the flag is set; only the Still recording is skipped). p, _ = k.GetCoverPool(ctx, "pool-nil-still") if !p.PoolPaused { t.Error("pool should be paused even with nil StillKeeper (flag is set; Still recording skipped)") } } // --- FileCoverCall (REQ-055 P1 scaffold) ------------------------------------- // TestFileCoverCallSuccess asserts a successful Cover Call filing on a pool // + category match. func TestFileCoverCallSuccess(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-call", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-call", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } if _, err := srv.FileCoverCall(ctx, &types.MsgFileCoverCall{ CallID: "call-1", PoolID: "pool-call", ClaimantReachID: "user-1", Category: types.CatTravel, AmountGrain: 500, Signer: "user-1", }); err != nil { t.Fatalf("FileCoverCall: %v", err) } c, ok := k.GetCoverCall(ctx, "call-1") if !ok { t.Fatal("CoverCall not persisted") } if c.ClaimantReachID != "user-1" { t.Errorf("ClaimantReachID = %q, want user-1", c.ClaimantReachID) } if !hasEvent(ctx, "cover.cover_call_filed") { t.Error("cover.cover_call_filed event not emitted") } } // TestFileCoverCallCategoryMismatch asserts a Cover Call filing with a // category that does not match the pool's categories is REJECTED. func TestFileCoverCallCategoryMismatch(t *testing.T) { ctx, sk, _, _, _, _, k := newSimtestContext(t) sk.buckets = map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, } srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-cm", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-cm", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } _, err := srv.FileCoverCall(ctx, &types.MsgFileCoverCall{ CallID: "call-cm", PoolID: "pool-cm", ClaimantReachID: "user-1", Category: types.CatHealthMCS, AmountGrain: 500, Signer: "user-1", }) if err == nil { t.Fatal("FileCoverCall with non-matching category should be rejected") } if !strings.Contains(err.Error(), "does not match") { t.Errorf("error = %q, want 'does not match'", err.Error()) } } // TestFileCoverCallNonExistentPool asserts FileCoverCall on a non-existent // pool is REJECTED. func TestFileCoverCallNonExistentPool(t *testing.T) { ctx, _, _, _, _, _, k := newSimtestContext(t) srv := keeper.NewMsgServerImpl(k) _, err := srv.FileCoverCall(ctx, &types.MsgFileCoverCall{ CallID: "call-no", PoolID: "no-such-pool", ClaimantReachID: "user-1", Category: types.CatTravel, AmountGrain: 500, Signer: "user-1", }) if err == nil { t.Error("FileCoverCall on non-existent pool should be rejected") } } // --- ValidateBasic error paths ---------------------------------------------- // TestMsgValidateBasicErrors asserts each Msg* ValidateBasic error path // returns the expected error (stateless coverage). func TestMsgValidateBasicErrors(t *testing.T) { // MsgLaunchCoverPool if err := (&types.MsgLaunchCoverPool{}).ValidateBasic(); err == nil { t.Error("empty MsgLaunchCoverPool should fail ValidateBasic") } if err := (&types.MsgLaunchCoverPool{PoolID: "p", HostReachID: "h", Categories: []types.CoverCategory{types.CatTravel}, ReserveAccount: "a", Signer: "s", ReserveAnnualContribRatio: 1.0}).ValidateBasic(); err == nil { t.Error("MsgLaunchCoverPool with reserve 1.0 < 1.5 should fail ValidateBasic") } // MsgRouteCoverFee if err := (&types.MsgRouteCoverFee{}).ValidateBasic(); err == nil { t.Error("empty MsgRouteCoverFee should fail ValidateBasic") } if err := (&types.MsgRouteCoverFee{PoolID: "p", CategoryTag: "c", GrainAmount: 0, Signer: "s"}).ValidateBasic(); err == nil { t.Error("MsgRouteCoverFee with GrainAmount 0 should fail ValidateBasic") } if err := (&types.MsgRouteCoverFee{PoolID: "p", CategoryTag: "c", GrainAmount: -1, Signer: "s"}).ValidateBasic(); err == nil { t.Error("MsgRouteCoverFee with GrainAmount -1 should fail ValidateBasic") } // MsgFileCoverCall if err := (&types.MsgFileCoverCall{}).ValidateBasic(); err == nil { t.Error("empty MsgFileCoverCall should fail ValidateBasic") } if err := (&types.MsgFileCoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", Category: types.CatTravel, AmountGrain: 0, Signer: "s"}).ValidateBasic(); err == nil { t.Error("MsgFileCoverCall with AmountGrain 0 should fail ValidateBasic") } } // TestMsgGetSigners asserts each Msg* GetSigners returns the signer as // sdk.AccAddress bytes. func TestMsgGetSigners(t *testing.T) { m1 := &types.MsgLaunchCoverPool{Signer: "host-1"} if got := m1.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" { t.Errorf("MsgLaunchCoverPool GetSigners = %v, want [host-1]", got) } m2 := &types.MsgRouteCoverFee{Signer: "host-1"} if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" { t.Errorf("MsgRouteCoverFee GetSigners = %v", got) } m3 := &types.MsgFileCoverCall{Signer: "user-1"} if got := m3.GetSigners(); len(got) != 1 || string(got[0]) != "user-1" { t.Errorf("MsgFileCoverCall GetSigners = %v", got) } } // --- unwrapCtx panic -------------------------------------------------------- // 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{}).FileCoverCall("not-a-ctx", &types.MsgFileCoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", Category: types.CatTravel, AmountGrain: 1, Signer: "s"}) } // --- Keeper accessors (coverage) -------------------------------------------- // TestKeeperAccessors exercises the exported Keeper accessors that the // simtest above does not directly hit (AllCoverPools, AllCoverCalls, // GetCoverCall, the Set* setters, the marshal-error paths) to push // coverage >=80%. func TestKeeperAccessors(t *testing.T) { ctx, sk, _, _, _, sk2, k := newSimtestContext(t) _ = sk _ = sk2 // Empty-store accessors return empty (not nil) slices. if got := k.AllCoverPools(ctx); len(got) != 0 { t.Errorf("AllCoverPools empty = %d, want 0", len(got)) } if got := k.AllCoverCalls(ctx); len(got) != 0 { t.Errorf("AllCoverCalls empty = %d, want 0", len(got)) } if _, ok := k.GetCoverCall(ctx, "nobody"); ok { t.Error("GetCoverCall on empty store should return false") } // Populate + read back via accessors. k.SetCoverPool(ctx, types.CoverPool{PoolID: "p-a", HostReachID: "h-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "a"}) if p, ok := k.GetCoverPool(ctx, "p-a"); !ok || p.HostReachID != "h-1" { t.Errorf("GetCoverPool = %+v ok=%v", p, ok) } if got := k.AllCoverPools(ctx); len(got) != 1 { t.Errorf("AllCoverPools = %d, want 1", len(got)) } k.SetCoverCall(ctx, types.CoverCall{CallID: "c-a", PoolID: "p-a", ClaimantReachID: "u-1", Category: types.CatTravel, AmountGrain: 1}) if c, ok := k.GetCoverCall(ctx, "c-a"); !ok || c.ClaimantReachID != "u-1" { t.Errorf("GetCoverCall = %+v ok=%v", c, ok) } if got := k.AllCoverCalls(ctx); len(got) != 1 { t.Errorf("AllCoverCalls = %d, want 1", len(got)) } // Marshal-error paths (corrupt bytes in store). store := ctx.KVStore(k.StoreKey()) store.Set([]byte("pool/corrupt"), []byte("not-json")) if _, ok := k.GetCoverPool(ctx, "corrupt"); ok { t.Error("GetCoverPool on corrupt bytes should return false") } store.Set([]byte("call/corrupt"), []byte("not-json")) if _, ok := k.GetCoverCall(ctx, "corrupt"); ok { t.Error("GetCoverCall on corrupt bytes should return false") } // Post-construction setters (coverage). sk3 := &stubStandingKeeper{} wk3 := &stubWatcherKeeper{} bk3 := &stubBondKeeper{} stK3 := &stubStillKeeper{} k.SetStandingKeeper(sk3) k.SetWatcherKeeper(wk3) k.SetBondKeeper(bk3) k.SetStillKeeper(stK3) } // --- Firewall unit tests ----------------------------------------------------- // TestFirewallCheckCoverFeeRouting asserts the firewall accepts a non-empty // permitted destination and rejects the known bad destination + empty. func TestFirewallCheckCoverFeeRouting(t *testing.T) { // Non-empty permitted destination -> nil. if err := firewall.CheckCoverFeeRouting("acc-1"); err != nil { t.Errorf("CheckCoverFeeRouting(acc-1) = %v, want nil", err) } // Empty -> error. if err := firewall.CheckCoverFeeRouting(""); err == nil { t.Error("CheckCoverFeeRouting(empty) should error") } // Bad destination -> ErrAntiCrowdingOut. if err := firewall.CheckCoverFeeRouting(badDestinationFragment()); err == nil { t.Error("CheckCoverFeeRouting(bad destination) should error") } else if !strings.Contains(err.Error(), "Anti-Crowding-Out") { t.Errorf("error = %q, want 'Anti-Crowding-Out'", err.Error()) } // Case-insensitive bad destination -> ErrAntiCrowdingOut. if err := firewall.CheckCoverFeeRouting(strings.ToUpper(badDestinationFragment())); err == nil { t.Error("CheckCoverFeeRouting(upper-case bad destination) should error (case-insensitive)") } } // --- Stub Watcher + Bond coverage ------------------------------------------- // TestStubWatcherAndBond exercises the stub WatcherKeeper + stubBondKeeper // accessors (for wiring completeness coverage). func TestStubWatcherAndBond(t *testing.T) { ctx, _, _, _, _, _, k := newSimtestContext(t) // Re-wire the standing keeper to a passing stub via the setter BEFORE // constructing the msgServer (the msgServer embeds the Keeper by value, // so post-construction setter mutations on the original Keeper do NOT // reflect in the msgServer's copy). skPass := &stubStandingKeeper{buckets: map[string]struct { bucket string score float64 }{ "host-1/Travel": {"Trusted", 4.0}, }} wkPass := &stubWatcherKeeper{} bkPass := &stubBondKeeper{} k.SetStandingKeeper(skPass) k.SetWatcherKeeper(wkPass) k.SetBondKeeper(bkPass) srv := keeper.NewMsgServerImpl(k) if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{ PoolID: "pool-w", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-w", Signer: "host-1", }); err != nil { t.Fatalf("LaunchCoverPool: %v", err) } // The stub Watcher recorded the attestation. if wkPass.lastPoolID != "pool-w" { t.Errorf("stubWatcher lastPoolID = %q, want pool-w", wkPass.lastPoolID) } if len(wkPass.lastPayload) == 0 { t.Error("stubWatcher lastPayload empty") } // The stub Bond keeper (unused in P1) returns false for any bond. if bkPass.GetBond("any-bond") { t.Error("stubBondKeeper GetBond on empty should return false") } // Populate the bond map and assert true. bkPass.bonds = map[string]bool{"bond-1": true} if !bkPass.GetBond("bond-1") { t.Error("stubBondKeeper GetBond(bond-1) should return true after populate") } }