Files
felhom.eu/hub/internal/web/r331_backup_card_test.go
T

166 lines
7.7 KiB
Go

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)
}
}
}