package console import ( "context" "encoding/json" "errors" "net/http" "net/url" "os" "testing" "time" "github.com/carlosframework/platform/internal/economics" ) // TestBillPassPullsAndManualWins: billPass pulls current + previous month // through the ceCosts seam and writes each SourceCostExplorer, then a // manually-entered bill for the current month (written directly, the way // the write doors this task adds do it) survives a second billPass run // byte-identical — manual always wins over an automatic pull. func TestBillPassPullsAndManualWins(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() now := time.Date(2026, 8, 11, 3, 0, 0, 0, time.UTC) fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) { return map[string]float64{"AmazonEC2": 55.5}, nil } if err := fx.srv.billPass(ctx, now); err != nil { t.Fatal(err) } getBill := func(month string) economics.Bill { t.Helper() raw, _, err := fx.rel.Get(ctx, economics.BillKey(month)) if err != nil { t.Fatalf("bill %s: %v", month, err) } var b economics.Bill if err := json.Unmarshal(raw, &b); err != nil { t.Fatal(err) } return b } for _, month := range []string{"2026-08", "2026-07"} { b := getBill(month) if b.Source != economics.SourceCostExplorer { t.Fatalf("%s source = %q, want %q", month, b.Source, economics.SourceCostExplorer) } if b.TotalUSD != 55.5 { t.Fatalf("%s total = %v, want 55.5", month, b.TotalUSD) } if b.Services["AmazonEC2"] != 55.5 { t.Fatalf("%s services = %v, want AmazonEC2=55.5", month, b.Services) } } // Write a manual bill for the current month straight to the bucket // (read-etag-then-put, since the object already exists from the pull // above), then confirm a second billPass run leaves it byte-identical. _, etag, err := fx.rel.Get(ctx, economics.BillKey("2026-08")) if err != nil { t.Fatal(err) } manual := economics.Bill{Schema: 1, Month: "2026-08", TotalUSD: 999, Source: economics.SourceManual} manualRaw, err := json.Marshal(manual) if err != nil { t.Fatal(err) } if _, err := fx.rel.Put(ctx, economics.BillKey("2026-08"), manualRaw, etag); err != nil { t.Fatal(err) } if err := fx.srv.billPass(ctx, now); err != nil { t.Fatal(err) } after, _, err := fx.rel.Get(ctx, economics.BillKey("2026-08")) if err != nil { t.Fatal(err) } if string(after) != string(manualRaw) { t.Fatalf("manual bill overwritten by billPass:\n got %s\n want %s", after, manualRaw) } } // TestBillPassNoCEIsQuiet: a ceCosts failure (no IAM grant, network down, // whatever) degrades billPass to a logged no-op — never a fatal error, and // never a written bill for either month. func TestBillPassNoCEIsQuiet(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() now := time.Date(2026, 8, 11, 3, 0, 0, 0, time.UTC) fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) { return nil, errors.New("AccessDeniedException: user is not authorized") } if err := fx.srv.billPass(ctx, now); err != nil { t.Fatal(err) } for _, month := range []string{"2026-08", "2026-07"} { if _, _, err := fx.rel.Get(ctx, economics.BillKey(month)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("bill %s written despite a ceCosts failure: err=%v", month, err) } } } // TestBillPassHandlesMonthBoundary is the review-round regression for the // day-1 bug: CE's TimePeriod.End is EXCLUSIVE and must be strictly after // Start. On day 1 of any month, the naive "current month" period // (firstOfThisMonth .. now) collapses Start==End (both format to the same // calendar date), which CE would refuse outright — and that refusal used to // latch ceDisabled and kill the PREVIOUS month's pull in the very same // tick, the most valuable and just-closed one. The fake fails loudly if it // is ever asked for a same-day period, so this proves billPass never sends // one; both months must land. func TestBillPassHandlesMonthBoundary(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() now := time.Date(2026, 9, 1, 3, 0, 0, 0, time.UTC) fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) { if start.Format("2006-01-02") == end.Format("2006-01-02") { t.Fatalf("ceCosts called with start == end == %s (day 1 collapse)", start.Format("2006-01-02")) } return map[string]float64{"AmazonEC2": 10}, nil } if err := fx.srv.billPass(ctx, now); err != nil { t.Fatal(err) } for _, month := range []string{"2026-09", "2026-08"} { if _, _, err := fx.rel.Get(ctx, economics.BillKey(month)); err != nil { t.Fatalf("bill %s not written on day 1 of the month: %v", month, err) } } } // TestBillPassSkipsSecondPullSameDay: CE bills $0.01/request, so an hourly // pass tick must pull each month AT MOST ONCE per UTC day — a second // billPass call the same day must not touch the ceCosts seam again at all. func TestBillPassSkipsSecondPullSameDay(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() now := time.Date(2026, 8, 11, 3, 0, 0, 0, time.UTC) var calls int fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) { calls++ return map[string]float64{"AmazonEC2": 10}, nil } if err := fx.srv.billPass(ctx, now); err != nil { t.Fatal(err) } first := calls if first != 2 { t.Fatalf("first billPass made %d ceCosts calls, want 2 (current + previous month)", first) } if err := fx.srv.billPass(ctx, now.Add(4*time.Hour)); err != nil { t.Fatal(err) } if calls != first { t.Fatalf("second billPass the same UTC day made %d more ceCosts calls, want 0", calls-first) } } // TestBillPassStopsPullingSettledClosedMonth: a closed (previous) month // that was already pulled on or after day 3 of the following month has // seen CE's late-arriving line items settle — billPass must stop pulling // it forever, rather than paying for it every day for the rest of time. // The still-open current month, on a later day, still pulls normally. func TestBillPassStopsPullingSettledClosedMonth(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() settled := economics.Bill{ Schema: 1, Month: "2026-07", TotalUSD: 10, Services: map[string]float64{"AmazonEC2": 10}, Source: economics.SourceCostExplorer, PulledAt: time.Date(2026, 8, 5, 3, 0, 0, 0, time.UTC), // day 5 >= the day-3 grace } putStoreJSON(t, fx.rel, economics.BillKey("2026-07"), settled) var calls int fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) { calls++ return map[string]float64{"AmazonEC2": 99}, nil } if err := fx.srv.billPass(ctx, time.Date(2026, 8, 20, 3, 0, 0, 0, time.UTC)); err != nil { t.Fatal(err) } if calls != 1 { t.Fatalf("billPass made %d ceCosts calls, want 1 (current month only; the settled closed month must be skipped)", calls) } raw, _, err := fx.rel.Get(ctx, economics.BillKey("2026-07")) if err != nil { t.Fatal(err) } var got economics.Bill if err := json.Unmarshal(raw, &got); err != nil { t.Fatal(err) } if got.TotalUSD != 10 { t.Fatalf("settled closed-month bill total = %v, want unchanged 10 (must not have been re-pulled)", got.TotalUSD) } } // TestBillPassRepullsJustClosedMonth is the regression for the "own last // daily pull looks settled" bug: a month's last pull WHILE STILL OPEN (e.g. // July 31, Day()==31) must not be mistaken for the day-3-of-the-FOLLOWING- // month settle-and-stop rule just because Day() happens to be >= // billCEDayGrace. The moment the month closes (billPass ticks on Aug 1) it // must still be re-pulled — CE's ~24h-lagged final line items for July // haven't landed yet at the instant July turns into August. func TestBillPassRepullsJustClosedMonth(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() notYetSettled := economics.Bill{ Schema: 1, Month: "2026-07", TotalUSD: 10, Services: map[string]float64{"AmazonEC2": 10}, Source: economics.SourceCostExplorer, PulledAt: time.Date(2026, 7, 31, 3, 0, 0, 0, time.UTC), // the month's own last daily pull } putStoreJSON(t, fx.rel, economics.BillKey("2026-07"), notYetSettled) var julyPulled bool fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) { if start.Format("2006-01") == "2026-07" { julyPulled = true } return map[string]float64{"AmazonEC2": 99}, nil } if err := fx.srv.billPass(ctx, time.Date(2026, 8, 1, 3, 0, 0, 0, time.UTC)); err != nil { t.Fatal(err) } if !julyPulled { 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") } raw, _, err := fx.rel.Get(ctx, economics.BillKey("2026-07")) if err != nil { t.Fatal(err) } var got economics.Bill if err := json.Unmarshal(raw, &got); err != nil { t.Fatal(err) } if got.TotalUSD != 99 { t.Fatalf("July bill total = %v, want 99 (re-pulled with the fresh ceCosts value)", got.TotalUSD) } } // TestBillPassRetriesAfterTransientCEFailure: a non-auth-shaped ceCosts // failure (a throttle, a network blip) must not disable further attempts — // realCECosts' auth-shaped latch (cepull.go, isAuthShapedError) is the only // thing that may do that, and the seam bypasses realCECosts entirely, so // this proves billPass itself adds no independent latch of its own: a tick // whose calls all fail leaves nothing written, and the very next tick's // calls still reach the seam and succeed. func TestBillPassRetriesAfterTransientCEFailure(t *testing.T) { fx := newConsoleFixture(t) ctx := context.Background() now := time.Date(2026, 8, 11, 3, 0, 0, 0, time.UTC) var calls int fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) { calls++ if calls <= 2 { // both months fail on the first tick return nil, errors.New("ThrottlingException: rate exceeded") } return map[string]float64{"AmazonEC2": 42}, nil } if err := fx.srv.billPass(ctx, now); err != nil { t.Fatal(err) } if _, _, err := fx.rel.Get(ctx, economics.BillKey("2026-08")); !errors.Is(err, os.ErrNotExist) { t.Fatal("bill written despite every ceCosts call failing on the first tick") } if err := fx.srv.billPass(ctx, now.Add(time.Hour)); err != nil { t.Fatal(err) } raw, _, err := fx.rel.Get(ctx, economics.BillKey("2026-08")) if err != nil { t.Fatal(err) } var b economics.Bill if err := json.Unmarshal(raw, &b); err != nil { t.Fatal(err) } if b.TotalUSD != 42 { t.Fatalf("bill total = %v, want 42 (the retried tick's data)", b.TotalUSD) } } // TestEconomicsWriteDoorsRejectNonPOST: the three write doors are // registered WITHOUT a method prefix (server.go) specifically so a // wrong-method request never gets Go's default 405 + Allow — which would // itself confirm the route exists to someone who merely guessed the URL. // Every one of them must answer a GET with the exact same 404 an unknown // route gets, signed out AND signed in as an operator (the method check // runs before the session/operator gate). func TestEconomicsWriteDoorsRejectNonPOST(t *testing.T) { fx := newConsoleFixture(t) out := newBrowser(t) ghostStatus, _, ghostBody := get(t, out, fx.ts.URL+"/this-route-does-not-exist") consolePaths := []string{"/fleet/economics/scenarios", "/fleet/economics/bill"} for _, path := range consolePaths { if status, _, body := get(t, out, fx.ts.URL+path); status != ghostStatus || body != ghostBody { t.Fatalf("signed-out GET %s = %d %q, want the same 404 as a missing route (%d %q)", path, status, body, ghostStatus, ghostBody) } } c := signInPaul(t, fx) if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } for _, path := range consolePaths { if status, _, body := get(t, c, fx.ts.URL+path); status != ghostStatus || body != ghostBody { t.Fatalf("operator GET %s = %d %q, want the same 404 as a missing route (%d %q)", path, status, body, ghostStatus, ghostBody) } } // The API door: an unauthenticated GET (no Authorization header) must // be the same ghost 404 too, not cliUser's 401 — that would confirm // the route exists just as surely as a 405 would. if status, _, body := get(t, out, fx.ts.URL+"/api/economics/bill"); status != ghostStatus || body != ghostBody { t.Fatalf("unauthenticated GET /api/economics/bill = %d %q, want the same 404 as a missing route (%d %q)", status, body, ghostStatus, ghostBody) } } // TestScenarioSaveGateAndValidation pins the gate order (session -> // IsOperator -> byte-identical 404, matching TestFleetOperatorOnly's // comparison shape) BEFORE csrf, then bad-JSON and happy-path behaviour. func TestScenarioSaveGateAndValidation(t *testing.T) { fx := newConsoleFixture(t) out := newBrowser(t) _, _, bodyGhost := get(t, out, fx.ts.URL+"/this-route-does-not-exist") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) sc := economics.DefaultScenario() scRaw, err := json.Marshal(sc) if err != nil { t.Fatal(err) } // Signed in, owns kass, but is not an operator: the same 404 a missing // route gets. form := url.Values{"csrf": {csrf}, "name": {"launch"}, "scenario": {string(scRaw)}} if status, _, body := postForm(t, c, fx.ts.URL+"/fleet/economics/scenarios", form); status != http.StatusNotFound || body != bodyGhost { t.Fatalf("non-operator scenario save = %d %q, want the same 404 as a missing route (%q)", status, body, bodyGhost) } if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } // Bad JSON: a 4xx, and nothing gets written. badForm := url.Values{"csrf": {csrf}, "name": {"launch"}, "scenario": {"{not valid json"}} if status, _, _ := postForm(t, c, fx.ts.URL+"/fleet/economics/scenarios", badForm); status < 400 || status >= 500 { t.Fatalf("bad scenario JSON = %d, want 4xx", status) } if _, _, err := fx.rel.Get(context.Background(), economics.ScenarioKey("launch")); !errors.Is(err, os.ErrNotExist) { t.Fatal("a bad-JSON scenario save still wrote a scenario object") } // Valid scenario + activate=1: both objects land, Active.Name is right. okForm := url.Values{"csrf": {csrf}, "name": {"launch"}, "scenario": {string(scRaw)}, "activate": {"1"}} status, loc, _ := postForm(t, c, fx.ts.URL+"/fleet/economics/scenarios", okForm) if status != http.StatusSeeOther || loc != "/fleet/economics" { t.Fatalf("valid scenario save = %d -> %q, want 303 -> /fleet/economics", status, loc) } raw, _, err := fx.rel.Get(context.Background(), economics.ScenarioKey("launch")) if err != nil { t.Fatal(err) } var saved economics.Scenario if err := json.Unmarshal(raw, &saved); err != nil { t.Fatal(err) } if saved.Name != "launch" { t.Fatalf("saved scenario name = %q, want %q (form name must win over the JSON body's)", saved.Name, "launch") } activeRaw, _, err := fx.rel.Get(context.Background(), economics.ActiveKey) if err != nil { t.Fatal(err) } var active economics.Active if err := json.Unmarshal(activeRaw, &active); err != nil { t.Fatal(err) } if active.Name != "launch" { t.Fatalf("active.Name = %q, want %q", active.Name, "launch") } } // TestAPIBillDoor: the CLI's bearer door mirrors the console form's gate // order (cliUser -> IsOperator -> byte-identical 404) and manual-write // semantics. func TestAPIBillDoor(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-apibill") body := `{"month":"2026-08","total_usd":123.45,"services":{"AmazonEC2":55.0}}` ghostStatus, ghostBody := postBody(t, fx.ts.URL+"/this-api-route-does-not-exist", token, body) status, out := postBody(t, fx.ts.URL+"/api/economics/bill", token, body) if status != http.StatusNotFound || status != ghostStatus || string(out) != string(ghostBody) { t.Fatalf("non-operator api bill = %d %q, want the same 404 as a missing route (%d %q)", status, out, ghostStatus, ghostBody) } if err := fx.st.SetOperator("paul@keymail.dev", true); err != nil { t.Fatal(err) } status, out = postBody(t, fx.ts.URL+"/api/economics/bill", token, body) if status != http.StatusOK { t.Fatalf("operator api bill = %d %s, want 200", status, out) } var b economics.Bill if err := json.Unmarshal(out, &b); err != nil { t.Fatalf("bill response: %v (%s)", err, out) } if b.Month != "2026-08" || b.TotalUSD != 123.45 || b.Source != economics.SourceManual { t.Fatalf("bill response = %+v", b) } raw, _, err := fx.rel.Get(context.Background(), economics.BillKey("2026-08")) if err != nil { t.Fatal(err) } var stored economics.Bill if err := json.Unmarshal(raw, &stored); err != nil { t.Fatal(err) } if stored.Source != economics.SourceManual || stored.TotalUSD != 123.45 || stored.Services["AmazonEC2"] != 55.0 { t.Fatalf("stored bill = %+v", stored) } }