R-102 phase 1.1: split the recovery-unit path helpers, zero behaviour change
Every unit path helper took (nsRoot, stackName) and joined backups/primary/<stack>/... . That hard-coded 'primary' is the mechanism of R-102: Tier-2 mirrors the whole unit directory to <dest>/backups/secondary/<stack>/recovery-unit/ every night, and because no reader could NAME a unit outside backups/primary/, that mirror has been captured for months and read by nothing. Adds four unit-directory-relative primitives - UnitComposeDir, UnitManifestFile, UnitDBDumpDir, UnitVolumeDumpDir - each taking the recovery-unit DIRECTORY itself. The four existing (nsRoot, stackName) helpers become thin wrappers over them and keep their exact signatures and their exact return values; every current caller compiles untouched. ONE implementation, two callers - the rule restoreDockerVolumesFrom already follows in this repo. TestR102_PathWrappersAreByteIdenticalToToday pins the wrappers against hand-written literals (not re-derived from the helpers under test). Red-proof: UnitComposeDir join changed to 'compose2' -> the test fails on all three fixtures.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package appbackup
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A1 — TestR102_PathWrappersAreByteIdenticalToToday.
|
||||
//
|
||||
// R-102 Part 1.1 split each unit path helper into a unit-directory-relative primitive plus a thin
|
||||
// `(nsRoot, stackName)` wrapper. The refactor's whole claim is ZERO behaviour change, and the only
|
||||
// honest way to pin that is to compare each wrapper against the literal string it produced before the
|
||||
// split — written out here by hand, not re-derived from the helpers under test (a test that called
|
||||
// UnitComposeDir to compute its own expectation would pass for any consistent pair of wrong joins).
|
||||
//
|
||||
// Red-proof (recorded in REPORT.md): change one wrapper's join — e.g. UnitComposeDir to "compose2" —
|
||||
// and this test fails.
|
||||
func TestR102_PathWrappersAreByteIdenticalToToday(t *testing.T) {
|
||||
cases := []struct {
|
||||
nsRoot string
|
||||
stack string
|
||||
}{
|
||||
{"/mnt/hdd1", "docmost"},
|
||||
{"/mnt/sys_drive/felhom-data", "immich"},
|
||||
{"/mnt/felhom-usb", "paperless-ngx"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
unit := c.nsRoot + "/backups/primary/" + c.stack
|
||||
|
||||
if got, want := RecoveryUnitPath(c.nsRoot, c.stack), unit; got != want {
|
||||
t.Errorf("RecoveryUnitPath(%q,%q) = %q, want %q", c.nsRoot, c.stack, got, want)
|
||||
}
|
||||
if got, want := RecoveryUnitComposePath(c.nsRoot, c.stack), unit+"/compose"; got != want {
|
||||
t.Errorf("RecoveryUnitComposePath(%q,%q) = %q, want %q", c.nsRoot, c.stack, got, want)
|
||||
}
|
||||
if got, want := RecoveryUnitManifestPath(c.nsRoot, c.stack), unit+"/manifest.json"; got != want {
|
||||
t.Errorf("RecoveryUnitManifestPath(%q,%q) = %q, want %q", c.nsRoot, c.stack, got, want)
|
||||
}
|
||||
if got, want := AppDBDumpPath(c.nsRoot, c.stack), unit+"/db-dumps"; got != want {
|
||||
t.Errorf("AppDBDumpPath(%q,%q) = %q, want %q", c.nsRoot, c.stack, got, want)
|
||||
}
|
||||
if got, want := AppVolumeDumpPath(c.nsRoot, c.stack), unit+"/volume-dumps"; got != want {
|
||||
t.Errorf("AppVolumeDumpPath(%q,%q) = %q, want %q", c.nsRoot, c.stack, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestR102_UnitHelpersAreDirectoryRelative pins the OTHER half of the contract: the four primitives
|
||||
// join onto whatever directory they are handed, with no `primary` segment reintroduced. The secondary
|
||||
// mirror path below is the exact shape Tier-2 writes (tier2.go: `<dest>/backups/secondary/<stack>/
|
||||
// recovery-unit`), so a regression that re-derived a namespace root would show up here as a `primary`
|
||||
// appearing in a secondary path.
|
||||
func TestR102_UnitHelpersAreDirectoryRelative(t *testing.T) {
|
||||
unit := "/mnt/hdd2/backups/secondary/docmost/recovery-unit"
|
||||
checks := []struct {
|
||||
name string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{"UnitComposeDir", UnitComposeDir(unit), unit + "/compose"},
|
||||
{"UnitManifestFile", UnitManifestFile(unit), unit + "/manifest.json"},
|
||||
{"UnitDBDumpDir", UnitDBDumpDir(unit), unit + "/db-dumps"},
|
||||
{"UnitVolumeDumpDir", UnitVolumeDumpDir(unit), unit + "/volume-dumps"},
|
||||
}
|
||||
for _, c := range checks {
|
||||
if c.got != c.want {
|
||||
t.Errorf("%s(%q) = %q, want %q", c.name, unit, c.got, c.want)
|
||||
}
|
||||
if filepath.Base(filepath.Dir(c.got)) == "primary" {
|
||||
t.Errorf("%s reintroduced a primary segment: %q", c.name, c.got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user