package backup import ( "context" "io/fs" "os" "path/filepath" "strings" "testing" ) // scratchManifestPath finds the unit manifest the fixture wrote into the prepared scratch, so a test // can rewrite it. Fails loudly rather than returning "": a silently-missing manifest would make the // tests below pass for the wrong reason. func scratchManifestPath(t *testing.T, m *Manager, stack string) string { t.Helper() scratch, _, err := m.offboxRestoreScratchDir(stack) if err != nil { t.Fatalf("fixture: scratch dir: %v", err) } var found string _ = filepath.WalkDir(scratch, func(p string, d fs.DirEntry, err error) error { if err == nil && !d.IsDir() && d.Name() == "manifest.json" { found = p return fs.SkipAll } return nil }) if found == "" { t.Fatal("fixture: no unit manifest in the prepared scratch — the test would prove nothing") } return found } // R-351 SCENARIO B — THE APP IS NOT INSTALLED AND THE BACKUP NAMES WHERE IT LIVED. // // The refusal already existed (R-253) and correctly stopped the restore. What it did NOT do was say // where the data belonged, so the person on 2026-08-21 had to remember the drive and the address // themselves — both of which the backup was holding the whole time. // // WRONG OUTCOME THIS PINS: "it starts, and writes somewhere else." The assertions below are that the // restore is refused AND that the app was never touched — not merely that an error came back. func TestReconstitute_NotInstalled_RefusalNamesTheRecordedDrive(t *testing.T) { const recordedDrive = "/mnt/felhom-drives/hdd_1" m, prov, _ := reconFixture(t, "20260719T060000Z", "2026-07-19T06:00:00Z", pgDump(1)) // The backup records a drive — the one this box no longer has attached. manPath := scratchManifestPath(t, m, "immich") if err := writeManifest(manPath, &RecoveryManifest{ SchemaVersion: 2, AppName: "immich", Drive: recordedDrive, NamespaceRoot: recordedDrive, }); err != nil { t.Fatal(err) } // The app is not installed: no live HDD path. This is the rebuilt-machine shape. prov.hdd = map[string]string{} if got := prov.GetStackHDDPath("immich"); got != "" { t.Fatalf("fixture: the app must look uninstalled, got hdd=%q", got) } callsBefore := len(prov.calls) _, err := m.ReconstituteFromOffsite(context.Background(), "immich", false) if err == nil { t.Fatal("a restore with no destination must be REFUSED, not started") } if !strings.Contains(err.Error(), recordedDrive) { t.Errorf("the refusal must name the drive the backup recorded (%s); got: %v", recordedDrive, err) } // The route, not just the reason. „Alkalmaz" is the ASCII stem of „Alkalmazások" — matching the // stem keeps accented bytes out of the comparison (strict rule 7). if !strings.Contains(err.Error(), "Alkalmaz") { t.Errorf("the refusal must name a route the person can take; got: %v", err) } // THE OBSERVABLE THAT MATTERS: nothing was stopped, placed or started. A refusal that still // touched the stack would be the defect wearing an error message. if len(prov.calls) != callsBefore { t.Errorf("a refused restore must not touch the app; provider calls: %v", prov.calls[callsBefore:]) } } // R-351 SCENARIO C — THE DESTINATION DIFFERS FROM THE ONE THE BACKUP RECORDED. // // Not blocked outright (a drive can legitimately change) and not silently accepted (which is how a // restore lands in the wrong place under a green message). Named, and then the customer's own // deliberate acknowledgement carries it. func TestReconstitute_MismatchedDestination_RefusesThenProceedsOnAcknowledgement(t *testing.T) { const recordedDrive = "/mnt/felhom-drives/hdd_1" m, prov, _ := reconFixture(t, "20260719T060000Z", "2026-07-19T06:00:00Z", pgDump(1)) manPath := scratchManifestPath(t, m, "immich") if err := writeManifest(manPath, &RecoveryManifest{ SchemaVersion: 2, AppName: "immich", Drive: recordedDrive, NamespaceRoot: recordedDrive, }); err != nil { t.Fatal(err) } // The app IS installed — on a different drive from the recorded one (the fixture's temp dir). live := prov.GetStackHDDPath("immich") if live == "" || live == recordedDrive { t.Fatalf("fixture: the live drive must exist and differ from the recorded one; got %q", live) } callsBefore := len(prov.calls) _, err := m.ReconstituteFromOffsite(context.Background(), "immich", false) if err == nil { t.Fatal("a divergent destination must be NAMED and refused, never silently accepted") } for _, must := range []string{recordedDrive, live} { if !strings.Contains(err.Error(), must) { t.Errorf("the refusal must name %q so the customer can decide; got: %v", must, err) } } if len(prov.calls) != callsBefore { t.Errorf("the refusal must not touch the app; provider calls: %v", prov.calls[callsBefore:]) } // ...and the customer may go ahead deliberately. Blocking outright is the other wrong outcome. res, err := m.ReconstituteFromOffsite(context.Background(), "immich", true) if err != nil { t.Fatalf("an acknowledged placement change must proceed: %v", err) } if !res.Placement.Mismatch { t.Error("the outcome must still record that the destination differed — a success that forgets the difference reads as a match") } if res.Placement.Recorded.Drive != recordedDrive { t.Errorf("the outcome must carry the recorded drive, got %q", res.Placement.Recorded.Drive) } } // recordedPlacementFromScratch must be TOTAL: an absent scratch, an unreadable one and a manifest // with no drive all yield the zero value, never a fabricated empty path. func TestRecordedPlacementFromScratch_UnknownStaysUnknown(t *testing.T) { m := &Manager{} if got := m.recordedPlacementFromScratch(filepath.Join(t.TempDir(), "does-not-exist")); got.Known() { t.Errorf("an absent scratch must not yield a recorded placement, got %+v", got) } empty := t.TempDir() if got := m.recordedPlacementFromScratch(empty); got.Known() { t.Errorf("an empty scratch must not yield a recorded placement, got %+v", got) } noDrive := t.TempDir() if err := writeManifest(filepath.Join(noDrive, "manifest.json"), &RecoveryManifest{SchemaVersion: 1, AppName: "immich"}); err != nil { t.Fatal(err) } if got := m.recordedPlacementFromScratch(noDrive); got.Known() { t.Errorf("a manifest with no drive is an UNKNOWN, not a value; got %+v", got) } withDrive := t.TempDir() deep := filepath.Join(withDrive, "a", "b", "c", "backups", "primary", "immich") if err := os.MkdirAll(deep, 0o755); err != nil { t.Fatal(err) } if err := writeManifest(filepath.Join(deep, "manifest.json"), &RecoveryManifest{SchemaVersion: 2, AppName: "immich", Drive: "/mnt/sys_drive"}); err != nil { t.Fatal(err) } got := m.recordedPlacementFromScratch(withDrive) if !got.Known() || got.Drive != "/mnt/sys_drive" { t.Errorf("a nested unit manifest must be found, got %+v", got) } }