56db37a463
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---
138 lines
4.6 KiB
Go
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:])
|
|
}
|