package web import ( "strings" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // ── R-331 — the Backup card said "Snapshots 0" over a working backup ───────────────────────────── // // Measured on `demo-hp` 2026-08-30: the card read `Snapshots 0 · Repo Size 0 MB · Integrity Unknown` // while the box's own settings held `snapshot_count: 67, repo_size_bytes: 140829678, stats_known: // true` and that night's log said `[offbox] backup OK: 8 app(s) backed up, 67 snapshot(s), 2m14s`. // The card rendered the report's `backup` object, whose snapshot/size/integrity fields have had no // producer since disk-tier restic moved to the host agent; the live numbers were in the `offsite` // object all along. // // These tests use demo-hp's REAL reported values, so a regression fails against the same numbers the // defect was measured against rather than against invented ones. // demoHPReportJSON is the shape demo-hp actually sends (controller 0.225.0), trimmed to the objects // the Backup card reads. const demoHPReportJSON = `{ "backup": {"enabled": true, "last_db_dump": "2026-08-30T02:15:01Z"}, "offsite": {"enabled": true, "escrow_state": "escrowed", "last_status": "ok", "last_success": "2026-08-30T02:17:19Z", "snapshot_count": 67, "repo_size_bytes": 140829678, "quota_gb": 50, "stats_known": true} }` func renderWithReport(t *testing.T, reportJSON string) string { t.Helper() s, st := newTestServer(t) if err := st.SaveCustomerConfig(&store.CustomerConfig{ CustomerID: "demo-hp", CustomerName: "Demo HP", Domain: "enkisfelhom.hu", RetrievalPassword: "pw", APIKey: "k", Status: "active", }); err != nil { t.Fatal(err) } if err := st.SaveReport("demo-hp", []byte(reportJSON)); err != nil { t.Fatal(err) } return renderCustomerPage(t, s, "demo-hp") } // THE CONSEQUENCE TEST. Not "does reportBackupCard return 67" — does the page an operator actually // looks at contain 67. The defect lived in the template's choice of source object, so a test one // layer below it would have been green against the shipped bug. // // RED-PROOF (run 2026-08-30, recorded in REPORT.md): point the card back at `.Report.backup` → // this fails with the rendered page carrying `Snapshots 0` and no `67`. func TestBackupCard_RendersTheRealSnapshotCount(t *testing.T) { html := renderWithReport(t, demoHPReportJSON) if !strings.Contains(html, ">67<") { t.Error("the rendered Backup card does not contain demo-hp's real snapshot count (67) — " + "this is the defect: an operator reading this page concludes the customer has no backups") } if !strings.Contains(html, "134.3 MB") { t.Error("the card does not carry the real repository size (134.3 MB from 140829678 bytes)") } if strings.Contains(html, "Repo Size 0 MB") || strings.Contains(html, ">0 MB<") { t.Error("the card still renders the dead `repo_size_mb` field as 0 MB") } // `integrity_ok` has no producer anywhere in the controller — NotifyIntegrityOK and // NotifyIntegrityFailed exist and are called from nowhere. A row that always reads "Unknown" is // not information, and one that could read "OK" from an unwritten field would be a lie. if strings.Contains(html, "Integrity") { t.Error("the card still shows an Integrity row — nothing in the controller produces it") } } // The three-way ruling, which is the reason this card needed Go and not a template {{if}}. func TestBackupCard_ThreeWayRuling(t *testing.T) { t.Run("never measured shows a dash, never 0", func(t *testing.T) { // A box with an off-site tier that has never read its repository. Reporting 0 here would // claim the repository is EMPTY, which is not what is known — R-225's exact defect, which // was measured live over a store that really held snapshot f3d9cd67. html := renderWithReport(t, `{"backup":{"enabled":true}, "offsite":{"enabled":true,"last_status":"ok","snapshot_count":0,"repo_size_bytes":0}}`) if !strings.Contains(html, "never been") { t.Error("an unmeasured repository is not called out as unmeasured — the operator reads " + "the dash as a formatting quirk rather than as missing knowledge") } if strings.Contains(html, ">0<") { t.Error("an unmeasured repository rendered a literal 0 snapshot count — zero is a claim " + "about the repository's contents and nothing here justifies making it") } }) t.Run("measured empty is stated, not hidden", func(t *testing.T) { // The opposite news, and it must be sayable: the repository was read and really holds // nothing. If this rendered a dash too, the field would be pointless. html := renderWithReport(t, `{"backup":{"enabled":true}, "offsite":{"enabled":true,"last_status":"ok","snapshot_count":0,"repo_size_bytes":0, "stats_known":true}}`) if strings.Contains(html, "never been") { t.Error("a MEASURED empty repository was reported as unmeasured — the operator never " + "learns that a customer genuinely has zero off-site snapshots, which is an alarm") } if !strings.Contains(html, ">0<") { t.Error("a measured-empty repository did not render its 0 — measured zero is knowledge " + "and must be shown") } }) t.Run("no offsite object says so instead of showing numbers", func(t *testing.T) { html := renderWithReport(t, `{"backup":{"enabled":true}}`) if !strings.Contains(html, "No off-site data reported") { t.Error("a report with no `offsite` object did not say so — the same rule offsiteBoxTile " + "already follows: absent, never a zeroed tile") } if strings.Contains(html, "Off-site snapshots") { t.Error("numbers were rendered for a customer whose controller has never reported any") } }) t.Run("a disabled tier with a declared state names the blocker", func(t *testing.T) { html := renderWithReport(t, `{"backup":{"enabled":true}, "offsite":{"enabled":false,"state":"needs_credential"}}`) if !strings.Contains(html, "needs_credential") { t.Error("the declared state was dropped — 'off-site not enabled' and 'off-site blocked " + "waiting for a credential' are different operator actions and must read differently") } }) } // A pre-v0.225.0 controller sends no `stats_known` key. Absence must degrade to UNKNOWN, never to // "empty" — otherwise upgrading the hub before the fleet would tell the operator that every // un-upgraded customer has zero backups. func TestBackupCard_OldControllerDegradesToUnknownNotEmpty(t *testing.T) { html := renderWithReport(t, `{"backup":{"enabled":true}, "offsite":{"enabled":true,"last_status":"ok","last_success":"2026-08-30T02:17:19Z", "snapshot_count":67,"repo_size_bytes":140829678,"quota_gb":50}}`) if !strings.Contains(html, "never been") { t.Error("a report without stats_known was treated as authoritative — the hub cannot know " + "whether an old controller's counts were ever read, and must say so") } // The rest of the card must still work: last successful run is independent of stats_known. if !strings.Contains(html, "Last successful run") { t.Error("the whole off-site section vanished for an old controller") } } func TestFmtBytesAuto_ScalesInsteadOfCollapsingToZero(t *testing.T) { // The Offsite page's fmtBytesGB renders demo-hp's real size as "0.1 GB". On a card whose entire // defect was under-reporting a real backup, that reads as "nearly nothing" — which is why this // formatter exists rather than reusing that one. for _, tc := range []struct{ in int64; want string }{ {140829678, "134.3 MB"}, // demo-hp, measured {0, "0 B"}, {1 << 10, "1.0 KB"}, {1 << 20, "1.0 MB"}, {1 << 30, "1.0 GB"}, {1 << 40, "1.00 TB"}, } { if got := fmtBytesAuto(tc.in); got != tc.want { t.Errorf("fmtBytesAuto(%d) = %q, want %q", tc.in, got, tc.want) } } }