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) }