Files
openyield/web/handlers/bloom_test.go
T
cloudinit-bot 56db37a463 feat(P5): Bloom accrual view — per-Stash BloomRecord (REQ-044)
web/handlers/bloom.go: GET /bloom/{stashID} renders BloomRecord
(AccruedGrain, RateBasisPoints as %, LastAccrualBlock) + the 4.5%
target rate read from x/bloom/types.TargetBloomRateBasisPoints (D-073
code-constant source-of-truth, NOT hardcoded) + the 4.0%-5.0% band
(Min/MaxBloomRateBasisPoints) + Mission Lock note. Store extensions:
GetBloomRecord/ListBloomRecords. Seed: holder-alia at target rate (450
bps), holder-bryn at 420 bps (within band). bloom_test.go: D-073
target-rate source-of-truth regression guard (asserts TargetBloomRate-
BasisPoints=450, Min=400, Max=500 from code constants; would fail if
hardcoded), G-026 error lexicon check, highest-risk screen for banned-
term drift (uses "Bloom"/"real production"/"accrual" labels only).
Coverage: store 98.1%, handlers 89.2%.

---ci---
project: oy
phase: 5
milestone: v0.6
status: execute
---/ci---
2026-08-18 19:06:16 +00:00

138 lines
4.6 KiB
Go

package handlers
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
bloomtypes "github.com/oy/openyield/x/bloom/types"
)
func TestBloomSeededRecordRendersTargetRate(t *testing.T) {
srv := newTestServer(t)
mux := http.NewServeMux()
srv.Register(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/bloom/stash-holder-alia", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("GET /bloom/stash-holder-alia: status %d, want 200", rec.Code)
}
body := rec.Body.String()
// Accrued Grain present.
if !strings.Contains(body, "Grain") {
t.Errorf("body missing 'Grain'")
}
// Target rate 4.5% (from TargetBloomRateBasisPoints=450).
want := formatFloat(float64(bloomtypes.TargetBloomRateBasisPoints) / 100)
if !strings.Contains(body, want) {
t.Errorf("body missing target rate %s%% (TargetBloomRateBasisPoints=%d)", want, bloomtypes.TargetBloomRateBasisPoints)
}
// Mission Lock note present.
if !strings.Contains(body, "real production") {
t.Errorf("body missing Mission Lock note about real production")
}
assertNoBannedTerms(t, body)
}
func TestBloomMissingReturns404(t *testing.T) {
srv := newTestServer(t)
mux := http.NewServeMux()
srv.Register(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/bloom/stash-nobody", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("GET /bloom/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())
}
// TestBloomTargetRateFromCodeConstant (D-073 regression guard): asserts the
// handler reads x/bloom/types.TargetBloomRateBasisPoints=450 (NOT a hardcoded
// 450 or a docs value). This test would FAIL if the handler hardcoded the rate
// instead of reading the code constant.
func TestBloomTargetRateFromCodeConstant(t *testing.T) {
// D-073: the code constant is the source of truth.
if bloomtypes.TargetBloomRateBasisPoints != 450 {
t.Fatalf("D-073: TargetBloomRateBasisPoints = %d, want 450 (code constant)", bloomtypes.TargetBloomRateBasisPoints)
}
if bloomtypes.MinBloomRateBasisPoints != 400 {
t.Fatalf("D-073: MinBloomRateBasisPoints = %d, want 400 (code constant)", bloomtypes.MinBloomRateBasisPoints)
}
if bloomtypes.MaxBloomRateBasisPoints != 500 {
t.Fatalf("D-073: MaxBloomRateBasisPoints = %d, want 500 (code constant)", bloomtypes.MaxBloomRateBasisPoints)
}
srv := newTestServer(t)
mux := http.NewServeMux()
srv.Register(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/bloom/stash-holder-alia", nil)
mux.ServeHTTP(rec, req)
body := rec.Body.String()
// The rendered target rate must be the code constant / 100 = 4.5.
wantTarget := formatFloat(float64(bloomtypes.TargetBloomRateBasisPoints) / 100)
if !strings.Contains(body, wantTarget) {
t.Errorf("D-073: body missing target rate %s%% (from code constant %d)", wantTarget, bloomtypes.TargetBloomRateBasisPoints)
}
// The seeded record for holder-alia uses RateBasisPoints=450 (the target).
rec2, ok := srv.Store.GetBloomRecord("stash-holder-alia")
if !ok {
t.Fatal("seeded bloom record stash-holder-alia missing")
}
if rec2.RateBasisPoints != bloomtypes.TargetBloomRateBasisPoints {
t.Errorf("D-073: seeded record RateBasisPoints = %d, want %d (code constant)", rec2.RateBasisPoints, bloomtypes.TargetBloomRateBasisPoints)
}
// The rate band must be rendered from the code constants.
wantMin := formatFloat(float64(bloomtypes.MinBloomRateBasisPoints) / 100)
wantMax := formatFloat(float64(bloomtypes.MaxBloomRateBasisPoints) / 100)
if !strings.Contains(body, wantMin) {
t.Errorf("D-073: body missing min rate %s%% (from code constant)", wantMin)
}
if !strings.Contains(body, wantMax) {
t.Errorf("D-073: body missing max rate %s%% (from code constant)", wantMax)
}
}
// Compile-time assertion that the handler uses the real x/bloom/types struct.
var _ bloomtypes.BloomRecord
// formatFloat formats a float to 1 decimal place without importing strconv
// (keeps the test deps minimal; matches the template's printf "%.1f").
func formatFloat(f float64) string {
// Round to 1 decimal.
rounded := float64(int(f*10+0.5)) / 10
whole := int(rounded)
frac := int((rounded - float64(whole)) * 10)
if frac == 0 {
return formatInt2(int64(whole)) + ".0"
}
return formatInt2(int64(whole)) + "." + string(rune('0'+frac))
}
func formatInt2(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:])
}