carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net/http"
8 "net/url"
9 "os"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/economics"
14)
15
16// TestBillPassPullsAndManualWins: billPass pulls current + previous month
17// through the ceCosts seam and writes each SourceCostExplorer, then a
18// manually-entered bill for the current month (written directly, the way
19// the write doors this task adds do it) survives a second billPass run
20// byte-identical — manual always wins over an automatic pull.
21func TestBillPassPullsAndManualWins(t *testing.T) {
22 fx := newConsoleFixture(t)
23 ctx := context.Background()
24 now := time.Date(2026, 8, 11, 3, 0, 0, 0, time.UTC)
25
26 fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) {
27 return map[string]float64{"AmazonEC2": 55.5}, nil
28 }
29
30 if err := fx.srv.billPass(ctx, now); err != nil {
31 t.Fatal(err)
32 }
33
34 getBill := func(month string) economics.Bill {
35 t.Helper()
36 raw, _, err := fx.rel.Get(ctx, economics.BillKey(month))
37 if err != nil {
38 t.Fatalf("bill %s: %v", month, err)
39 }
40 var b economics.Bill
41 if err := json.Unmarshal(raw, &b); err != nil {
42 t.Fatal(err)
43 }
44 return b
45 }
46
47 for _, month := range []string{"2026-08", "2026-07"} {
48 b := getBill(month)
49 if b.Source != economics.SourceCostExplorer {
50 t.Fatalf("%s source = %q, want %q", month, b.Source, economics.SourceCostExplorer)
51 }
52 if b.TotalUSD != 55.5 {
53 t.Fatalf("%s total = %v, want 55.5", month, b.TotalUSD)
54 }
55 if b.Services["AmazonEC2"] != 55.5 {
56 t.Fatalf("%s services = %v, want AmazonEC2=55.5", month, b.Services)
57 }
58 }
59
60 // Write a manual bill for the current month straight to the bucket
61 // (read-etag-then-put, since the object already exists from the pull
62 // above), then confirm a second billPass run leaves it byte-identical.
63 _, etag, err := fx.rel.Get(ctx, economics.BillKey("2026-08"))
64 if err != nil {
65 t.Fatal(err)
66 }
67 manual := economics.Bill{Schema: 1, Month: "2026-08", TotalUSD: 999, Source: economics.SourceManual}
68 manualRaw, err := json.Marshal(manual)
69 if err != nil {
70 t.Fatal(err)
71 }
72 if _, err := fx.rel.Put(ctx, economics.BillKey("2026-08"), manualRaw, etag); err != nil {
73 t.Fatal(err)
74 }
75
76 if err := fx.srv.billPass(ctx, now); err != nil {
77 t.Fatal(err)
78 }
79 after, _, err := fx.rel.Get(ctx, economics.BillKey("2026-08"))
80 if err != nil {
81 t.Fatal(err)
82 }
83 if string(after) != string(manualRaw) {
84 t.Fatalf("manual bill overwritten by billPass:\n got %s\n want %s", after, manualRaw)
85 }
86}
87
88// TestBillPassNoCEIsQuiet: a ceCosts failure (no IAM grant, network down,
89// whatever) degrades billPass to a logged no-op — never a fatal error, and
90// never a written bill for either month.
91func TestBillPassNoCEIsQuiet(t *testing.T) {
92 fx := newConsoleFixture(t)
93 ctx := context.Background()
94 now := time.Date(2026, 8, 11, 3, 0, 0, 0, time.UTC)
95
96 fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) {
97 return nil, errors.New("AccessDeniedException: user is not authorized")
98 }
99
100 if err := fx.srv.billPass(ctx, now); err != nil {
101 t.Fatal(err)
102 }
103 for _, month := range []string{"2026-08", "2026-07"} {
104 if _, _, err := fx.rel.Get(ctx, economics.BillKey(month)); !errors.Is(err, os.ErrNotExist) {
105 t.Fatalf("bill %s written despite a ceCosts failure: err=%v", month, err)
106 }
107 }
108}
109
110// TestBillPassHandlesMonthBoundary is the review-round regression for the
111// day-1 bug: CE's TimePeriod.End is EXCLUSIVE and must be strictly after
112// Start. On day 1 of any month, the naive "current month" period
113// (firstOfThisMonth .. now) collapses Start==End (both format to the same
114// calendar date), which CE would refuse outright — and that refusal used to
115// latch ceDisabled and kill the PREVIOUS month's pull in the very same
116// tick, the most valuable and just-closed one. The fake fails loudly if it
117// is ever asked for a same-day period, so this proves billPass never sends
118// one; both months must land.
119func TestBillPassHandlesMonthBoundary(t *testing.T) {
120 fx := newConsoleFixture(t)
121 ctx := context.Background()
122 now := time.Date(2026, 9, 1, 3, 0, 0, 0, time.UTC)
123
124 fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) {
125 if start.Format("2006-01-02") == end.Format("2006-01-02") {
126 t.Fatalf("ceCosts called with start == end == %s (day 1 collapse)", start.Format("2006-01-02"))
127 }
128 return map[string]float64{"AmazonEC2": 10}, nil
129 }
130
131 if err := fx.srv.billPass(ctx, now); err != nil {
132 t.Fatal(err)
133 }
134 for _, month := range []string{"2026-09", "2026-08"} {
135 if _, _, err := fx.rel.Get(ctx, economics.BillKey(month)); err != nil {
136 t.Fatalf("bill %s not written on day 1 of the month: %v", month, err)
137 }
138 }
139}
140
141// TestBillPassSkipsSecondPullSameDay: CE bills $0.01/request, so an hourly
142// pass tick must pull each month AT MOST ONCE per UTC day — a second
143// billPass call the same day must not touch the ceCosts seam again at all.
144func TestBillPassSkipsSecondPullSameDay(t *testing.T) {
145 fx := newConsoleFixture(t)
146 ctx := context.Background()
147 now := time.Date(2026, 8, 11, 3, 0, 0, 0, time.UTC)
148
149 var calls int
150 fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) {
151 calls++
152 return map[string]float64{"AmazonEC2": 10}, nil
153 }
154
155 if err := fx.srv.billPass(ctx, now); err != nil {
156 t.Fatal(err)
157 }
158 first := calls
159 if first != 2 {
160 t.Fatalf("first billPass made %d ceCosts calls, want 2 (current + previous month)", first)
161 }
162
163 if err := fx.srv.billPass(ctx, now.Add(4*time.Hour)); err != nil {
164 t.Fatal(err)
165 }
166 if calls != first {
167 t.Fatalf("second billPass the same UTC day made %d more ceCosts calls, want 0", calls-first)
168 }
169}
170
171// TestBillPassStopsPullingSettledClosedMonth: a closed (previous) month
172// that was already pulled on or after day 3 of the following month has
173// seen CE's late-arriving line items settle — billPass must stop pulling
174// it forever, rather than paying for it every day for the rest of time.
175// The still-open current month, on a later day, still pulls normally.
176func TestBillPassStopsPullingSettledClosedMonth(t *testing.T) {
177 fx := newConsoleFixture(t)
178 ctx := context.Background()
179
180 settled := economics.Bill{
181 Schema: 1, Month: "2026-07", TotalUSD: 10,
182 Services: map[string]float64{"AmazonEC2": 10}, Source: economics.SourceCostExplorer,
183 PulledAt: time.Date(2026, 8, 5, 3, 0, 0, 0, time.UTC), // day 5 >= the day-3 grace
184 }
185 putStoreJSON(t, fx.rel, economics.BillKey("2026-07"), settled)
186
187 var calls int
188 fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) {
189 calls++
190 return map[string]float64{"AmazonEC2": 99}, nil
191 }
192
193 if err := fx.srv.billPass(ctx, time.Date(2026, 8, 20, 3, 0, 0, 0, time.UTC)); err != nil {
194 t.Fatal(err)
195 }
196 if calls != 1 {
197 t.Fatalf("billPass made %d ceCosts calls, want 1 (current month only; the settled closed month must be skipped)", calls)
198 }
199 raw, _, err := fx.rel.Get(ctx, economics.BillKey("2026-07"))
200 if err != nil {
201 t.Fatal(err)
202 }
203 var got economics.Bill
204 if err := json.Unmarshal(raw, &got); err != nil {
205 t.Fatal(err)
206 }
207 if got.TotalUSD != 10 {
208 t.Fatalf("settled closed-month bill total = %v, want unchanged 10 (must not have been re-pulled)", got.TotalUSD)
209 }
210}
211
212// TestBillPassRepullsJustClosedMonth is the regression for the "own last
213// daily pull looks settled" bug: a month's last pull WHILE STILL OPEN (e.g.
214// July 31, Day()==31) must not be mistaken for the day-3-of-the-FOLLOWING-
215// month settle-and-stop rule just because Day() happens to be >=
216// billCEDayGrace. The moment the month closes (billPass ticks on Aug 1) it
217// must still be re-pulled — CE's ~24h-lagged final line items for July
218// haven't landed yet at the instant July turns into August.
219func TestBillPassRepullsJustClosedMonth(t *testing.T) {
220 fx := newConsoleFixture(t)
221 ctx := context.Background()
222
223 notYetSettled := economics.Bill{
224 Schema: 1, Month: "2026-07", TotalUSD: 10,
225 Services: map[string]float64{"AmazonEC2": 10}, Source: economics.SourceCostExplorer,
226 PulledAt: time.Date(2026, 7, 31, 3, 0, 0, 0, time.UTC), // the month's own last daily pull
227 }
228 putStoreJSON(t, fx.rel, economics.BillKey("2026-07"), notYetSettled)
229
230 var julyPulled bool
231 fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) {
232 if start.Format("2006-01") == "2026-07" {
233 julyPulled = true
234 }
235 return map[string]float64{"AmazonEC2": 99}, nil
236 }
237
238 if err := fx.srv.billPass(ctx, time.Date(2026, 8, 1, 3, 0, 0, 0, time.UTC)); err != nil {
239 t.Fatal(err)
240 }
241 if !julyPulled {
242 t.Fatal("billPass did not re-pull July on Aug 1, though July's own PulledAt.Day()==31 satisfies the day-3 grace by coincidence — the just-closed month must still be re-pulled")
243 }
244
245 raw, _, err := fx.rel.Get(ctx, economics.BillKey("2026-07"))
246 if err != nil {
247 t.Fatal(err)
248 }
249 var got economics.Bill
250 if err := json.Unmarshal(raw, &got); err != nil {
251 t.Fatal(err)
252 }
253 if got.TotalUSD != 99 {
254 t.Fatalf("July bill total = %v, want 99 (re-pulled with the fresh ceCosts value)", got.TotalUSD)
255 }
256}
257
258// TestBillPassRetriesAfterTransientCEFailure: a non-auth-shaped ceCosts
259// failure (a throttle, a network blip) must not disable further attempts —
260// realCECosts' auth-shaped latch (cepull.go, isAuthShapedError) is the only
261// thing that may do that, and the seam bypasses realCECosts entirely, so
262// this proves billPass itself adds no independent latch of its own: a tick
263// whose calls all fail leaves nothing written, and the very next tick's
264// calls still reach the seam and succeed.
265func TestBillPassRetriesAfterTransientCEFailure(t *testing.T) {
266 fx := newConsoleFixture(t)
267 ctx := context.Background()
268 now := time.Date(2026, 8, 11, 3, 0, 0, 0, time.UTC)
269
270 var calls int
271 fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) {
272 calls++
273 if calls <= 2 { // both months fail on the first tick
274 return nil, errors.New("ThrottlingException: rate exceeded")
275 }
276 return map[string]float64{"AmazonEC2": 42}, nil
277 }
278
279 if err := fx.srv.billPass(ctx, now); err != nil {
280 t.Fatal(err)
281 }
282 if _, _, err := fx.rel.Get(ctx, economics.BillKey("2026-08")); !errors.Is(err, os.ErrNotExist) {
283 t.Fatal("bill written despite every ceCosts call failing on the first tick")
284 }
285
286 if err := fx.srv.billPass(ctx, now.Add(time.Hour)); err != nil {
287 t.Fatal(err)
288 }
289 raw, _, err := fx.rel.Get(ctx, economics.BillKey("2026-08"))
290 if err != nil {
291 t.Fatal(err)
292 }
293 var b economics.Bill
294 if err := json.Unmarshal(raw, &b); err != nil {
295 t.Fatal(err)
296 }
297 if b.TotalUSD != 42 {
298 t.Fatalf("bill total = %v, want 42 (the retried tick's data)", b.TotalUSD)
299 }
300}
301
302// TestEconomicsWriteDoorsRejectNonPOST: the three write doors are
303// registered WITHOUT a method prefix (server.go) specifically so a
304// wrong-method request never gets Go's default 405 + Allow — which would
305// itself confirm the route exists to someone who merely guessed the URL.
306// Every one of them must answer a GET with the exact same 404 an unknown
307// route gets, signed out AND signed in as an operator (the method check
308// runs before the session/operator gate).
309func TestEconomicsWriteDoorsRejectNonPOST(t *testing.T) {
310 fx := newConsoleFixture(t)
311 out := newBrowser(t)
312 ghostStatus, _, ghostBody := get(t, out, fx.ts.URL+"/this-route-does-not-exist")
313
314 consolePaths := []string{"/fleet/economics/scenarios", "/fleet/economics/bill"}
315 for _, path := range consolePaths {
316 if status, _, body := get(t, out, fx.ts.URL+path); status != ghostStatus || body != ghostBody {
317 t.Fatalf("signed-out GET %s = %d %q, want the same 404 as a missing route (%d %q)", path, status, body, ghostStatus, ghostBody)
318 }
319 }
320
321 c := signInPaul(t, fx)
322 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
323 t.Fatal(err)
324 }
325 for _, path := range consolePaths {
326 if status, _, body := get(t, c, fx.ts.URL+path); status != ghostStatus || body != ghostBody {
327 t.Fatalf("operator GET %s = %d %q, want the same 404 as a missing route (%d %q)", path, status, body, ghostStatus, ghostBody)
328 }
329 }
330
331 // The API door: an unauthenticated GET (no Authorization header) must
332 // be the same ghost 404 too, not cliUser's 401 — that would confirm
333 // the route exists just as surely as a 405 would.
334 if status, _, body := get(t, out, fx.ts.URL+"/api/economics/bill"); status != ghostStatus || body != ghostBody {
335 t.Fatalf("unauthenticated GET /api/economics/bill = %d %q, want the same 404 as a missing route (%d %q)", status, body, ghostStatus, ghostBody)
336 }
337}
338
339// TestScenarioSaveGateAndValidation pins the gate order (session ->
340// IsOperator -> byte-identical 404, matching TestFleetOperatorOnly's
341// comparison shape) BEFORE csrf, then bad-JSON and happy-path behaviour.
342func TestScenarioSaveGateAndValidation(t *testing.T) {
343 fx := newConsoleFixture(t)
344
345 out := newBrowser(t)
346 _, _, bodyGhost := get(t, out, fx.ts.URL+"/this-route-does-not-exist")
347
348 c := signInPaul(t, fx)
349 csrf := sessionToken(t, fx, c)
350 sc := economics.DefaultScenario()
351 scRaw, err := json.Marshal(sc)
352 if err != nil {
353 t.Fatal(err)
354 }
355
356 // Signed in, owns kass, but is not an operator: the same 404 a missing
357 // route gets.
358 form := url.Values{"csrf": {csrf}, "name": {"launch"}, "scenario": {string(scRaw)}}
359 if status, _, body := postForm(t, c, fx.ts.URL+"/fleet/economics/scenarios", form); status != http.StatusNotFound || body != bodyGhost {
360 t.Fatalf("non-operator scenario save = %d %q, want the same 404 as a missing route (%q)", status, body, bodyGhost)
361 }
362
363 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
364 t.Fatal(err)
365 }
366
367 // Bad JSON: a 4xx, and nothing gets written.
368 badForm := url.Values{"csrf": {csrf}, "name": {"launch"}, "scenario": {"{not valid json"}}
369 if status, _, _ := postForm(t, c, fx.ts.URL+"/fleet/economics/scenarios", badForm); status < 400 || status >= 500 {
370 t.Fatalf("bad scenario JSON = %d, want 4xx", status)
371 }
372 if _, _, err := fx.rel.Get(context.Background(), economics.ScenarioKey("launch")); !errors.Is(err, os.ErrNotExist) {
373 t.Fatal("a bad-JSON scenario save still wrote a scenario object")
374 }
375
376 // Valid scenario + activate=1: both objects land, Active.Name is right.
377 okForm := url.Values{"csrf": {csrf}, "name": {"launch"}, "scenario": {string(scRaw)}, "activate": {"1"}}
378 status, loc, _ := postForm(t, c, fx.ts.URL+"/fleet/economics/scenarios", okForm)
379 if status != http.StatusSeeOther || loc != "/fleet/economics" {
380 t.Fatalf("valid scenario save = %d -> %q, want 303 -> /fleet/economics", status, loc)
381 }
382 raw, _, err := fx.rel.Get(context.Background(), economics.ScenarioKey("launch"))
383 if err != nil {
384 t.Fatal(err)
385 }
386 var saved economics.Scenario
387 if err := json.Unmarshal(raw, &saved); err != nil {
388 t.Fatal(err)
389 }
390 if saved.Name != "launch" {
391 t.Fatalf("saved scenario name = %q, want %q (form name must win over the JSON body's)", saved.Name, "launch")
392 }
393 activeRaw, _, err := fx.rel.Get(context.Background(), economics.ActiveKey)
394 if err != nil {
395 t.Fatal(err)
396 }
397 var active economics.Active
398 if err := json.Unmarshal(activeRaw, &active); err != nil {
399 t.Fatal(err)
400 }
401 if active.Name != "launch" {
402 t.Fatalf("active.Name = %q, want %q", active.Name, "launch")
403 }
404}
405
406// TestAPIBillDoor: the CLI's bearer door mirrors the console form's gate
407// order (cliUser -> IsOperator -> byte-identical 404) and manual-write
408// semantics.
409func TestAPIBillDoor(t *testing.T) {
410 fx := newConsoleFixture(t)
411 token := cliToken(t, fx, "paul@keymail.dev", "code-apibill")
412
413 body := `{"month":"2026-08","total_usd":123.45,"services":{"AmazonEC2":55.0}}`
414
415 ghostStatus, ghostBody := postBody(t, fx.ts.URL+"/this-api-route-does-not-exist", token, body)
416 status, out := postBody(t, fx.ts.URL+"/api/economics/bill", token, body)
417 if status != http.StatusNotFound || status != ghostStatus || string(out) != string(ghostBody) {
418 t.Fatalf("non-operator api bill = %d %q, want the same 404 as a missing route (%d %q)", status, out, ghostStatus, ghostBody)
419 }
420
421 if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil {
422 t.Fatal(err)
423 }
424
425 status, out = postBody(t, fx.ts.URL+"/api/economics/bill", token, body)
426 if status != http.StatusOK {
427 t.Fatalf("operator api bill = %d %s, want 200", status, out)
428 }
429 var b economics.Bill
430 if err := json.Unmarshal(out, &b); err != nil {
431 t.Fatalf("bill response: %v (%s)", err, out)
432 }
433 if b.Month != "2026-08" || b.TotalUSD != 123.45 || b.Source != economics.SourceManual {
434 t.Fatalf("bill response = %+v", b)
435 }
436
437 raw, _, err := fx.rel.Get(context.Background(), economics.BillKey("2026-08"))
438 if err != nil {
439 t.Fatal(err)
440 }
441 var stored economics.Bill
442 if err := json.Unmarshal(raw, &stored); err != nil {
443 t.Fatal(err)
444 }
445 if stored.Source != economics.SourceManual || stored.TotalUSD != 123.45 || stored.Services["AmazonEC2"] != 55.0 {
446 t.Fatalf("stored bill = %+v", stored)
447 }
448}
449