package web import ( "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" "gitea.dooplex.hu/admin/felhom-controller/internal/backup" ) // fileLegProvider declares a MANDATORY drive-side bind for one app and nothing for the others — the // shape of a class-A app (calibre-web, immich, nextcloud, paperless-ngx) against the 45 whose data // lives entirely in Docker volumes. type fileLegProvider struct { blockProvider withLegs string } func (p *fileLegProvider) GetStackComposePath(string) (string, bool) { return "compose", true } func (p *fileLegProvider) GetStackClassifiedBinds(n string) ([]backup.ClassifiedBind, bool) { if n != p.withLegs { return nil, false } return []backup.ClassifiedBind{{ ComposeBind: appbackup.ComposeBind{Root: appbackup.RootHDD, RelPath: "appdata/" + n}, Class: appbackup.ClassMandatory, }}, true } // R-537 — the backup page must describe what each TIER captured, not what the app is shaped like. // // The defect this pins, measured live on 2026-09-16 on a fresh box: the Tier-1 row read // „DB + Konfig + Adatok" for Nextcloud, over a unit that held a database dump, three volume tars and // no copy of the customer's files at all. The five photos in that folder were in no backup on the // box, and the page said they were. // // Red-proof: put the old single label back (`if app.HasHDDData || app.HasVolumeData → "Adatok"`, // rendered on all three tier rows) → the Tier-1 assertion fails. func TestAppBackupRows_Tier1LabelDoesNotClaimFilesItCannotHold(t *testing.T) { s, _, m := newOffboxWebServer(t) drive := t.TempDir() m.SetStackProvider(&fileLegProvider{blockProvider: blockProvider{hdd: drive}, withLegs: "nextcloud"}) rows := s.buildAppBackupRows(&backup.FullBackupStatus{AppDataInfo: []backup.AppBackupInfo{ {StackName: "nextcloud", DisplayName: "Nextcloud", HasHDDData: true, HasVolumeData: true}, {StackName: "privatebin", DisplayName: "PrivateBin", HasVolumeData: true}, }}, "hu") nc := findRow(rows, "nextcloud") if nc == nil { t.Fatal("no row for nextcloud") } if strings.Contains(nc.Tier1Contents, "Adatok") { t.Fatalf("the Tier-1 label claims it holds the app's data: %q — the unit has no file leg and the customer's files are on the drive", nc.Tier1Contents) } if !strings.Contains(nc.Tier23Contents, "Adatok") { t.Fatalf("Tier 2/3 DO carry the file legs; their label must say so: %q", nc.Tier23Contents) } if nc.DriveFilesNote == "" { t.Fatal("an app whose files a local backup cannot hold must be told where they ARE protected") } // NEGATIVE CONTROL: an app whose data really is inside the volumes the unit captured keeps its // „Adatok". Without this, "never say Adatok" would pass and would be a different lie. pb := findRow(rows, "privatebin") if pb == nil || !strings.Contains(pb.Tier1Contents, "Adatok") { t.Fatalf("an app whose data IS in the captured volumes must keep its Adatok label: %+v", pb) } if pb.DriveFilesNote != "" { t.Fatalf("an app with no drive-side files needs no note about them: %q", pb.DriveFilesNote) } }