5a12ab0e76
web/store instantiates real x/identity/types.Reach + x/stash/types.Stash (app-layer consumption per D-070, NOT a cross-x/ import). CreateReach atomically creates Reach (IsNomad=true) + Stash (D-071). G-027 validates HolderID/PublicKey (non-empty, <=128, no path separators, no template syntax). import_test.go enforces G-025: web/ imports only x/*/types, never x/*/keeper or x/<module> (module.go). Coverage 100%. ---ci--- project: oy phase: 1 milestone: v0.6 status: execute ---/ci---
217 lines
5.7 KiB
Go
217 lines
5.7 KiB
Go
package store
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
|
|
identitytypes "github.com/oy/openyield/x/identity/types"
|
|
stashtypes "github.com/oy/openyield/x/stash/types"
|
|
)
|
|
|
|
func TestNewStoreSeedsFixtures(t *testing.T) {
|
|
s := NewStore()
|
|
reaches := s.ListReaches()
|
|
if len(reaches) < 2 {
|
|
t.Fatalf("NewStore seeded %d reaches, want >=2", len(reaches))
|
|
}
|
|
// Both seeded reaches must be Nomads (IsNomad=true).
|
|
for _, r := range reaches {
|
|
if !r.IsNomad {
|
|
t.Errorf("seeded reach %q: IsNomad=false, want true", r.HolderID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateReachAtomicReachAndStash(t *testing.T) {
|
|
s := NewStore()
|
|
reach, stash, err := s.CreateReach("holder-test1", "pk-test1")
|
|
if err != nil {
|
|
t.Fatalf("CreateReach: %v", err)
|
|
}
|
|
// D-071: Reach must be IsNomad=true.
|
|
if !reach.IsNomad {
|
|
t.Errorf("reach.IsNomad = false, want true (D-071)")
|
|
}
|
|
if reach.HolderID != "holder-test1" {
|
|
t.Errorf("reach.HolderID = %q, want holder-test1", reach.HolderID)
|
|
}
|
|
// D-071: Stash must have matching HolderID + seeded BalanceGrain.
|
|
if stash.HolderID != reach.HolderID {
|
|
t.Errorf("stash.HolderID = %q, want %q (D-071 atomic)", stash.HolderID, reach.HolderID)
|
|
}
|
|
if stash.BalanceGrain != seedBalanceGrain {
|
|
t.Errorf("stash.BalanceGrain = %d, want %d", stash.BalanceGrain, seedBalanceGrain)
|
|
}
|
|
// Both must be retrievable after the atomic call.
|
|
if _, ok := s.GetReach("holder-test1"); !ok {
|
|
t.Errorf("GetReach miss after CreateReach (atomicity broken)")
|
|
}
|
|
if _, ok := s.GetStash("holder-test1"); !ok {
|
|
t.Errorf("GetStash miss after CreateReach (atomicity broken)")
|
|
}
|
|
if _, ok := s.GetStashActivity(stash.StashID); !ok {
|
|
t.Errorf("GetStashActivity miss after CreateReach (atomicity broken)")
|
|
}
|
|
}
|
|
|
|
func TestCreateReachDuplicateRejected(t *testing.T) {
|
|
s := NewStore()
|
|
if _, _, err := s.CreateReach("holder-alia", "pk-dupe"); err == nil {
|
|
t.Errorf("CreateReach duplicate holder-alia: expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestCreateReachValidationG027(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
holderID string
|
|
publicKey string
|
|
wantErr bool
|
|
}{
|
|
{"empty holder", "", "pk", true},
|
|
{"empty pubkey", "h", "", true},
|
|
{"holder too long", stringOf('x', 129), "pk", true},
|
|
{"pubkey too long", "h", stringOf('y', 129), true},
|
|
{"holder with slash", "h/x", "pk", true},
|
|
{"holder with backslash", "h\\x", "pk", true},
|
|
{"holder with template syntax", "h{{", "pk", true},
|
|
{"pubkey with slash", "h", "p/x", true},
|
|
{"valid minimal", "h", "p", false},
|
|
{"valid typical", "holder-oka", "pk-oka-7", false},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
s := NewStore()
|
|
_, _, err := s.CreateReach(c.holderID, c.publicKey)
|
|
if c.wantErr && err == nil {
|
|
t.Errorf("expected error, got nil")
|
|
}
|
|
if !c.wantErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetReachHitMiss(t *testing.T) {
|
|
s := NewStore()
|
|
if _, ok := s.GetReach("holder-alia"); !ok {
|
|
t.Errorf("GetReach(holder-alia) miss, want hit (seeded)")
|
|
}
|
|
if _, ok := s.GetReach("nobody"); ok {
|
|
t.Errorf("GetReach(nobody) hit, want miss")
|
|
}
|
|
}
|
|
|
|
func TestGetStashHitMiss(t *testing.T) {
|
|
s := NewStore()
|
|
if _, ok := s.GetStash("holder-alia"); !ok {
|
|
t.Errorf("GetStash(holder-alia) miss, want hit (seeded)")
|
|
}
|
|
if _, ok := s.GetStash("nobody"); ok {
|
|
t.Errorf("GetStash(nobody) hit, want miss")
|
|
}
|
|
}
|
|
|
|
func TestGetStashActivityHitMiss(t *testing.T) {
|
|
s := NewStore()
|
|
stash, ok := s.GetStash("holder-alia")
|
|
if !ok {
|
|
t.Fatal("seeded stash holder-alia missing")
|
|
}
|
|
if _, ok := s.GetStashActivity(stash.StashID); !ok {
|
|
t.Errorf("GetStashActivity(%q) miss, want hit", stash.StashID)
|
|
}
|
|
if _, ok := s.GetStashActivity("stash-nobody"); ok {
|
|
t.Errorf("GetStashActivity(stash-nobody) hit, want miss")
|
|
}
|
|
}
|
|
|
|
func TestCreateReachConcurrentNoRace(t *testing.T) {
|
|
s := NewStore()
|
|
const n = 50
|
|
var wg sync.WaitGroup
|
|
wg.Add(n)
|
|
for i := 0; i < n; i++ {
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
holder := "holder-concurrent-" + itoa(i)
|
|
_, _, _ = s.CreateReach(holder, "pk")
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
// All n concurrent creates with distinct holder IDs must be present.
|
|
for i := 0; i < n; i++ {
|
|
if _, ok := s.GetReach("holder-concurrent-" + itoa(i)); !ok {
|
|
t.Errorf("concurrent reach %d missing after wg.Wait", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSeededMatureVsImmature(t *testing.T) {
|
|
s := NewStore()
|
|
// holder-alia: ActiveDays=92, MaxGapDays=10 -> mature.
|
|
aliaStash, ok := s.GetStash("holder-alia")
|
|
if !ok {
|
|
t.Fatal("seeded holder-alia missing")
|
|
}
|
|
aliaAct, ok := s.GetStashActivity(aliaStash.StashID)
|
|
if !ok {
|
|
t.Fatal("seeded alia activity missing")
|
|
}
|
|
if !aliaAct.IsMature() {
|
|
t.Errorf("holder-alia IsMature=false, want true (ActiveDays=%d, MaxGap=%d)",
|
|
aliaAct.ActiveDays, aliaAct.MaxGapDays)
|
|
}
|
|
// holder-bryn: ActiveDays=45, MaxGapDays=5 -> not mature.
|
|
brynStash, ok := s.GetStash("holder-bryn")
|
|
if !ok {
|
|
t.Fatal("seeded holder-bryn missing")
|
|
}
|
|
brynAct, ok := s.GetStashActivity(brynStash.StashID)
|
|
if !ok {
|
|
t.Fatal("seeded bryn activity missing")
|
|
}
|
|
if brynAct.IsMature() {
|
|
t.Errorf("holder-bryn IsMature=true, want false (ActiveDays=%d, MaxGap=%d)",
|
|
brynAct.ActiveDays, brynAct.MaxGapDays)
|
|
}
|
|
}
|
|
|
|
// Compile-time assertions that the types are the real x/*/types structs
|
|
// (D-067: the mock store grounds the UI in the real Go type definitions).
|
|
var _ identitytypes.Reach
|
|
var _ stashtypes.Stash
|
|
|
|
// itoa is a tiny strconv.Itoa without the import (keeps store_test.go deps
|
|
// to just sync + testing + the two x/*/types packages).
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
neg := n < 0
|
|
if neg {
|
|
n = -n
|
|
}
|
|
var buf [20]byte
|
|
i := len(buf)
|
|
for n > 0 {
|
|
i--
|
|
buf[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
if neg {
|
|
i--
|
|
buf[i] = '-'
|
|
}
|
|
return string(buf[i:])
|
|
}
|
|
|
|
func stringOf(r rune, n int) string {
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = byte(r)
|
|
}
|
|
return string(b)
|
|
}
|