973e25a7c9
web/handlers/stash.go: GET /stash/{holderID} renders balance in Grain +
Bread-scale conversion (x/bread/types.BreadScaleAll() + GrainsPerBread=10000,
D-073 code constants NOT docs) + 90-day maturity progress bar (ActiveDays/90
capped at 100%) + IsMature badge. stash_test.go: D-073 regression guard
(GrainsPerBread=10000, Crumb=100 Grain; would fail if docs 1000x values used),
mature vs immature fixture, 404 + G-026 error lexicon check. Coverage 83.5%
cumulative. Template FuncMap divGrain for the scale table.
---ci---
project: oy
phase: 2
milestone: v0.6
status: execute
---/ci---
157 lines
5.1 KiB
Go
157 lines
5.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
breadtypes "github.com/oy/openyield/x/bread/types"
|
|
stashtypes "github.com/oy/openyield/x/stash/types"
|
|
)
|
|
|
|
func TestStashDashboardSeededMatureHolder(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
mux := http.NewServeMux()
|
|
srv.Register(mux)
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/stash/holder-alia", nil)
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("GET /stash/holder-alia: status %d, want 200", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
// Balance in Grain present.
|
|
if !strings.Contains(body, "Grain") {
|
|
t.Errorf("body missing 'Grain' balance")
|
|
}
|
|
// Bread-scale conversion table present (all 11 denominations from BreadScaleAll).
|
|
for _, ds := range breadtypes.BreadScaleAll() {
|
|
if !strings.Contains(body, ds.Name) {
|
|
t.Errorf("body missing Bread-scale denomination %q", ds.Name)
|
|
}
|
|
}
|
|
// Mature holder (ActiveDays=92, MaxGap=10): progress ~100%, Mature badge.
|
|
if !strings.Contains(body, "Mature") {
|
|
t.Errorf("body missing 'Mature' badge for mature holder-alia")
|
|
}
|
|
assertNoBannedTerms(t, body)
|
|
}
|
|
|
|
func TestStashDashboardImmatureHolder(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
mux := http.NewServeMux()
|
|
srv.Register(mux)
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/stash/holder-bryn", nil)
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("GET /stash/holder-bryn: status %d, want 200", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
// Immature holder (ActiveDays=45, MaxGap=5): Not mature badge.
|
|
if !strings.Contains(body, "Not mature") {
|
|
t.Errorf("body missing 'Not mature' badge for immature holder-bryn")
|
|
}
|
|
// Progress bar at 50% (45/90).
|
|
if !strings.Contains(body, "50%") {
|
|
t.Errorf("body missing 50%% progress for holder-bryn (45/90 days)")
|
|
}
|
|
assertNoBannedTerms(t, body)
|
|
}
|
|
|
|
func TestStashDashboardMissingReturns404(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
mux := http.NewServeMux()
|
|
srv.Register(mux)
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/stash/nobody", nil)
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("GET /stash/nobody: status %d, want 404", rec.Code)
|
|
}
|
|
// G-026: rendered-HTML lexicon check on the ERROR response body too.
|
|
assertNoBannedTerms(t, rec.Body.String())
|
|
}
|
|
|
|
// TestStashBreadScaleConversionCorrectness (D-073 regression guard): asserts
|
|
// the Stash dashboard uses x/bread/types code constants (GrainsPerBread=10000,
|
|
// BreadScaleAll() with Grain=1, Crumb=100, Bread=10000...), NOT the outdated
|
|
// docs/shared/bread-scale.md (which claims 1,000x ratios). This test would FAIL
|
|
// if the handler hardcoded the docs values instead of using the code constants.
|
|
func TestStashBreadScaleConversionCorrectness(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
mux := http.NewServeMux()
|
|
srv.Register(mux)
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/stash/holder-alia", nil)
|
|
mux.ServeHTTP(rec, req)
|
|
body := rec.Body.String()
|
|
|
|
// D-073: the code constants are the source of truth.
|
|
// GrainsPerBread must be 10000 (code), NOT 1000 (docs claim 1 Crumb=1000 Grain).
|
|
if breadtypes.GrainsPerBread != 10000 {
|
|
t.Fatalf("D-073: x/bread/types.GrainsPerBread = %d, want 10000 (code constant)", breadtypes.GrainsPerBread)
|
|
}
|
|
|
|
// The handler computes BalanceBread = BalanceGrain / GrainsPerBread.
|
|
// holder-alia seed: BalanceGrain = 920000 -> 92 Bread.
|
|
stash, ok := srv.Store.GetStash("holder-alia")
|
|
if !ok {
|
|
t.Fatal("seeded holder-alia stash missing")
|
|
}
|
|
wantBread := stash.BalanceGrain / breadtypes.GrainsPerBread
|
|
wantBreadStr := []byte(formatInt(wantBread))
|
|
if !strings.Contains(body, string(wantBreadStr)) {
|
|
t.Errorf("D-073: body missing expected Bread conversion %d (from %d Grain / %d GrainsPerBread)",
|
|
wantBread, stash.BalanceGrain, breadtypes.GrainsPerBread)
|
|
}
|
|
|
|
// The Bread-scale table must include the code-constant Grain values.
|
|
scale := breadtypes.BreadScaleAll()
|
|
for _, ds := range scale {
|
|
if !strings.Contains(body, formatInt(ds.GrainValue)) {
|
|
t.Errorf("D-073: body missing Bread-scale GrainValue %d for %s", ds.GrainValue, ds.Name)
|
|
}
|
|
}
|
|
|
|
// Regression guard: if someone used the outdated docs value (1 Crumb = 1000
|
|
// Grain) instead of the code constant (1 Crumb = 100 Grain), the Crumb row
|
|
// would show 1000 — assert it shows 100 (the code value).
|
|
crumbs := scale[1] // index 1 = Crumb
|
|
if crumbs.Name != "Crumb" || crumbs.GrainValue != 100 {
|
|
t.Fatalf("D-073: BreadScaleAll()[1] = {%s, %d}, want {Crumb, 100}", crumbs.Name, crumbs.GrainValue)
|
|
}
|
|
if !strings.Contains(body, "100") {
|
|
t.Errorf("D-073: body missing code-constant Crumb=100 Grain (would show 1000 if docs values were used)")
|
|
}
|
|
}
|
|
|
|
// Compile-time assertions that the handler uses the real x/*/types structs
|
|
// (D-067: the UI grounds in the real Go type definitions).
|
|
var _ stashtypes.Stash
|
|
var _ stashtypes.StashActivity
|
|
|
|
// formatInt is a tiny strconv.Itoa without the import (keeps test deps minimal).
|
|
func formatInt(n int64) 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:])
|
|
}
|