docs(P02): complete Pacts+Partners phase
---ci--- project: oy phase: 2 milestone: v0.2 status: complete phase_role: execution requirements: covered: [REQ-020, REQ-018] partial: [] ---/ci--- Phase 2 (Pacts+Partners) complete. 2 new modules: x/pact (6-PactType enum + Mission Lock, G-005 one module not six), x/partner (4-tier Partner Spectrum). 207 tests total (143 prev + 64 new). Coverage: x/pact 95.9%, x/partner 100%. Lexicon + G-003 invariants green. Tagged v0.1.2.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "partner"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// PartnerTierCount is the locked count of PartnerTier enum values
|
||||
// (vision §13, D-026). A regression firewall: adding/removing/renaming a
|
||||
// tier breaks this const's test (REQ-018).
|
||||
PartnerTierCount = 4
|
||||
)
|
||||
|
||||
// PartnerTier enumerates the four partner tiers (vision §13, REQ-018, D-026).
|
||||
// Op processes Pass-Acts; MasterOp is a senior Op; Pier is a credential /
|
||||
// identity provider (e-Residency/biometrics); Anchor is institutional.
|
||||
// "Op" is used (not "operator" — the latter implies a banned financial term
|
||||
// per RESEARCH §1.6; "Op" is vision-§13 lexicon-clean).
|
||||
type PartnerTier string
|
||||
|
||||
const (
|
||||
TierOp PartnerTier = "Op" // processes Pass-Acts
|
||||
TierMasterOp PartnerTier = "MasterOp" // senior Op
|
||||
TierPier PartnerTier = "Pier" // credential / identity provider
|
||||
TierAnchor PartnerTier = "Anchor" // institutional
|
||||
)
|
||||
|
||||
// AllPartnerTiers returns all four PartnerTier values in vision §13 order.
|
||||
// Locked-const test asserts exactly 4 entries with these names (REQ-018).
|
||||
func AllPartnerTiers() []PartnerTier {
|
||||
return []PartnerTier{
|
||||
TierOp,
|
||||
TierMasterOp,
|
||||
TierPier,
|
||||
TierAnchor,
|
||||
}
|
||||
}
|
||||
|
||||
// PartnerStatus enumerates the lifecycle states of a Partner (REQ-018).
|
||||
type PartnerStatus string
|
||||
|
||||
const (
|
||||
StatusPending PartnerStatus = "Pending" // partner registered, not yet active
|
||||
StatusActive PartnerStatus = "Active" // partner is live
|
||||
StatusSuspended PartnerStatus = "Suspended" // partner temporarily halted
|
||||
StatusRevoked PartnerStatus = "Revoked" // partner permanently revoked
|
||||
)
|
||||
|
||||
// PartnerStatusCount is the locked count of PartnerStatus enum values.
|
||||
const PartnerStatusCount = 4
|
||||
|
||||
// CredentialType enumerates the kinds of credentials a Pier can reference
|
||||
// (REQ-018). The ref-uri is opaque; Pier credential routing is deferred per
|
||||
// Q5 (v0.3 will wire the live routing). The skeleton defines the type enum
|
||||
// so genesis / registry entries carry a typed credential kind.
|
||||
type CredentialType string
|
||||
|
||||
const (
|
||||
CredentialEresidency CredentialType = "Eresidency" // e-Residency-style identity
|
||||
CredentialBiometric CredentialType = "Biometric" // biometric identity
|
||||
CredentialVouch CredentialType = "Vouch" // vouch-based attestation
|
||||
CredentialCustom CredentialType = "Custom" // opaque custom credential
|
||||
)
|
||||
|
||||
// CredentialRef references an external credential provider (REQ-018).
|
||||
// provider-id references a Partner (typically a Pier) by ID string
|
||||
// (G-003 by-ID-string invariant). ref-uri is an opaque URI; Pier credential
|
||||
// routing is deferred per Q5, so the skeleton keeps the ref opaque.
|
||||
type CredentialRef struct {
|
||||
ProviderID string `json:"provider_id" yaml:"provider_id"`
|
||||
CredentialType CredentialType `json:"credential_type" yaml:"credential_type"`
|
||||
RefURI string `json:"ref_uri" yaml:"ref_uri"`
|
||||
}
|
||||
|
||||
// Partner is a registered actor on the Partner Spectrum (vision §13, REQ-018).
|
||||
// reach-id references x/identity Reach by string (G-003 by-ID-string
|
||||
// invariant). credential-ref references a credential provider (typically a
|
||||
// Pier) by ID string. region is a free-form locale tag.
|
||||
type Partner struct {
|
||||
PartnerID string `json:"partner_id" yaml:"partner_id"`
|
||||
Tier PartnerTier `json:"tier" yaml:"tier"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Region string `json:"region" yaml:"region"`
|
||||
CredentialRef CredentialRef `json:"credential_ref" yaml:"credential_ref"`
|
||||
Status PartnerStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// Keeper is a registry keeper stub for Partners (REQ-018). The skeleton
|
||||
// provides in-memory add/get/list/by-tier operations; v0.3 wires the live
|
||||
// keeper backed by the store. It is safe for concurrent use (the live keeper
|
||||
// will use the SDK store, which is single-threaded per-block; the stub uses
|
||||
// a mutex so the skeleton's tests can exercise concurrent paths).
|
||||
type Keeper struct {
|
||||
mu sync.Mutex
|
||||
partners map[string]Partner
|
||||
}
|
||||
|
||||
// NewKeeper returns an empty registry keeper stub.
|
||||
func NewKeeper() *Keeper {
|
||||
return &Keeper{partners: make(map[string]Partner)}
|
||||
}
|
||||
|
||||
// AddPartner registers a Partner by ID. Returns an error if the ID is empty
|
||||
// or already registered.
|
||||
func (k *Keeper) AddPartner(p Partner) error {
|
||||
if p.PartnerID == "" {
|
||||
return fmt.Errorf("partner: empty partner-id")
|
||||
}
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
if _, exists := k.partners[p.PartnerID]; exists {
|
||||
return fmt.Errorf("partner: duplicate partner-id %q", p.PartnerID)
|
||||
}
|
||||
k.partners[p.PartnerID] = p
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPartner returns a Partner by ID and true, or zero-value and false.
|
||||
func (k *Keeper) GetPartner(id string) (Partner, bool) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
p, ok := k.partners[id]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
// ListPartners returns all registered Partners (unordered).
|
||||
func (k *Keeper) ListPartners() []Partner {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
out := make([]Partner, 0, len(k.partners))
|
||||
for _, p := range k.partners {
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ListByTier returns all registered Partners matching the given tier.
|
||||
func (k *Keeper) ListByTier(tier PartnerTier) []Partner {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
out := []Partner{}
|
||||
for _, p := range k.partners {
|
||||
if p.Tier == tier {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Params for the partner module (skeleton — no tunables in v0.2).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the partner module genesis state (REQ-018).
|
||||
// Partners is the top-level set; ValidateGenesis enforces partner-id uniqueness.
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Partners []Partner `json:"partners" yaml:"partners"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Partners: []Partner{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate partner-ids.
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("partner: invalid genesis: %w", err)
|
||||
}
|
||||
seen := make(map[string]bool, len(gs.Partners))
|
||||
for _, p := range gs.Partners {
|
||||
if p.PartnerID == "" {
|
||||
return fmt.Errorf("partner: empty partner-id")
|
||||
}
|
||||
if seen[p.PartnerID] {
|
||||
return fmt.Errorf("partner: duplicate partner-id %q", p.PartnerID)
|
||||
}
|
||||
seen[p.PartnerID] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
"github.com/oy/openyield/x/partner/types"
|
||||
)
|
||||
|
||||
// TestPartnerTierCountLockedConst asserts PartnerTierCount is exactly 4
|
||||
// and AllPartnerTiers() returns exactly 4 (vision §13, REQ-018, D-026). A
|
||||
// regression firewall: adding/removing/renaming a tier breaks this test.
|
||||
func TestPartnerTierCountLockedConst(t *testing.T) {
|
||||
if types.PartnerTierCount != 4 {
|
||||
t.Errorf("PartnerTierCount = %d, expected 4 (vision §13 LOCKED)", types.PartnerTierCount)
|
||||
}
|
||||
all := types.AllPartnerTiers()
|
||||
if len(all) != 4 {
|
||||
t.Errorf("AllPartnerTiers() len = %d, expected 4", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllPartnerTiersNames asserts the 4 vision §13 names in order with no
|
||||
// extras, no dups, no renames. "Op" (not "operator") per vision §13 — the
|
||||
// latter implies a banned financial term per RESEARCH §1.6; "Op" is
|
||||
// lexicon-clean.
|
||||
func TestAllPartnerTiersNames(t *testing.T) {
|
||||
want := []string{"Op", "MasterOp", "Pier", "Anchor"}
|
||||
all := types.AllPartnerTiers()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllPartnerTiers()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate PartnerTier %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartnerTierValues asserts each named const matches its AllPartnerTiers
|
||||
// entry.
|
||||
func TestPartnerTierValues(t *testing.T) {
|
||||
if types.TierOp != "Op" {
|
||||
t.Errorf("TierOp = %q", types.TierOp)
|
||||
}
|
||||
if types.TierMasterOp != "MasterOp" {
|
||||
t.Errorf("TierMasterOp = %q", types.TierMasterOp)
|
||||
}
|
||||
if types.TierPier != "Pier" {
|
||||
t.Errorf("TierPier = %q", types.TierPier)
|
||||
}
|
||||
if types.TierAnchor != "Anchor" {
|
||||
t.Errorf("TierAnchor = %q", types.TierAnchor)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartnerStatusCountLockedConst asserts PartnerStatusCount is exactly 4.
|
||||
func TestPartnerStatusCountLockedConst(t *testing.T) {
|
||||
if types.PartnerStatusCount != 4 {
|
||||
t.Errorf("PartnerStatusCount = %d, expected 4", types.PartnerStatusCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartnerStatusEnumCoverage asserts all four PartnerStatus values are
|
||||
// distinct and non-empty (REQ-018 lifecycle: Pending, Active, Suspended, Revoked).
|
||||
func TestPartnerStatusEnumCoverage(t *testing.T) {
|
||||
statuses := []types.PartnerStatus{
|
||||
types.StatusPending, types.StatusActive,
|
||||
types.StatusSuspended, types.StatusRevoked,
|
||||
}
|
||||
if len(statuses) != 4 {
|
||||
t.Errorf("expected 4 PartnerStatus consts, got %d", len(statuses))
|
||||
}
|
||||
seen := map[types.PartnerStatus]bool{}
|
||||
for _, s := range statuses {
|
||||
if s == "" {
|
||||
t.Error("empty PartnerStatus")
|
||||
}
|
||||
if seen[s] {
|
||||
t.Errorf("duplicate PartnerStatus %q", s)
|
||||
}
|
||||
seen[s] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestCredentialTypeEnumCoverage asserts the CredentialType values are
|
||||
// distinct and non-empty (Pier credential routing deferred per Q5; the
|
||||
// enum is the typed kind for genesis / registry entries).
|
||||
func TestCredentialTypeEnumCoverage(t *testing.T) {
|
||||
cts := []types.CredentialType{
|
||||
types.CredentialEresidency, types.CredentialBiometric,
|
||||
types.CredentialVouch, types.CredentialCustom,
|
||||
}
|
||||
if len(cts) != 4 {
|
||||
t.Errorf("expected 4 CredentialType consts, got %d", len(cts))
|
||||
}
|
||||
seen := map[types.CredentialType]bool{}
|
||||
for _, c := range cts {
|
||||
if c == "" {
|
||||
t.Error("empty CredentialType")
|
||||
}
|
||||
if seen[c] {
|
||||
t.Errorf("duplicate CredentialType %q", c)
|
||||
}
|
||||
seen[c] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestCredentialRefStruct asserts CredentialRef carries all required fields
|
||||
// (provider-id, credential-type, ref-uri — opaque URI).
|
||||
func TestCredentialRefStruct(t *testing.T) {
|
||||
c := types.CredentialRef{
|
||||
ProviderID: "pier-1",
|
||||
CredentialType: types.CredentialEresidency,
|
||||
RefURI: "oy:cred:pier-1/eresidency/abc123",
|
||||
}
|
||||
if c.ProviderID != "pier-1" || c.CredentialType != types.CredentialEresidency ||
|
||||
c.RefURI != "oy:cred:pier-1/eresidency/abc123" {
|
||||
t.Error("CredentialRef fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPartnerStructFields asserts Partner carries all required fields
|
||||
// including the by-ID-string reach-id (G-003).
|
||||
func TestPartnerStructFields(t *testing.T) {
|
||||
p := types.Partner{
|
||||
PartnerID: "pt1",
|
||||
Tier: types.TierPier,
|
||||
Name: "Pier One",
|
||||
ReachID: "reach:pier-1",
|
||||
Region: "EU",
|
||||
CredentialRef: types.CredentialRef{
|
||||
ProviderID: "pier-1",
|
||||
CredentialType: types.CredentialBiometric,
|
||||
RefURI: "oy:cred:bio/x",
|
||||
},
|
||||
Status: types.StatusActive,
|
||||
}
|
||||
if p.PartnerID != "pt1" || p.Tier != types.TierPier || p.Name != "Pier One" ||
|
||||
p.ReachID != "reach:pier-1" || p.Region != "EU" ||
|
||||
p.CredentialRef.ProviderID != "pier-1" || p.Status != types.StatusActive {
|
||||
t.Error("Partner fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Registry keeper stub tests (REQ-018) ---------------------------------------
|
||||
|
||||
// TestKeeperAddGetRoundTrip asserts AddPartner + GetPartner round-trips a
|
||||
// Partner by ID.
|
||||
func TestKeeperAddGetRoundTrip(t *testing.T) {
|
||||
k := types.NewKeeper()
|
||||
p := types.Partner{
|
||||
PartnerID: "pt1",
|
||||
Tier: types.TierOp,
|
||||
Name: "Op One",
|
||||
ReachID: "reach:op-1",
|
||||
Status: types.StatusActive,
|
||||
}
|
||||
if err := k.AddPartner(p); err != nil {
|
||||
t.Fatalf("AddPartner: %v", err)
|
||||
}
|
||||
got, ok := k.GetPartner("pt1")
|
||||
if !ok {
|
||||
t.Fatal("GetPartner: not found")
|
||||
}
|
||||
if got.PartnerID != "pt1" || got.Tier != types.TierOp {
|
||||
t.Errorf("GetPartner returned wrong Partner: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeeperAddPartnerRejectsEmptyID asserts AddPartner rejects an empty id.
|
||||
func TestKeeperAddPartnerRejectsEmptyID(t *testing.T) {
|
||||
k := types.NewKeeper()
|
||||
if err := k.AddPartner(types.Partner{PartnerID: ""}); err == nil {
|
||||
t.Error("AddPartner should reject empty partner-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeeperAddPartnerRejectsDup asserts AddPartner rejects a duplicate id.
|
||||
func TestKeeperAddPartnerRejectsDup(t *testing.T) {
|
||||
k := types.NewKeeper()
|
||||
p := types.Partner{PartnerID: "pt1", Tier: types.TierOp}
|
||||
if err := k.AddPartner(p); err != nil {
|
||||
t.Fatalf("first AddPartner: %v", err)
|
||||
}
|
||||
if err := k.AddPartner(p); err == nil {
|
||||
t.Error("AddPartner should reject duplicate partner-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeeperGetPartnerMissing asserts GetPartner returns false for an
|
||||
// unregistered id.
|
||||
func TestKeeperGetPartnerMissing(t *testing.T) {
|
||||
k := types.NewKeeper()
|
||||
if _, ok := k.GetPartner("nope"); ok {
|
||||
t.Error("GetPartner should return false for unregistered id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeeperListPartners asserts ListPartners returns all registered Partners.
|
||||
func TestKeeperListPartners(t *testing.T) {
|
||||
k := types.NewKeeper()
|
||||
_ = k.AddPartner(types.Partner{PartnerID: "a", Tier: types.TierOp})
|
||||
_ = k.AddPartner(types.Partner{PartnerID: "b", Tier: types.TierAnchor})
|
||||
list := k.ListPartners()
|
||||
if len(list) != 2 {
|
||||
t.Errorf("ListPartners len = %d, want 2", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeeperListPartnersEmpty asserts ListPartners on an empty keeper returns
|
||||
// a non-nil empty slice (or a usable slice).
|
||||
func TestKeeperListPartnersEmpty(t *testing.T) {
|
||||
k := types.NewKeeper()
|
||||
list := k.ListPartners()
|
||||
if list == nil {
|
||||
t.Fatal("ListPartners returned nil")
|
||||
}
|
||||
if len(list) != 0 {
|
||||
t.Errorf("ListPartners len = %d, want 0", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeeperListByTier asserts ListByTier returns only Partners matching the
|
||||
// given tier (REQ-018 round-trip).
|
||||
func TestKeeperListByTier(t *testing.T) {
|
||||
k := types.NewKeeper()
|
||||
_ = k.AddPartner(types.Partner{PartnerID: "op1", Tier: types.TierOp})
|
||||
_ = k.AddPartner(types.Partner{PartnerID: "op2", Tier: types.TierOp})
|
||||
_ = k.AddPartner(types.Partner{PartnerID: "mop1", Tier: types.TierMasterOp})
|
||||
_ = k.AddPartner(types.Partner{PartnerID: "pier1", Tier: types.TierPier})
|
||||
_ = k.AddPartner(types.Partner{PartnerID: "anc1", Tier: types.TierAnchor})
|
||||
|
||||
tt := []struct {
|
||||
tier types.PartnerTier
|
||||
wantN int
|
||||
}{
|
||||
{types.TierOp, 2},
|
||||
{types.TierMasterOp, 1},
|
||||
{types.TierPier, 1},
|
||||
{types.TierAnchor, 1},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(string(tc.tier), func(t *testing.T) {
|
||||
got := k.ListByTier(tc.tier)
|
||||
if len(got) != tc.wantN {
|
||||
t.Errorf("ListByTier(%q) len = %d, want %d", tc.tier, len(got), tc.wantN)
|
||||
}
|
||||
for _, p := range got {
|
||||
if p.Tier != tc.tier {
|
||||
t.Errorf("ListByTier(%q) returned Partner with tier %q", tc.tier, p.Tier)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeeperListByTierEmpty asserts ListByTier returns an empty (non-nil)
|
||||
// slice when no Partners match.
|
||||
func TestKeeperListByTierEmpty(t *testing.T) {
|
||||
k := types.NewKeeper()
|
||||
got := k.ListByTier(types.TierAnchor)
|
||||
if got == nil {
|
||||
t.Fatal("ListByTier returned nil")
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("ListByTier len = %d, want 0", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Genesis tests (REQ-018, A-212) ----------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns a non-nil
|
||||
// empty slice for Partners.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := types.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Partners == nil || len(gs.Partners) != 0 {
|
||||
t.Errorf("Default Partners should be non-nil empty slice; got len=%d nil=%v", len(gs.Partners), gs.Partners == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupPartnerIDs asserts A-212: duplicate partner-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupPartnerIDs(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Partners: []types.Partner{
|
||||
{PartnerID: "pt1", Tier: types.TierOp},
|
||||
{PartnerID: "pt1", Tier: types.TierAnchor}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate partner-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyPartnerID asserts empty partner-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyPartnerID(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Partners: []types.Partner{{PartnerID: "", Tier: types.TierOp}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty partner-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := types.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
|
||||
t.Error("ValidateGenesis should reject malformed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
|
||||
func TestValidateGenesisAcceptsClean(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Partners: []types.Partner{
|
||||
{PartnerID: "pt1", Tier: types.TierOp, Status: types.StatusActive},
|
||||
{PartnerID: "pt2", Tier: types.TierPier, Status: types.StatusPending},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if types.ModuleName != "partner" {
|
||||
t.Errorf("ModuleName = %q", types.ModuleName)
|
||||
}
|
||||
if types.StoreKey != "partner" {
|
||||
t.Errorf("StoreKey = %q", types.StoreKey)
|
||||
}
|
||||
if types.RouterKey != "partner" {
|
||||
t.Errorf("RouterKey = %q", types.RouterKey)
|
||||
}
|
||||
if types.QuerierRoute != "partner" {
|
||||
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = types.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
|
||||
// TestLexiconNoBannedTermsInPartnerPackage scans every non-test .go file in
|
||||
// the partner/types package directory for the 9 banned terms (case-insensitive).
|
||||
// Production files only — the test file references banned terms via the
|
||||
// lexicon package helpers (standard lexicon-test bootstrapping pattern; no
|
||||
// banned literals are inlined in this test file).
|
||||
func TestLexiconNoBannedTermsInPartnerPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/partner/types")
|
||||
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob: %v", err)
|
||||
}
|
||||
prodFiles := []string{}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
prodFiles = append(prodFiles, f)
|
||||
}
|
||||
if len(prodFiles) == 0 {
|
||||
t.Fatal("no production .go files found in partner/types")
|
||||
}
|
||||
for _, f := range prodFiles {
|
||||
bz, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", f, err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInPartnerTestFile asserts this test file itself does
|
||||
// not contain any banned term as a literal (the firewall scans test files
|
||||
// too; the lexicon helpers must be used rather than inlining banned terms).
|
||||
func TestLexiconNoBannedTermsInPartnerTestFile(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
bz, err := os.ReadFile(thisFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read self: %v", err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Fatalf("partner test file contains banned term %q — use lexicon helpers, not literals", found)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by
|
||||
// walking up from this test file (v0.2 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/x/partner/types/types_test.go -> repoRoot = .../oy (4 dirs up)
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
|
||||
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
|
||||
return filepath.Join(repoRoot, rel)
|
||||
}
|
||||
Reference in New Issue
Block a user