eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// The free-pool gauge (pool.go): pure budget math, the cost-model config
4// parser, the sweep's ledger round-trip, and the admin endpoint. No test here
5// exercises admission control — slice 1 is a gauge and these tests pin that
6// (nothing refuses, nothing actuates).
7
8import (
9 "encoding/json"
10 "math"
11 "net/http"
12 "net/http/httptest"
13 "testing"
14 "time"
15)
16
17func near(t *testing.T, name string, got, want float64) {
18 t.Helper()
19 if math.Abs(got-want) > 1e-9 {
20 t.Fatalf("%s = %v, want %v", name, got, want)
21 }
22}
23
24// Easy-arithmetic cost model for the math tests.
25func testPoolCfg() poolConfig {
26 return poolConfig{
27 maxShare: 25,
28 revenueShare: 30,
29 baseEURDay: 0.20,
30 demoShare: 20,
31 costs: poolCosts{
32 fixedDay: 1.0,
33 slotDay: 0.03,
34 reqDay: 0.01,
35 storageGBDay: 0.001,
36 awakeFree: 0.5,
37 awakeDemo: 1.0,
38 },
39 }
40}
41
42func TestParsePoolCostsDefaults(t *testing.T) {
43 c, err := parsePoolCosts("")
44 if err != nil {
45 t.Fatal(err)
46 }
47 if c != defaultPoolCosts() {
48 t.Fatalf("empty config should yield defaults, got %+v", c)
49 }
50}
51
52func TestParsePoolCostsOverrides(t *testing.T) {
53 c, err := parsePoolCosts("fixed_day=2.5, awake_demo=0.6")
54 if err != nil {
55 t.Fatal(err)
56 }
57 near(t, "fixedDay", c.fixedDay, 2.5)
58 near(t, "awakeDemo", c.awakeDemo, 0.6)
59 // Untouched keys keep their defaults.
60 near(t, "slotDay", c.slotDay, defaultPoolCosts().slotDay)
61}
62
63func TestParsePoolCostsRejectsMistakes(t *testing.T) {
64 for _, bad := range []string{"fixed=1.0", "fixed_day=abc", "slot_day=-1", "fixed_day"} {
65 if _, err := parsePoolCosts(bad); err == nil {
66 t.Fatalf("parsePoolCosts(%q) should error", bad)
67 }
68 }
69}
70
71// Zero revenue: the operator floor carries the pool, clamped by the X ceiling.
72func TestComputePoolZeroRevenue(t *testing.T) {
73 cfg := testPoolCfg()
74 rows := []poolRow{
75 {Entitlement: "free", StorageBytes: 1e9},
76 {Entitlement: "free", StorageBytes: 1e9},
77 }
78 r := computePool(cfg, rows)
79 // Each free space: 0.5*(0.03+0.01) + 1GB*0.001 = 0.021.
80 near(t, "FreeCostDay", r.FreeCostDay, 0.042)
81 near(t, "TotalCostDay", r.TotalCostDay, 1.042)
82 // budget = min(25% x 1.042, max(0.20, 0)) = 0.20 — the floor wins at zero revenue.
83 near(t, "BudgetDay", r.BudgetDay, 0.20)
84 near(t, "Pressure", r.Pressure, 0.042/0.20)
85 near(t, "DemoBudgetDay", r.DemoBudgetDay, 0.04)
86 if r.FreeCount != 2 || r.PaidCount != 0 || r.DemoCount != 0 {
87 t.Fatalf("counts = %d/%d/%d, want 2 free / 0 demo / 0 paid", r.FreeCount, r.DemoCount, r.PaidCount)
88 }
89}
90
91// With revenue, the Y-term takes over — and the X ceiling clamps it.
92func TestComputePoolRevenueClampedByCeiling(t *testing.T) {
93 cfg := testPoolCfg()
94 rows := []poolRow{
95 {Entitlement: "free", StorageBytes: 1e9},
96 {Entitlement: "free", StorageBytes: 1e9},
97 {Entitlement: entPaid, Tier: "business", NetEURYear: 3650}, // €10/day net
98 }
99 r := computePool(cfg, rows)
100 near(t, "NetRevenueDay", r.NetRevenueDay, 10)
101 // Paid space still costs (0.5*0.04 = 0.02): total = 0.042+0.02+1.0 = 1.062.
102 near(t, "TotalCostDay", r.TotalCostDay, 1.062)
103 // revenue pool = 3.0 but the invariant cap = 25% x 1.062 = 0.2655 clamps.
104 near(t, "BudgetDay", r.BudgetDay, 0.2655)
105 if r.PaidCount != 1 {
106 t.Fatalf("PaidCount = %d, want 1", r.PaidCount)
107 }
108}
109
110// Demos cost at the demo awake fraction and count into free spend.
111func TestComputePoolDemo(t *testing.T) {
112 cfg := testPoolCfg()
113 r := computePool(cfg, []poolRow{{Entitlement: "demo"}})
114 // 1.0*(0.03+0.01) = 0.04, no storage.
115 near(t, "FreeCostDay", r.FreeCostDay, 0.04)
116 if r.DemoCount != 1 || r.FreeCount != 0 {
117 t.Fatalf("counts = %d free / %d demo, want 0/1", r.FreeCount, r.DemoCount)
118 }
119}
120
121func TestPoolStateThresholds(t *testing.T) {
122 for _, tc := range []struct {
123 pressure float64
124 want string
125 }{{0, "green"}, {0.79, "green"}, {0.8, "amber"}, {0.99, "amber"}, {1.0, "red"}, {1.49, "red"}, {1.5, "deep_red"}} {
126 if got := poolState(tc.pressure); got != tc.want {
127 t.Fatalf("poolState(%v) = %q, want %q", tc.pressure, got, tc.want)
128 }
129 }
130}
131
132// The sweep reads the ledger, resolves net revenue from the price book, and
133// records a reading that round-trips through pool_meter.
134func TestPoolSweepRecordsReading(t *testing.T) {
135 db := openTestCloudDB(t)
136 free := insertSpaceRow(t, db, "free.example.com", 1000)
137 paid := insertSpaceRow(t, db, "paid.example.com", 1000)
138 if _, err := db.sql.Exec(`UPDATE spaces SET storage_bytes = 2000000000 WHERE id = ?`, free); err != nil {
139 t.Fatal(err)
140 }
141 if _, err := db.sql.Exec(`UPDATE spaces SET entitlement = 'paid', tier = 'friends' WHERE id = ?`, paid); err != nil {
142 t.Fatal(err)
143 }
144 s := &cloudServer{
145 db: db,
146 pool: testPoolCfg(),
147 billing: &stripeBilling{tiers: []billingTier{
148 {Key: "friends", Name: "Friends", Label: "€40/year", Price: "price_f", NetEURYear: 36.5},
149 }},
150 }
151 s.poolSweep(time.Unix(2_000_000_000, 0))
152
153 ts, r, ok, err := db.latestPoolMeter()
154 if err != nil || !ok {
155 t.Fatalf("latestPoolMeter: ok=%v err=%v", ok, err)
156 }
157 if ts != 2_000_000_000 {
158 t.Fatalf("ts = %d", ts)
159 }
160 if r.FreeCount != 1 || r.PaidCount != 1 || r.DemoCount != 0 {
161 t.Fatalf("counts = %d/%d/%d", r.FreeCount, r.DemoCount, r.PaidCount)
162 }
163 near(t, "NetRevenueDay", r.NetRevenueDay, 36.5/365) // 0.1 — from the price book
164 // free: 0.5*0.04 + 2GB*0.001 = 0.022; paid adds 0.02; fixed 1.0.
165 near(t, "FreeCostDay", r.FreeCostDay, 0.022)
166 near(t, "TotalCostDay", r.TotalCostDay, 1.042)
167 if r.Pressure <= 0 {
168 t.Fatalf("pressure = %v, want > 0", r.Pressure)
169 }
170}
171
172// The gauge rides the meter sweep when enabled — and stays silent when off.
173func TestMeterOnceRunsPoolSweep(t *testing.T) {
174 db := openTestCloudDB(t)
175 insertSpaceRow(t, db, "a.example.com", 1000)
176 sock := fakeControl(t, func(w http.ResponseWriter, r *http.Request) {
177 writeJSON(w, http.StatusOK, map[string]any{
178 "members": 1, "setup_complete": true, "has_content": true, "storage_bytes": 1000,
179 })
180 })
181 s := &cloudServer{db: db, control: controlClient(sock), controlToken: "t", pool: testPoolCfg()}
182 s.meterOnce()
183 if _, _, ok, _ := db.latestPoolMeter(); !ok {
184 t.Fatal("meterOnce with the pool enabled should record a gauge reading")
185 }
186
187 // Pool off (the shipped default): same sweep, no reading.
188 db2 := openTestCloudDB(t)
189 insertSpaceRow(t, db2, "b.example.com", 1000)
190 s2 := &cloudServer{db: db2, control: controlClient(sock), controlToken: "t"}
191 s2.meterOnce()
192 if _, _, ok, _ := db2.latestPoolMeter(); ok {
193 t.Fatal("meterOnce with the pool off must not record a reading")
194 }
195}
196
197func poolGET(t *testing.T, url, token string) (*http.Response, map[string]any) {
198 t.Helper()
199 req, err := http.NewRequest("GET", url+"/api/pool", nil)
200 if err != nil {
201 t.Fatal(err)
202 }
203 if token != "" {
204 req.Header.Set("Authorization", "Bearer "+token)
205 }
206 resp, err := http.DefaultClient.Do(req)
207 if err != nil {
208 t.Fatal(err)
209 }
210 defer resp.Body.Close()
211 var body map[string]any
212 _ = json.NewDecoder(resp.Body).Decode(&body)
213 return resp, body
214}
215
216func TestPoolHandler(t *testing.T) {
217 db := openTestCloudDB(t)
218 s := &cloudServer{db: db, adminHash: hashToken("admin-secret"), pool: testPoolCfg()}
219 srv := httptest.NewServer(s.handler())
220 defer srv.Close()
221
222 // Admin-gated like the token surface.
223 if resp, _ := poolGET(t, srv.URL, ""); resp.StatusCode != http.StatusUnauthorized {
224 t.Fatalf("no token: HTTP %d, want 401", resp.StatusCode)
225 }
226 if resp, _ := poolGET(t, srv.URL, "wrong"); resp.StatusCode != http.StatusUnauthorized {
227 t.Fatalf("bad token: HTTP %d, want 401", resp.StatusCode)
228 }
229
230 // Enabled but never swept.
231 resp, body := poolGET(t, srv.URL, "admin-secret")
232 if resp.StatusCode != http.StatusOK || body["enabled"] != true || body["measured"] != false {
233 t.Fatalf("pre-sweep: HTTP %d body %v", resp.StatusCode, body)
234 }
235
236 // After a sweep: the reading, labelled an estimate.
237 insertSpaceRow(t, db, "c.example.com", 1000)
238 s.poolSweep(time.Unix(2_000_000_000, 0))
239 resp, body = poolGET(t, srv.URL, "admin-secret")
240 if resp.StatusCode != http.StatusOK || body["measured"] != true || body["estimate"] != true {
241 t.Fatalf("post-sweep: HTTP %d body %v", resp.StatusCode, body)
242 }
243 if body["state"] != "green" {
244 t.Fatalf("state = %v, want green (one tiny free space)", body["state"])
245 }
246 if _, ok := body["pressure"].(float64); !ok {
247 t.Fatalf("pressure missing: %v", body)
248 }
249
250 // Pool off: the endpoint says so instead of 404ing (status pages consume this).
251 sOff := &cloudServer{db: db, adminHash: hashToken("admin-secret")}
252 srvOff := httptest.NewServer(sOff.handler())
253 defer srvOff.Close()
254 resp, body = poolGET(t, srvOff.URL, "admin-secret")
255 if resp.StatusCode != http.StatusOK || body["enabled"] != false {
256 t.Fatalf("pool off: HTTP %d body %v", resp.StatusCode, body)
257 }
258}
259
260// ---- slice 2: admission control ---------------------------------------------
261
262// seedPressure plants a gauge reading with a chosen pressure.
263func seedPressure(t *testing.T, db *cloudDB, pressure float64) {
264 t.Helper()
265 if err := db.insertPoolMeter(1_999_000_000, poolReading{BudgetDay: 1, FreeCostDay: pressure, Pressure: pressure}); err != nil {
266 t.Fatal(err)
267 }
268}
269
270func TestSmoothRevenue(t *testing.T) {
271 near(t, "no history", smoothRevenue(10, nil), 10)
272 near(t, "averaged", smoothRevenue(0, []float64{10, 10, 10}), 7.5)
273}
274
275func TestPoolAdmissionRefusesAtRed(t *testing.T) {
276 srv, s := newTestCloud(t, okControl(t))
277 s.pool = testPoolCfg()
278 s.demoTTL, s.demoMaxLive = 24*time.Hour, 10
279 token := mintCaller(t, srv, "home")
280 seedPressure(t, s.db, 1.3)
281
282 resp := createKind(t, srv, token, "alice", "")
283 if resp.StatusCode != http.StatusServiceUnavailable {
284 t.Fatalf("free create at red: HTTP %d, want 503", resp.StatusCode)
285 }
286 body := decodeBody(t, resp)
287 if body["error"] != "free_pool_full" {
288 t.Fatalf("error = %v", body["error"])
289 }
290 if msg, _ := body["message"].(string); msg != freePoolFullMsg {
291 t.Fatalf("message = %q", msg)
292 }
293 // Demos stay available at red (the carve-out keeps the try-it path alive).
294 if resp := createKind(t, srv, token, "alice", "demo"); resp.StatusCode != http.StatusCreated {
295 t.Fatalf("demo at red: HTTP %d, want 201", resp.StatusCode)
296 }
297 // The refusal held no quota slot: at green the same requester creates fine.
298 seedPressure(t, s.db, 0.2)
299 if resp := createKind(t, srv, token, "alice", ""); resp.StatusCode != http.StatusCreated {
300 t.Fatalf("free create at green: HTTP %d, want 201", resp.StatusCode)
301 }
302}
303
304func TestPoolAdmissionFailsOpen(t *testing.T) {
305 // Dry-run: red pressure refuses nothing.
306 srv, s := newTestCloud(t, okControl(t))
307 s.pool = testPoolCfg()
308 s.pool.dryRun = true
309 token := mintCaller(t, srv, "home")
310 seedPressure(t, s.db, 2.0)
311 if resp := createKind(t, srv, token, "bob", ""); resp.StatusCode != http.StatusCreated {
312 t.Fatalf("dry-run at red: HTTP %d, want 201", resp.StatusCode)
313 }
314
315 // No reading yet: never refuse.
316 srv2, s2 := newTestCloud(t, okControl(t))
317 s2.pool = testPoolCfg()
318 token2 := mintCaller(t, srv2, "home")
319 if resp := createKind(t, srv2, token2, "carol", ""); resp.StatusCode != http.StatusCreated {
320 t.Fatalf("no reading: HTTP %d, want 201", resp.StatusCode)
321 }
322
323 // Pool off: a stale red reading in the table gates nothing.
324 srv3, s3 := newTestCloud(t, okControl(t))
325 token3 := mintCaller(t, srv3, "home")
326 seedPressure(t, s3.db, 5.0)
327 if resp := createKind(t, srv3, token3, "dave", ""); resp.StatusCode != http.StatusCreated {
328 t.Fatalf("pool off: HTTP %d, want 201", resp.StatusCode)
329 }
330}
331
332// The sweep's budget uses smoothed revenue: with fat historical readings and
333// zero current revenue, the recorded budget sits between floor and history.
334func TestPoolSweepSmoothsRevenue(t *testing.T) {
335 db := openTestCloudDB(t)
336 insertSpaceRow(t, db, "a.example.com", 1000)
337 // Three past sweeps at €10/day revenue each.
338 for i, ts := range []int64{1_990_000_000, 1_990_100_000, 1_990_200_000} {
339 _ = i
340 if err := db.insertPoolMeter(ts, poolReading{NetRevenueDay: 10}); err != nil {
341 t.Fatal(err)
342 }
343 }
344 s := &cloudServer{db: db, pool: testPoolCfg()}
345 s.poolSweep(time.Unix(1_990_300_000, 0))
346 _, r, ok, err := db.latestPoolMeter()
347 if err != nil || !ok {
348 t.Fatalf("latestPoolMeter: ok=%v err=%v", ok, err)
349 }
350 // Instantaneous revenue is 0 (no paid rows) and is what the row stores...
351 near(t, "NetRevenueDay", r.NetRevenueDay, 0)
352 // ...but the budget was derived from smoothed (0+10+10+10)/4 = 7.5€/day:
353 // revenue pool = 30% x 7.5 = 2.25, clamped by the X ceiling (25% of total).
354 wantBudget := math.Min(0.25*r.TotalCostDay, 2.25)
355 near(t, "BudgetDay", r.BudgetDay, wantBudget)
356}
357