6d63482c48
Add the new x/cover module (Cover Pool runtime) implementing P1 of the v0.7 milestone: CoverPool/CoverFeeTag/CoverCall types with the 4 GRILL- ratified locked consts (CoverReserveFloorAnnualContribX=1.5, CoverReserveCeilingAnnualContribX=2.5, CoverStandingGateTrusted=4.0, CoverStandingGatePreferred=4.5), the 8-category/3-phase CoverCategory enum with D-086 FactoryAllowedPhases=[Phase2]-only default, three Msg* types (LaunchCoverPool/RouteCoverFee/FileCoverCall) with full sdk.Msg impls, store-backed Keeper with 4 G-003 expected-keeper shims (StandingKeeper/WatcherKeeper/BondKeeper/StillKeeper), and three handlers enforcing the D-077 Standing gate, D-086 category phase check, REQ-047 reserve floor + below-floor auto-pause (D-089(1) Still invocation), and REQ-050 category-tag match. Add the x/cover/firewall subpackage (Anti-Crowding-Out firewall, D-079/ D-088): a stdlib-only leaf checker enforcing RightNoTaxOnPersonalStash by rejecting Cover-Fee routing to the Root-Pool operating-expenses destination (defense in depth with the lexicon meta-test). Add the lexicon_meta_cover meta-test (4th lexicon firewall, D-088): scans x/cover/**/*.go for both lexicon.FindBannedTerm (10 project-wide terms) AND lexicon.FindCoverBannedTerm (4 Cover-specific terms), with G-013 walk-coverage + G-009 self-test tables. Add lexicon.CoverBannedTerms()/FindCoverBannedTerm()/ SyntheticCoverBannedStrings() helpers (additive to the existing project-wide BannedTerms — no changes to existing helpers). Apply D-088(3) optional doc-fix: replace 'insurance-like' with 'Cover-like' in x/pact/types docstrings. Coverage: x/cover/types 97.8%, x/cover/keeper 94.1%, x/cover/firewall 100.0%. go.mod/go.sum unchanged (G-006/G-028). All existing tests pass. REQs: REQ-046, REQ-047, REQ-049, REQ-050 ---ci--- project: oy phase: 1 milestone: v0.7 status: execute ---/ci---
999 lines
37 KiB
Go
999 lines
37 KiB
Go
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")
|
|
}
|
|
}
|