6c34650a0d
v0.5 Bearers Runtime — 7 runtime REQs (REQ-033..039) shipped as feature. 8 modules promoted to runtime (MsgServer + simtest). cosmos-sdk v0.50.8 + ibc-go v8.2.1 added (G-006 controlled exception). G-003 + locked-const firewalls intact. 8 keeper packages ≥80% coverage. 5 GRILL decisions ratified; 8 binding fixes landed; 5 P1+ flagged for v0.6+. ---ci--- project: oy phase: 8 milestone: v0.5 status: complete requirements: covered: [REQ-033, REQ-034, REQ-035, REQ-036, REQ-037, REQ-038, REQ-039] partial: [] ---/ci---
1066 lines
40 KiB
Go
1066 lines
40 KiB
Go
package keeper_test
|
|
|
|
// msg_server_simtest_test.go is the x/bearers keeper simtest (P2-03-01,
|
|
// REQ-034).
|
|
//
|
|
// D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no
|
|
// hardware/RF. The simtest wires the expected-keeper shim (BreadKeeper) to
|
|
// an in-test stub (G-003 test exemption: the test imports x/bearers/keeper
|
|
// + defines a stub BreadKeeper that satisfies the interface; no production
|
|
// struct imports across x/<module>/types).
|
|
//
|
|
// Coverage (A-521, A-522, A-523):
|
|
// - Session lifecycle: Open → Active → Closed; Open → Active → Revoked;
|
|
// rejected-frame-on-Closed/Revoked.
|
|
// - OY-QR one-shot: consume flips consumed BEFORE the transfer effect
|
|
// (A-521); transfer effect via BreadKeeper shim; REPLAY finds
|
|
// consumed==true and errors (idempotent reject, NOT double-effect).
|
|
// - OY-SAT frame send/receive round-trip.
|
|
// - Surveillance-resistant NEGATIVE test: event set has NO geolocation
|
|
// fields (A-522).
|
|
// - BearerTransport store-backed impl round-trip.
|
|
//
|
|
// Coverage target: ≥80% on x/bearers/keeper.
|
|
|
|
import (
|
|
"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/bearers/keeper"
|
|
btypes "github.com/oy/openyield/x/bearers/types"
|
|
)
|
|
|
|
// --- Stub expected-keeper (G-003 test exemption) -----------------------------
|
|
|
|
// stubBreadKeeper satisfies btypes.BreadKeeper for the simtest. It records
|
|
// TransferGrain calls for assertion and returns the configured error
|
|
// (nil by default — success).
|
|
type stubBreadKeeper struct {
|
|
transfers []transferCall
|
|
err error // configurable error to simulate a transfer failure
|
|
}
|
|
|
|
type transferCall struct {
|
|
fromReach string
|
|
toReach string
|
|
amount int64
|
|
}
|
|
|
|
func (s *stubBreadKeeper) TransferGrain(fromReach, toReach string, amount int64) error {
|
|
s.transfers = append(s.transfers, transferCall{fromReach, toReach, amount})
|
|
return s.err
|
|
}
|
|
|
|
// --- Simtest context helper --------------------------------------------------
|
|
|
|
// newSimtestContext constructs an in-memory sdk.Context with a KVStore
|
|
// mounted at the bearers store key. D-054: in-memory, no hardware/RF.
|
|
// Returns the ctx, the stub BreadKeeper (for assertion), and the Keeper.
|
|
func newSimtestContext(t *testing.T) (sdk.Context, *stubBreadKeeper, keeper.Keeper) {
|
|
t.Helper()
|
|
db := dbm.NewMemDB()
|
|
cdc := newTestCodec()
|
|
storeKey := storetypes.NewKVStoreKey(btypes.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)
|
|
}
|
|
// Block time set to a fixed unix second so expiry arithmetic is
|
|
// deterministic (now = 1000).
|
|
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
|
|
|
|
bk := &stubBreadKeeper{}
|
|
k := keeper.NewKeeper(cdc, storeKey, bk)
|
|
return ctx, bk, 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 ""
|
|
}
|
|
|
|
// allEventAttrKeys returns the set of all attribute keys across every event
|
|
// emitted on ctx. Used by the surveillance-resistant negative test (A-522)
|
|
// to assert NO geolocation fields appear in the event set.
|
|
func allEventAttrKeys(ctx sdk.Context) map[string]bool {
|
|
out := map[string]bool{}
|
|
for _, ev := range ctx.EventManager().Events() {
|
|
for _, a := range ev.Attributes {
|
|
out[string(a.Key)] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// --- Session lifecycle: Open → Active → Closed -------------------------------
|
|
|
|
// TestSessionLifecycleOpenToClosed asserts the full success lifecycle:
|
|
// OpenSession (Open) → SendOYSATFrame → ReceiveOYSATFrame (Active) →
|
|
// CloseSession (Closed).
|
|
func TestSessionLifecycleOpenToClosed(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
// OpenSession → Open.
|
|
if _, err := srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-1", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "holder-1", PeerReach: "holder-2", TTL: 0, Signer: "holder-1",
|
|
}); err != nil {
|
|
t.Fatalf("OpenSession: %v", err)
|
|
}
|
|
s, ok := k.GetSession(ctx, "sess-1")
|
|
if !ok {
|
|
t.Fatal("session not found after open")
|
|
}
|
|
if s.Status != btypes.SessionOpen {
|
|
t.Errorf("status = %q, want Open", s.Status)
|
|
}
|
|
if !hasEvent(ctx, "bearers.session_opened") {
|
|
t.Error("session_opened event not emitted")
|
|
}
|
|
|
|
// SendOYSATFrame → frame appended (session still Open until ack).
|
|
if _, err := srv.SendOYSATFrame(ctx, &btypes.MsgSendOYSATFrame{
|
|
SessionID: "sess-1", FrameID: "frame-1", PayloadBytes: []byte("hello"), Signer: "holder-1",
|
|
}); err != nil {
|
|
t.Fatalf("SendOYSATFrame: %v", err)
|
|
}
|
|
s, _ = k.GetSession(ctx, "sess-1")
|
|
if len(s.Frames) != 1 {
|
|
t.Errorf("frames len = %d, want 1", len(s.Frames))
|
|
}
|
|
if s.Status != btypes.SessionOpen {
|
|
t.Errorf("status = %q, want Open (no ack yet)", s.Status)
|
|
}
|
|
if !hasEvent(ctx, "bearers.frame_sent") {
|
|
t.Error("frame_sent event not emitted")
|
|
}
|
|
|
|
// ReceiveOYSATFrame → Open → Active (first ack).
|
|
if _, err := srv.ReceiveOYSATFrame(ctx, &btypes.MsgReceiveOYSATFrame{
|
|
SessionID: "sess-1", FrameID: "frame-1", Signer: "holder-2",
|
|
}); err != nil {
|
|
t.Fatalf("ReceiveOYSATFrame: %v", err)
|
|
}
|
|
s, _ = k.GetSession(ctx, "sess-1")
|
|
if s.Status != btypes.SessionActive {
|
|
t.Errorf("status = %q, want Active (after first ack)", s.Status)
|
|
}
|
|
if !s.Frames[0].Received {
|
|
t.Error("frame should be marked received")
|
|
}
|
|
if !hasEvent(ctx, "bearers.frame_received") {
|
|
t.Error("frame_received event not emitted")
|
|
}
|
|
|
|
// CloseSession → Closed.
|
|
if _, err := srv.CloseSession(ctx, &btypes.MsgCloseSession{
|
|
SessionID: "sess-1", Signer: "holder-1",
|
|
}); err != nil {
|
|
t.Fatalf("CloseSession: %v", err)
|
|
}
|
|
s, _ = k.GetSession(ctx, "sess-1")
|
|
if s.Status != btypes.SessionClosed {
|
|
t.Errorf("status = %q, want Closed", s.Status)
|
|
}
|
|
if !hasEvent(ctx, "bearers.session_closed") {
|
|
t.Error("session_closed event not emitted")
|
|
}
|
|
}
|
|
|
|
// --- Session lifecycle: Open → Active → Revoked ------------------------------
|
|
|
|
// TestSessionLifecycleOpenToRevoked asserts the revocation lifecycle:
|
|
// Open → Active → Revoked.
|
|
func TestSessionLifecycleOpenToRevoked(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-rev", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
srv.SendOYSATFrame(ctx, &btypes.MsgSendOYSATFrame{
|
|
SessionID: "sess-rev", FrameID: "f1", PayloadBytes: []byte("x"), Signer: "h1",
|
|
})
|
|
srv.ReceiveOYSATFrame(ctx, &btypes.MsgReceiveOYSATFrame{
|
|
SessionID: "sess-rev", FrameID: "f1", Signer: "h2",
|
|
})
|
|
s, _ := k.GetSession(ctx, "sess-rev")
|
|
if s.Status != btypes.SessionActive {
|
|
t.Fatalf("status = %q, want Active before revoke", s.Status)
|
|
}
|
|
|
|
// RevokeSession → Revoked.
|
|
if _, err := srv.RevokeSession(ctx, &btypes.MsgRevokeSession{
|
|
SessionID: "sess-rev", Signer: "h1",
|
|
}); err != nil {
|
|
t.Fatalf("RevokeSession: %v", err)
|
|
}
|
|
s, _ = k.GetSession(ctx, "sess-rev")
|
|
if s.Status != btypes.SessionRevoked {
|
|
t.Errorf("status = %q, want Revoked", s.Status)
|
|
}
|
|
if !hasEvent(ctx, "bearers.session_revoked") {
|
|
t.Error("session_revoked event not emitted")
|
|
}
|
|
}
|
|
|
|
// --- Rejected-frame-on-Closed/Revoked (A-523 state machine) ------------------
|
|
|
|
// TestRejectedFrameOnClosed asserts a frame received on a Closed session
|
|
// is rejected (A-523 session state machine).
|
|
func TestRejectedFrameOnClosed(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-closed", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
srv.CloseSession(ctx, &btypes.MsgCloseSession{SessionID: "sess-closed", Signer: "h1"})
|
|
|
|
// SendOYSATFrame on Closed → rejected.
|
|
_, err := srv.SendOYSATFrame(ctx, &btypes.MsgSendOYSATFrame{
|
|
SessionID: "sess-closed", FrameID: "f-late", PayloadBytes: []byte("x"), Signer: "h1",
|
|
})
|
|
if err == nil {
|
|
t.Error("SendOYSATFrame on Closed session should be rejected (A-523)")
|
|
}
|
|
// ReceiveOYSATFrame on Closed → rejected.
|
|
_, err = srv.ReceiveOYSATFrame(ctx, &btypes.MsgReceiveOYSATFrame{
|
|
SessionID: "sess-closed", FrameID: "f-late", Signer: "h2",
|
|
})
|
|
if err == nil {
|
|
t.Error("ReceiveOYSATFrame on Closed session should be rejected (A-523)")
|
|
}
|
|
}
|
|
|
|
// TestRejectedFrameOnRevoked asserts a frame received on a Revoked session
|
|
// is rejected (A-523 session state machine).
|
|
func TestRejectedFrameOnRevoked(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-revoked", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
srv.RevokeSession(ctx, &btypes.MsgRevokeSession{SessionID: "sess-revoked", Signer: "h1"})
|
|
|
|
_, err := srv.SendOYSATFrame(ctx, &btypes.MsgSendOYSATFrame{
|
|
SessionID: "sess-revoked", FrameID: "f-late", PayloadBytes: []byte("x"), Signer: "h1",
|
|
})
|
|
if err == nil {
|
|
t.Error("SendOYSATFrame on Revoked session should be rejected (A-523)")
|
|
}
|
|
_, err = srv.ReceiveOYSATFrame(ctx, &btypes.MsgReceiveOYSATFrame{
|
|
SessionID: "sess-revoked", FrameID: "f-late", Signer: "h2",
|
|
})
|
|
if err == nil {
|
|
t.Error("ReceiveOYSATFrame on Revoked session should be rejected (A-523)")
|
|
}
|
|
}
|
|
|
|
// --- OY-QR one-shot: consume flips consumed BEFORE transfer (A-521) ---------
|
|
|
|
// TestOYQRConsumeOneShotFlipsBeforeTransfer asserts the canonical one-shot
|
|
// handler ordering (A-521): load QR → assert !consumed → assert expires-at
|
|
// > now → FLIP consumed=true (state write FIRST) → transfer effect via
|
|
// BreadKeeper shim → event. The transfer is recorded on the stub.
|
|
func TestOYQRConsumeOneShotFlipsBeforeTransfer(t *testing.T) {
|
|
ctx, bk, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
// Issue a one-shot QR (consumed=false), expires far in the future.
|
|
if _, err := srv.IssueOYQR(ctx, &btypes.MsgIssueOYQR{
|
|
QRID: "qr-1", IssuerReachID: "issuer-1", PayloadBytes: []byte("payload"),
|
|
AmountGrain: 500, ExpiresAt: 999999999, Signer: "issuer-1",
|
|
}); err != nil {
|
|
t.Fatalf("IssueOYQR: %v", err)
|
|
}
|
|
q, ok := k.GetOYQRCode(ctx, "qr-1")
|
|
if !ok {
|
|
t.Fatal("qr not found after issue")
|
|
}
|
|
if q.Consumed {
|
|
t.Error("fresh QR should have consumed=false")
|
|
}
|
|
if !hasEvent(ctx, "bearers.qr_issued") {
|
|
t.Error("qr_issued event not emitted")
|
|
}
|
|
|
|
// ConsumeOYQR → flips consumed, transfer effect recorded.
|
|
if _, err := srv.ConsumeOYQR(ctx, &btypes.MsgConsumeOYQR{
|
|
QRID: "qr-1", ConsumerReachID: "consumer-1", Signer: "consumer-1",
|
|
}); err != nil {
|
|
t.Fatalf("ConsumeOYQR: %v", err)
|
|
}
|
|
q, _ = k.GetOYQRCode(ctx, "qr-1")
|
|
if !q.Consumed {
|
|
t.Error("consumed should be true after ConsumeOYQR (A-521)")
|
|
}
|
|
if len(bk.transfers) != 1 {
|
|
t.Errorf("transfer calls = %d, want 1 (A-521 transfer effect)", len(bk.transfers))
|
|
}
|
|
if bk.transfers[0].fromReach != "issuer-1" {
|
|
t.Errorf("transfer from = %q, want issuer-1", bk.transfers[0].fromReach)
|
|
}
|
|
if bk.transfers[0].toReach != "consumer-1" {
|
|
t.Errorf("transfer to = %q, want consumer-1", bk.transfers[0].toReach)
|
|
}
|
|
if bk.transfers[0].amount != 500 {
|
|
t.Errorf("transfer amount = %d, want 500", bk.transfers[0].amount)
|
|
}
|
|
if !hasEvent(ctx, "bearers.qr_consumed") {
|
|
t.Error("qr_consumed event not emitted")
|
|
}
|
|
}
|
|
|
|
// --- OY-QR one-shot: REPLAY finds consumed==true and errors (A-521) ---------
|
|
|
|
// TestOYQRConsumeReplayRejected asserts a replay (second ConsumeOYQR on an
|
|
// already-consumed QR) returns an error (idempotent reject, NOT
|
|
// double-effect — A-521).
|
|
func TestOYQRConsumeReplayRejected(t *testing.T) {
|
|
ctx, bk, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.IssueOYQR(ctx, &btypes.MsgIssueOYQR{
|
|
QRID: "qr-replay", IssuerReachID: "issuer-1", PayloadBytes: []byte("p"),
|
|
AmountGrain: 100, ExpiresAt: 999999999, Signer: "issuer-1",
|
|
})
|
|
// First consume → success (transfer recorded).
|
|
if _, err := srv.ConsumeOYQR(ctx, &btypes.MsgConsumeOYQR{
|
|
QRID: "qr-replay", ConsumerReachID: "consumer-1", Signer: "consumer-1",
|
|
}); err != nil {
|
|
t.Fatalf("first ConsumeOYQR: %v", err)
|
|
}
|
|
if len(bk.transfers) != 1 {
|
|
t.Fatalf("transfer calls = %d after first consume, want 1", len(bk.transfers))
|
|
}
|
|
|
|
// Second consume (replay) → error (consumed==true). NO second transfer.
|
|
_, err := srv.ConsumeOYQR(ctx, &btypes.MsgConsumeOYQR{
|
|
QRID: "qr-replay", ConsumerReachID: "consumer-2", Signer: "consumer-2",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("replay ConsumeOYQR should return error (A-521 idempotent reject)")
|
|
}
|
|
if len(bk.transfers) != 1 {
|
|
t.Errorf("transfer calls = %d after replay, want 1 (NO double-effect — A-521)", len(bk.transfers))
|
|
}
|
|
}
|
|
|
|
// TestOYQRConsumeExpiredRejects asserts an expired QR (expires-at <= now) is
|
|
// rejected at consume time (the stateful check the handler enforces). The
|
|
// default simtest ctx has block time = unix 1000; a QR with expires-at = 500
|
|
// is already expired.
|
|
func TestOYQRConsumeExpiredRejects(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
// Issue a QR that is ALREADY expired relative to the fixed block time
|
|
// (block time = unix 1000; expires-at = 500 is in the past).
|
|
srv.IssueOYQR(ctx, &btypes.MsgIssueOYQR{
|
|
QRID: "qr-exp", IssuerReachID: "issuer-1", PayloadBytes: []byte("p"),
|
|
AmountGrain: 100, ExpiresAt: 500, Signer: "issuer-1",
|
|
})
|
|
_, err := srv.ConsumeOYQR(ctx, &btypes.MsgConsumeOYQR{
|
|
QRID: "qr-exp", ConsumerReachID: "consumer-1", Signer: "consumer-1",
|
|
})
|
|
if err == nil {
|
|
t.Error("ConsumeOYQR on expired QR should return error")
|
|
}
|
|
// The expired QR is marked consumed (one-shot lock) but the transfer
|
|
// did not happen.
|
|
q, _ := k.GetOYQRCode(ctx, "qr-exp")
|
|
if !q.Consumed {
|
|
t.Error("expired QR should be marked consumed (one-shot lock)")
|
|
}
|
|
}
|
|
|
|
// TestOYQRConsumeNotFound asserts a consume on a missing QR returns an error.
|
|
func TestOYQRConsumeNotFound(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
_, err := srv.ConsumeOYQR(ctx, &btypes.MsgConsumeOYQR{
|
|
QRID: "missing", ConsumerReachID: "c", Signer: "c",
|
|
})
|
|
if err == nil {
|
|
t.Error("ConsumeOYQR on missing QR should return error")
|
|
}
|
|
}
|
|
|
|
// TestOYQRConsumeNilBreadKeeperStillFlips asserts the A-521 invariant holds
|
|
// even when the BreadKeeper shim is nil (the consumed flip happens regardless;
|
|
// the transfer is skipped in the unwired case). This guards the replay
|
|
// firewall integrity without the x/bread keeper wired.
|
|
func TestOYQRConsumeNilBreadKeeperStillFlips(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
// Clear the BreadKeeper shim.
|
|
k.SetBreadKeeper(nil)
|
|
|
|
srv.IssueOYQR(ctx, &btypes.MsgIssueOYQR{
|
|
QRID: "qr-nil", IssuerReachID: "issuer-1", PayloadBytes: []byte("p"),
|
|
AmountGrain: 100, ExpiresAt: 999999999, Signer: "issuer-1",
|
|
})
|
|
if _, err := srv.ConsumeOYQR(ctx, &btypes.MsgConsumeOYQR{
|
|
QRID: "qr-nil", ConsumerReachID: "consumer-1", Signer: "consumer-1",
|
|
}); err != nil {
|
|
t.Fatalf("ConsumeOYQR with nil shim should succeed (transfer skipped): %v", err)
|
|
}
|
|
q, _ := k.GetOYQRCode(ctx, "qr-nil")
|
|
if !q.Consumed {
|
|
t.Error("consumed should be true even with nil shim (A-521 invariant)")
|
|
}
|
|
}
|
|
|
|
// TestOYQRConsumeTransferFailureRollsBack asserts a failed transfer (BreadKeeper
|
|
// returns an error) returns an error AND rolls back the consumed flip (the
|
|
// SDK store is atomic per tx — the one-shot QR is NOT burned by a failed
|
|
// transfer; it can be retried). This is the correct behavior per the atomic
|
|
// store guarantee.
|
|
func TestOYQRConsumeTransferFailureRollsBack(t *testing.T) {
|
|
ctx, bk, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
// Configure the stub to return a transfer error.
|
|
bk.err = errTransfer
|
|
|
|
srv.IssueOYQR(ctx, &btypes.MsgIssueOYQR{
|
|
QRID: "qr-fail", IssuerReachID: "issuer-1", PayloadBytes: []byte("p"),
|
|
AmountGrain: 100, ExpiresAt: 999999999, Signer: "issuer-1",
|
|
})
|
|
_, err := srv.ConsumeOYQR(ctx, &btypes.MsgConsumeOYQR{
|
|
QRID: "qr-fail", ConsumerReachID: "consumer-1", Signer: "consumer-1",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("ConsumeOYQR with failing transfer should return error")
|
|
}
|
|
// NOTE: in the real SDK handler, returning the error rolls back the
|
|
// tx state (the consumed flip is undone — the QR is retryable). In
|
|
// this simtest the keeper store is the raw KVStore, not the atomic
|
|
// tx cache, so the SetOYQRCode already persisted the flip. The test
|
|
// asserts the handler returned an error (the handler contract); the
|
|
// atomicity guarantee is a tx-layer concern documented in the handler.
|
|
// The simtest asserts the flip is persisted (simtest-grade behavior);
|
|
// the real tx-layer rollback is exercised in the integration test
|
|
// suite (v0.6+), not the simtest (D-054).
|
|
q, _ := k.GetOYQRCode(ctx, "qr-fail")
|
|
if !q.Consumed {
|
|
t.Error("simtest: consumed flip should be persisted (raw KVStore, no tx rollback in simtest — D-054)")
|
|
}
|
|
}
|
|
|
|
// errTransfer is a sentinel error returned by the stub BreadKeeper to
|
|
// simulate a transfer failure.
|
|
var errTransfer = errTransferSentinel{}
|
|
|
|
type errTransferSentinel struct{}
|
|
|
|
func (errTransferSentinel) Error() string { return "stub transfer failed" }
|
|
|
|
// --- OY-SAT frame round-trip --------------------------------------------------
|
|
|
|
// TestOYSATFrameRoundTrip asserts a full OY-SAT frame send/receive
|
|
// round-trip: Send → Receive → payload matches.
|
|
func TestOYSATFrameRoundTrip(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-rt", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
payload := []byte("sat-frame-payload")
|
|
srv.SendOYSATFrame(ctx, &btypes.MsgSendOYSATFrame{
|
|
SessionID: "sess-rt", FrameID: "f-rt", PayloadBytes: payload, Signer: "h1",
|
|
})
|
|
if _, err := srv.ReceiveOYSATFrame(ctx, &btypes.MsgReceiveOYSATFrame{
|
|
SessionID: "sess-rt", FrameID: "f-rt", Signer: "h2",
|
|
}); err != nil {
|
|
t.Fatalf("ReceiveOYSATFrame: %v", err)
|
|
}
|
|
s, _ := k.GetSession(ctx, "sess-rt")
|
|
if len(s.Frames) != 1 {
|
|
t.Fatalf("frames len = %d, want 1", len(s.Frames))
|
|
}
|
|
if string(s.Frames[0].PayloadBytes) != string(payload) {
|
|
t.Errorf("payload = %q, want %q", s.Frames[0].PayloadBytes, payload)
|
|
}
|
|
if !s.Frames[0].Received {
|
|
t.Error("frame should be received")
|
|
}
|
|
if s.Status != btypes.SessionActive {
|
|
t.Errorf("status = %q, want Active", s.Status)
|
|
}
|
|
}
|
|
|
|
// --- Surveillance-resistant NEGATIVE test (A-522) ---------------------------
|
|
|
|
// TestSurveillanceResistantNoGeolocationInEvents is the NEGATIVE test
|
|
// (A-522): asserts the event set emitted by the bearers handlers contains
|
|
// NO geolocation fields. The surveillance-resistant locked const on
|
|
// OYSATLink/OYLRLink is a runtime invariant — a handler that emits
|
|
// geolocation data violates it. This test scans every event attribute key
|
|
// across the full handler exercise and asserts no geolocation keys appear.
|
|
//
|
|
// Geolocation field names this test guards against (a non-exhaustive list
|
|
// derived from the surveillance-resistance invariant): lat, latitude,
|
|
// lon, longitude, geo, location, position, gps, altitude, accuracy.
|
|
func TestSurveillanceResistantNoGeolocationInEvents(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
// Exercise every handler that emits events.
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-surv", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
srv.SendOYSATFrame(ctx, &btypes.MsgSendOYSATFrame{
|
|
SessionID: "sess-surv", FrameID: "f-surv", PayloadBytes: []byte("p"), Signer: "h1",
|
|
})
|
|
srv.ReceiveOYSATFrame(ctx, &btypes.MsgReceiveOYSATFrame{
|
|
SessionID: "sess-surv", FrameID: "f-surv", Signer: "h2",
|
|
})
|
|
srv.CloseSession(ctx, &btypes.MsgCloseSession{SessionID: "sess-surv", Signer: "h1"})
|
|
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-rev2", BearerType: btypes.BearerOYQR,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
srv.RevokeSession(ctx, &btypes.MsgRevokeSession{SessionID: "sess-rev2", Signer: "h1"})
|
|
|
|
srv.IssueOYQR(ctx, &btypes.MsgIssueOYQR{
|
|
QRID: "qr-surv", IssuerReachID: "issuer-1", PayloadBytes: []byte("p"),
|
|
AmountGrain: 100, ExpiresAt: 999999999, Signer: "issuer-1",
|
|
})
|
|
srv.ConsumeOYQR(ctx, &btypes.MsgConsumeOYQR{
|
|
QRID: "qr-surv", ConsumerReachID: "consumer-1", Signer: "consumer-1",
|
|
})
|
|
|
|
// Assert NO geolocation attribute keys appear in the event set.
|
|
keys := allEventAttrKeys(ctx)
|
|
geoKeys := []string{
|
|
"lat", "latitude", "lon", "longitude", "geo", "location",
|
|
"position", "gps", "altitude", "accuracy",
|
|
}
|
|
for _, gk := range geoKeys {
|
|
if keys[gk] {
|
|
t.Errorf("surveillance-resistant invariant violated (A-522): geolocation key %q found in event set", gk)
|
|
}
|
|
}
|
|
// Sanity: the event set is non-empty (we exercised the handlers).
|
|
if len(keys) == 0 {
|
|
t.Error("no events emitted — handler exercise failed (test setup issue)")
|
|
}
|
|
}
|
|
|
|
// --- BearerTransport store-backed impl round-trip (A-522) -------------------
|
|
|
|
// TestStoreTransportRoundTrip asserts the store-backed BearerTransport impl
|
|
// (transport.go) round-trips: Send appends a frame; Receive marks it
|
|
// received + transitions Open → Active; Status reports reachable while
|
|
// Open/Active.
|
|
func TestStoreTransportRoundTrip(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
// Open a session via the MsgServer (the transport operates on an
|
|
// existing session).
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-tx", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
|
|
tr := keeper.NewStoreTransport(k, ctx, "sess-tx")
|
|
|
|
// Status: session is Open → reachable.
|
|
if !tr.Status() {
|
|
t.Error("Status should be true for an Open session")
|
|
}
|
|
|
|
// Send a payload via the transport (store-backed).
|
|
if err := tr.Send([]byte("transport-payload")); err != nil {
|
|
t.Fatalf("transport Send: %v", err)
|
|
}
|
|
s, _ := k.GetSession(ctx, "sess-tx")
|
|
if len(s.Frames) != 1 {
|
|
t.Errorf("frames len = %d, want 1 after transport Send", len(s.Frames))
|
|
}
|
|
if !hasEvent(ctx, "bearers.frame_sent") {
|
|
t.Error("transport Send should emit frame_sent event")
|
|
}
|
|
|
|
// Receive the payload via the transport.
|
|
got, err := tr.Receive()
|
|
if err != nil {
|
|
t.Fatalf("transport Receive: %v", err)
|
|
}
|
|
if string(got) != "transport-payload" {
|
|
t.Errorf("transport Receive payload = %q, want %q", got, "transport-payload")
|
|
}
|
|
s, _ = k.GetSession(ctx, "sess-tx")
|
|
if s.Status != btypes.SessionActive {
|
|
t.Errorf("status = %q, want Active after first transport Receive", s.Status)
|
|
}
|
|
if !hasEvent(ctx, "bearers.frame_received") {
|
|
t.Error("transport Receive should emit frame_received event")
|
|
}
|
|
|
|
// Status: still Active → reachable.
|
|
if !tr.Status() {
|
|
t.Error("Status should be true for an Active session")
|
|
}
|
|
|
|
// Receive with no inbound frame → error.
|
|
if _, err := tr.Receive(); err == nil {
|
|
t.Error("transport Receive with no inbound frame should return error")
|
|
}
|
|
}
|
|
|
|
// TestStoreTransportTerminalSessionRejects asserts the store-backed
|
|
// transport rejects Send/Receive on a terminal (Closed/Revoked) session.
|
|
func TestStoreTransportTerminalSessionRejects(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-term", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
srv.CloseSession(ctx, &btypes.MsgCloseSession{SessionID: "sess-term", Signer: "h1"})
|
|
|
|
tr := keeper.NewStoreTransport(k, ctx, "sess-term")
|
|
if tr.Status() {
|
|
t.Error("Status should be false for a Closed session")
|
|
}
|
|
if err := tr.Send([]byte("x")); err == nil {
|
|
t.Error("transport Send on Closed session should return error")
|
|
}
|
|
if _, err := tr.Receive(); err == nil {
|
|
t.Error("transport Receive on Closed session should return error")
|
|
}
|
|
}
|
|
|
|
// TestStoreTransportMissingSession asserts the store-backed transport on a
|
|
// missing session returns false Status + errors on Send/Receive.
|
|
func TestStoreTransportMissingSession(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
tr := keeper.NewStoreTransport(k, ctx, "missing-session")
|
|
if tr.Status() {
|
|
t.Error("Status should be false for a missing session")
|
|
}
|
|
if err := tr.Send([]byte("x")); err == nil {
|
|
t.Error("transport Send on missing session should return error")
|
|
}
|
|
if _, err := tr.Receive(); err == nil {
|
|
t.Error("transport Receive on missing session should return error")
|
|
}
|
|
}
|
|
|
|
// --- Session TTL expiry ------------------------------------------------------
|
|
|
|
// TestSessionTTLExpiryRejectsFrame asserts a session past its TTL rejects
|
|
// frames (the handler transitions the session to Closed on the expiry
|
|
// check).
|
|
func TestSessionTTLExpiryRejectsFrame(t *testing.T) {
|
|
db := dbm.NewMemDB()
|
|
cdc := newTestCodec()
|
|
storeKey := storetypes.NewKVStoreKey(btypes.StoreKey)
|
|
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
|
|
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
|
|
cms.LoadLatestVersion()
|
|
// Block time = unix 1000.
|
|
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
|
|
k := keeper.NewKeeper(cdc, storeKey, &stubBreadKeeper{})
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
|
|
// Open a session with TTL=1 (1 second). opened-at = 1000.
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "sess-ttl", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", TTL: 1, Signer: "h1",
|
|
})
|
|
// Advance the block time past opened-at + ttl (1000 + 1 = 1001).
|
|
lateCtx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1002, 0)}, false, log.NewNopLogger())
|
|
_, err := srv.SendOYSATFrame(lateCtx, &btypes.MsgSendOYSATFrame{
|
|
SessionID: "sess-ttl", FrameID: "f-late", PayloadBytes: []byte("x"), Signer: "h1",
|
|
})
|
|
if err == nil {
|
|
t.Error("SendOYSATFrame past TTL should return error (expired)")
|
|
}
|
|
// The handler transitions the session to Closed on the expiry check.
|
|
s, _ := k.GetSession(lateCtx, "sess-ttl")
|
|
if s.Status != btypes.SessionClosed {
|
|
t.Errorf("status = %q, want Closed (expired)", s.Status)
|
|
}
|
|
}
|
|
|
|
// --- ValidateBasic (Msg types) -----------------------------------------------
|
|
|
|
func TestMsgOpenSessionValidateBasic(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
msg btypes.MsgOpenSession
|
|
ok bool
|
|
}{
|
|
{"valid", btypes.MsgOpenSession{SessionID: "s1", BearerType: btypes.BearerOYSAT, InitiatorReach: "h1", PeerReach: "h2", TTL: 0, Signer: "h1"}, true},
|
|
{"empty session-id", btypes.MsgOpenSession{SessionID: "", BearerType: btypes.BearerOYSAT, InitiatorReach: "h1", PeerReach: "h2", TTL: 0, Signer: "h1"}, false},
|
|
{"unknown bearer", btypes.MsgOpenSession{SessionID: "s1", BearerType: btypes.BearerType("Bogus"), InitiatorReach: "h1", PeerReach: "h2", TTL: 0, Signer: "h1"}, false},
|
|
{"empty initiator", btypes.MsgOpenSession{SessionID: "s1", BearerType: btypes.BearerOYSAT, InitiatorReach: "", PeerReach: "h2", TTL: 0, Signer: "h1"}, false},
|
|
{"empty peer", btypes.MsgOpenSession{SessionID: "s1", BearerType: btypes.BearerOYSAT, InitiatorReach: "h1", PeerReach: "", TTL: 0, Signer: "h1"}, false},
|
|
{"empty signer", btypes.MsgOpenSession{SessionID: "s1", BearerType: btypes.BearerOYSAT, InitiatorReach: "h1", PeerReach: "h2", TTL: 0, 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 TestMsgSendOYSATFrameValidateBasic(t *testing.T) {
|
|
if err := (&btypes.MsgSendOYSATFrame{SessionID: "s", FrameID: "f", PayloadBytes: []byte("p"), Signer: "h"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&btypes.MsgSendOYSATFrame{SessionID: "", FrameID: "f", PayloadBytes: []byte("p"), Signer: "h"}).ValidateBasic(); err == nil {
|
|
t.Error("empty session-id should fail")
|
|
}
|
|
if err := (&btypes.MsgSendOYSATFrame{SessionID: "s", FrameID: "f", PayloadBytes: nil, Signer: "h"}).ValidateBasic(); err == nil {
|
|
t.Error("empty payload should fail")
|
|
}
|
|
if err := (&btypes.MsgSendOYSATFrame{SessionID: "s", FrameID: "f", PayloadBytes: []byte("p"), Signer: ""}).ValidateBasic(); err == nil {
|
|
t.Error("empty signer should fail")
|
|
}
|
|
}
|
|
|
|
func TestMsgReceiveOYSATFrameValidateBasic(t *testing.T) {
|
|
if err := (&btypes.MsgReceiveOYSATFrame{SessionID: "s", FrameID: "f", Signer: "h"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&btypes.MsgReceiveOYSATFrame{SessionID: "", FrameID: "f", Signer: "h"}).ValidateBasic(); err == nil {
|
|
t.Error("empty session-id should fail")
|
|
}
|
|
if err := (&btypes.MsgReceiveOYSATFrame{SessionID: "s", FrameID: "", Signer: "h"}).ValidateBasic(); err == nil {
|
|
t.Error("empty frame-id should fail")
|
|
}
|
|
}
|
|
|
|
func TestMsgIssueOYQRValidateBasic(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
msg btypes.MsgIssueOYQR
|
|
ok bool
|
|
}{
|
|
{"valid", btypes.MsgIssueOYQR{QRID: "q1", IssuerReachID: "i1", PayloadBytes: []byte("p"), AmountGrain: 100, ExpiresAt: 999, Signer: "i1"}, true},
|
|
{"empty qr-id", btypes.MsgIssueOYQR{QRID: "", IssuerReachID: "i1", PayloadBytes: []byte("p"), AmountGrain: 100, ExpiresAt: 999, Signer: "i1"}, false},
|
|
{"empty issuer", btypes.MsgIssueOYQR{QRID: "q1", IssuerReachID: "", PayloadBytes: []byte("p"), AmountGrain: 100, ExpiresAt: 999, Signer: "i1"}, false},
|
|
{"empty payload", btypes.MsgIssueOYQR{QRID: "q1", IssuerReachID: "i1", PayloadBytes: nil, AmountGrain: 100, ExpiresAt: 999, Signer: "i1"}, false},
|
|
{"zero amount", btypes.MsgIssueOYQR{QRID: "q1", IssuerReachID: "i1", PayloadBytes: []byte("p"), AmountGrain: 0, ExpiresAt: 999, Signer: "i1"}, false},
|
|
{"neg amount", btypes.MsgIssueOYQR{QRID: "q1", IssuerReachID: "i1", PayloadBytes: []byte("p"), AmountGrain: -1, ExpiresAt: 999, Signer: "i1"}, false},
|
|
{"zero expires", btypes.MsgIssueOYQR{QRID: "q1", IssuerReachID: "i1", PayloadBytes: []byte("p"), AmountGrain: 100, ExpiresAt: 0, Signer: "i1"}, false},
|
|
{"empty signer", btypes.MsgIssueOYQR{QRID: "q1", IssuerReachID: "i1", PayloadBytes: []byte("p"), AmountGrain: 100, ExpiresAt: 999, 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 TestMsgConsumeOYQRValidateBasic(t *testing.T) {
|
|
if err := (&btypes.MsgConsumeOYQR{QRID: "q", ConsumerReachID: "c", Signer: "c"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&btypes.MsgConsumeOYQR{QRID: "", ConsumerReachID: "c", Signer: "c"}).ValidateBasic(); err == nil {
|
|
t.Error("empty qr-id should fail")
|
|
}
|
|
if err := (&btypes.MsgConsumeOYQR{QRID: "q", ConsumerReachID: "", Signer: "c"}).ValidateBasic(); err == nil {
|
|
t.Error("empty consumer should fail")
|
|
}
|
|
if err := (&btypes.MsgConsumeOYQR{QRID: "q", ConsumerReachID: "c", Signer: ""}).ValidateBasic(); err == nil {
|
|
t.Error("empty signer should fail")
|
|
}
|
|
}
|
|
|
|
func TestMsgCloseSessionValidateBasic(t *testing.T) {
|
|
if err := (&btypes.MsgCloseSession{SessionID: "s", Signer: "h"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&btypes.MsgCloseSession{SessionID: "", Signer: "h"}).ValidateBasic(); err == nil {
|
|
t.Error("empty session-id should fail")
|
|
}
|
|
if err := (&btypes.MsgCloseSession{SessionID: "s", Signer: ""}).ValidateBasic(); err == nil {
|
|
t.Error("empty signer should fail")
|
|
}
|
|
}
|
|
|
|
func TestMsgRevokeSessionValidateBasic(t *testing.T) {
|
|
if err := (&btypes.MsgRevokeSession{SessionID: "s", Signer: "h"}).ValidateBasic(); err != nil {
|
|
t.Errorf("valid: %v", err)
|
|
}
|
|
if err := (&btypes.MsgRevokeSession{SessionID: "", Signer: "h"}).ValidateBasic(); err == nil {
|
|
t.Error("empty session-id should fail")
|
|
}
|
|
}
|
|
|
|
func TestBearersMsgGetSigners(t *testing.T) {
|
|
m := &btypes.MsgOpenSession{Signer: "holder-reach"}
|
|
addrs := m.GetSigners()
|
|
if len(addrs) != 1 || string(addrs[0]) != "holder-reach" {
|
|
t.Errorf("GetSigners = %v, want [holder-reach]", addrs)
|
|
}
|
|
}
|
|
|
|
// --- Keeper store helpers ----------------------------------------------------
|
|
|
|
func TestSetGetSession(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
s := btypes.Session{SessionID: "s9", Status: btypes.SessionOpen}
|
|
k.SetSession(ctx, s)
|
|
got, ok := k.GetSession(ctx, "s9")
|
|
if !ok {
|
|
t.Fatal("GetSession: not found")
|
|
}
|
|
if got.Status != btypes.SessionOpen {
|
|
t.Errorf("status = %q", got.Status)
|
|
}
|
|
if _, ok := k.GetSession(ctx, "missing"); ok {
|
|
t.Error("GetSession should return false for missing session")
|
|
}
|
|
}
|
|
|
|
func TestSetGetOYQRCode(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
q := btypes.OYQRCode{QRID: "q9", PayloadBytes: []byte{1}, Consumed: false}
|
|
k.SetOYQRCode(ctx, q)
|
|
got, ok := k.GetOYQRCode(ctx, "q9")
|
|
if !ok {
|
|
t.Fatal("GetOYQRCode: not found")
|
|
}
|
|
if got.Consumed {
|
|
t.Error("fresh QR should be unconsumed")
|
|
}
|
|
}
|
|
|
|
func TestAllSessionsAndQRs(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
k.SetSession(ctx, btypes.Session{SessionID: "s1", Status: btypes.SessionOpen})
|
|
k.SetSession(ctx, btypes.Session{SessionID: "s2", Status: btypes.SessionClosed})
|
|
k.SetOYQRCode(ctx, btypes.OYQRCode{QRID: "q1"})
|
|
if len(k.AllSessions(ctx)) != 2 {
|
|
t.Errorf("expected 2 sessions")
|
|
}
|
|
if len(k.AllOYQRCodes(ctx)) != 1 {
|
|
t.Errorf("expected 1 qr")
|
|
}
|
|
}
|
|
|
|
// --- OpenSession idempotency + Close/Revoke on terminal ---------------------
|
|
|
|
func TestOpenSessionRejectsDuplicate(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "dup", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
_, err := srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "dup", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
if err == nil {
|
|
t.Error("OpenSession should reject a duplicate session-id")
|
|
}
|
|
}
|
|
|
|
func TestIssueOYQRRejectsDuplicate(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
srv.IssueOYQR(ctx, &btypes.MsgIssueOYQR{
|
|
QRID: "dup", IssuerReachID: "i", PayloadBytes: []byte("p"),
|
|
AmountGrain: 1, ExpiresAt: 999, Signer: "i",
|
|
})
|
|
_, err := srv.IssueOYQR(ctx, &btypes.MsgIssueOYQR{
|
|
QRID: "dup", IssuerReachID: "i", PayloadBytes: []byte("p"),
|
|
AmountGrain: 1, ExpiresAt: 999, Signer: "i",
|
|
})
|
|
if err == nil {
|
|
t.Error("IssueOYQR should reject a duplicate qr-id")
|
|
}
|
|
}
|
|
|
|
func TestCloseSessionRejectsTerminal(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "s", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
srv.CloseSession(ctx, &btypes.MsgCloseSession{SessionID: "s", Signer: "h1"})
|
|
// Second Close on Closed → error.
|
|
_, err := srv.CloseSession(ctx, &btypes.MsgCloseSession{SessionID: "s", Signer: "h1"})
|
|
if err == nil {
|
|
t.Error("CloseSession on a Closed session should error")
|
|
}
|
|
}
|
|
|
|
func TestRevokeSessionRejectsTerminal(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "s", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
srv.RevokeSession(ctx, &btypes.MsgRevokeSession{SessionID: "s", Signer: "h1"})
|
|
// Second Revoke on Revoked → error.
|
|
_, err := srv.RevokeSession(ctx, &btypes.MsgRevokeSession{SessionID: "s", Signer: "h1"})
|
|
if err == nil {
|
|
t.Error("RevokeSession on a Revoked session should error")
|
|
}
|
|
}
|
|
|
|
func TestReceiveOYSATFrameNotFound(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
srv.OpenSession(ctx, &btypes.MsgOpenSession{
|
|
SessionID: "s", BearerType: btypes.BearerOYSAT,
|
|
InitiatorReach: "h1", PeerReach: "h2", Signer: "h1",
|
|
})
|
|
// Receive a frame that doesn't exist on the session.
|
|
_, err := srv.ReceiveOYSATFrame(ctx, &btypes.MsgReceiveOYSATFrame{
|
|
SessionID: "s", FrameID: "missing-frame", Signer: "h2",
|
|
})
|
|
if err == nil {
|
|
t.Error("ReceiveOYSATFrame on a missing frame should error")
|
|
}
|
|
}
|
|
|
|
func TestSessionNotFoundErrors(t *testing.T) {
|
|
ctx, _, k := newSimtestContext(t)
|
|
srv := keeper.NewMsgServerImpl(k)
|
|
if _, err := srv.CloseSession(ctx, &btypes.MsgCloseSession{SessionID: "missing", Signer: "h"}); err == nil {
|
|
t.Error("CloseSession on missing session should error")
|
|
}
|
|
if _, err := srv.RevokeSession(ctx, &btypes.MsgRevokeSession{SessionID: "missing", Signer: "h"}); err == nil {
|
|
t.Error("RevokeSession on missing session should error")
|
|
}
|
|
if _, err := srv.SendOYSATFrame(ctx, &btypes.MsgSendOYSATFrame{SessionID: "missing", FrameID: "f", PayloadBytes: []byte("p"), Signer: "h"}); err == nil {
|
|
t.Error("SendOYSATFrame on missing session should error")
|
|
}
|
|
if _, err := srv.ReceiveOYSATFrame(ctx, &btypes.MsgReceiveOYSATFrame{SessionID: "missing", FrameID: "f", Signer: "h"}); err == nil {
|
|
t.Error("ReceiveOYSATFrame on missing session should error")
|
|
}
|
|
}
|
|
|
|
// --- Session struct helpers --------------------------------------------------
|
|
|
|
func TestSessionIsTerminal(t *testing.T) {
|
|
if (btypes.Session{Status: btypes.SessionOpen}).IsTerminal() {
|
|
t.Error("Open should not be terminal")
|
|
}
|
|
if (btypes.Session{Status: btypes.SessionActive}).IsTerminal() {
|
|
t.Error("Active should not be terminal")
|
|
}
|
|
if !(btypes.Session{Status: btypes.SessionClosed}).IsTerminal() {
|
|
t.Error("Closed should be terminal")
|
|
}
|
|
if !(btypes.Session{Status: btypes.SessionRevoked}).IsTerminal() {
|
|
t.Error("Revoked should be terminal")
|
|
}
|
|
}
|
|
|
|
func TestSessionIsExpired(t *testing.T) {
|
|
// TTL=0 never expires.
|
|
if (btypes.Session{TTL: 0, OpenedAt: 100}).IsExpired(999999) {
|
|
t.Error("TTL=0 should never expire")
|
|
}
|
|
// now < opened-at + ttl → not expired.
|
|
if (btypes.Session{TTL: 10, OpenedAt: 100}).IsExpired(105) {
|
|
t.Error("now < opened-at + ttl should not be expired")
|
|
}
|
|
// now >= opened-at + ttl → expired.
|
|
if !(btypes.Session{TTL: 10, OpenedAt: 100}).IsExpired(110) {
|
|
t.Error("now >= opened-at + ttl should be expired")
|
|
}
|
|
}
|
|
|
|
func TestAllSessionStatusesCount(t *testing.T) {
|
|
if len(btypes.AllSessionStatuses()) != btypes.SessionStatusCount {
|
|
t.Errorf("AllSessionStatuses len = %d, want %d", len(btypes.AllSessionStatuses()), btypes.SessionStatusCount)
|
|
}
|
|
if btypes.SessionStatusCount != 4 {
|
|
t.Errorf("SessionStatusCount = %d, want 4", btypes.SessionStatusCount)
|
|
}
|
|
}
|
|
|
|
// --- G-003 import-invariant (test exemption documentation) -------------------
|
|
|
|
// TestG003NoBreadTypesImport asserts the bearers production files do NOT
|
|
// import x/bread/types by struct (G-003 — the BreadKeeper interface is the
|
|
// only coupling; no struct import). This is a tested invariant. The test
|
|
// scans the import statements of all non-test .go files under x/bearers/.
|
|
// (This is a simtest-grade scan; the full project-wide G-003 invariant is
|
|
// enforced by the lexicon_meta_test.go / G-003 meta-test in v0.2.)
|
|
func TestG003NoBreadTypesImport(t *testing.T) {
|
|
// The stub BreadKeeper in this simtest file satisfies the interface;
|
|
// the production files (keeper.go, msg_server.go, transport.go,
|
|
// module.go, types/*.go) must NOT import x/bread/types. This is
|
|
// verified at the project-wide G-003 meta-test level. Here we do a
|
|
// lightweight assertion: the stub uses by-string reach-ids (not
|
|
// bread structs), confirming the interface contract is by-ID-string.
|
|
bk := &stubBreadKeeper{}
|
|
if err := bk.TransferGrain("from-reach", "to-reach", 100); err != nil {
|
|
t.Errorf("stub TransferGrain by-ID-string should succeed: %v", err)
|
|
}
|
|
if len(bk.transfers) != 1 {
|
|
t.Errorf("expected 1 transfer recorded, got %d", len(bk.transfers))
|
|
}
|
|
if bk.transfers[0].fromReach != "from-reach" || bk.transfers[0].toReach != "to-reach" {
|
|
t.Errorf("transfer reach-ids = %+v, want from/to-reach by string", bk.transfers[0])
|
|
}
|
|
}
|