Files
felhom-controller/controller/internal/backup/r640_incomplete_dump_test.go
T
admin 80e6ad8c47
gates / gates (push) Successful in 26s
controller v0.267.0: tests off DooPlex's Docker, cut-off copies refused, two pages true
R-650: internal/dockerexec — every docker exec routed through it; under
go test a real docker is refused (opt-in FELHOM_TEST_REAL_DOCKER=1; a stub
under the temp dir is allowed). api/stacks/web tests run under a silent
stub (TestMain). TestR650_NoBareDockerExec pins it repo-wide.
R-640: a dump without its engine's completion marker is refused before
the first mutation (unit + off-site restore) and again before any load.
R-499: the Tier-2 page's system-disk sentence has four true branches.
R-518: the backup button states the measured ~8 min stop.
R-626: measured on 9202, not reproduced.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-23 20:25:28 +02:00

115 lines
5.0 KiB
Go

package backup
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
// R-640 — a cut-off database copy must never be loaded. Measured 2026-09-23 on 9202: the first half
// of a real pg_dump loaded with rc 0 into an EMPTY database (42 tables, 0 users). Every test here
// asserts the CONSEQUENCE — nothing was loaded, nothing was stopped — not only that an error came back.
// truncatedPG is pgDump cut off inside its COPY block: header and CREATE TABLEs present, so
// ValidateDump's header + table checks pass it; only the missing end marker betrays it.
func truncatedPG() string {
full := pgDump(50)
return full[:strings.Index(full, "\\.\n")]
}
// COMPANION RED-PROOF: deleting the CheckDumpComplete call in reimportDBDumpsFrom makes this fail on
// `a cut-off copy was LOADED`.
func TestR640_ReplayRefusesACutOffCopyAndLoadsNothing(t *testing.T) {
m := newReimportTestManager()
ns := t.TempDir()
p := writeDump(t, ns, "docmost", DBType("postgres"))
if err := os.WriteFile(p, []byte(truncatedPG()), 0o644); err != nil {
t.Fatal(err)
}
if v := ValidateDump(p, DBType("postgres")); !v.Valid {
t.Fatalf("precondition: the truncated copy must PASS the old structural check (that is the bug), got %+v", v)
}
m.discoverDBs = func(context.Context) ([]DiscoveredDB, error) {
return []DiscoveredDB{{StackName: "docmost", ContainerName: "docmost-postgres", DBType: DBType("postgres")}}, nil
}
var loaded []string
m.importDBDump = func(_ context.Context, _ DiscoveredDB, path string) error { loaded = append(loaded, path); return nil }
n, err := m.reimportDBDumps(context.Background(), "docmost", ns)
if len(loaded) != 0 {
t.Fatalf("a cut-off copy was LOADED: %v", loaded)
}
if err == nil || n != 0 || !strings.Contains(err.Error(), "csonka") {
t.Fatalf("want the Hungarian cut-off refusal and 0 replayed, got n=%d err=%v", n, err)
}
}
// The control: the same path with the complete copy loads it — the check refuses nothing whole.
func TestR640_ReplayLoadsACompleteCopy(t *testing.T) {
m := newReimportTestManager()
ns := t.TempDir()
p := writeDump(t, ns, "docmost", DBType("postgres"))
if err := os.WriteFile(p, []byte(pgDump(50)), 0o644); err != nil {
t.Fatal(err)
}
m.discoverDBs = func(context.Context) ([]DiscoveredDB, error) {
return []DiscoveredDB{{StackName: "docmost", ContainerName: "docmost-postgres", DBType: DBType("postgres")}}, nil
}
var loaded []string
m.importDBDump = func(_ context.Context, _ DiscoveredDB, path string) error { loaded = append(loaded, path); return nil }
if n, err := m.reimportDBDumps(context.Background(), "docmost", ns); err != nil || n != 1 || len(loaded) != 1 {
t.Fatalf("a complete copy must load: n=%d err=%v loaded=%v", n, err, loaded)
}
}
// The local unit restore refuses BEFORE the first mutation: no stop, no volume replay, no definition.
// COMPANION RED-PROOF: deleting the incompleteDumps gate in RestoreFromRecoveryUnit makes this fail
// on `the app was stopped`.
func TestR640_UnitRestoreRefusesACutOffCopyBeforeAnyMutation(t *testing.T) {
m, prov, imported := r47UnitFixture(t, immichLikeCompose, true)
drive := prov.hdd
mustWrite(t, filepath.Join(AppDBDumpPath(drive, "app"), "app-postgres.sql"), truncatedPG())
_, err := m.RestoreFromRecoveryUnit("app")
if err == nil || !strings.Contains(err.Error(), "csonka") {
t.Fatalf("want the cut-off refusal, got %v", err)
}
if prov.stopped || len(prov.calls) != 0 || prov.gotEnv != nil {
t.Fatalf("ZERO mutations required: stopped=%v calls=%v definition=%v", prov.stopped, prov.calls, prov.gotEnv != nil)
}
if len(*imported) != 0 {
t.Fatalf("a replay happened: %v", *imported)
}
}
// The off-site restore refuses before the safety dump, the stop and the file copy.
func TestR640_OffsiteRestoreRefusesACutOffCopyBeforeAnyMutation(t *testing.T) {
m, prov, imported := reconFixture(t, "run1", "2026-07-19T06:00:00Z", truncatedPG())
var copied bool
m.SetOffboxFullPlaceCopier(func(_, _ string) (int, error) { copied = true; return 1, nil })
_, err := m.ReconstituteFromOffsite(context.Background(), "immich", false)
if err == nil || !strings.Contains(err.Error(), "csonka") {
t.Fatalf("want the cut-off refusal, got %v", err)
}
if len(prov.calls) != 0 || copied || len(*imported) != 0 {
t.Fatalf("ZERO mutations required: calls=%v copied=%v imported=%v", prov.calls, copied, *imported)
}
}
// A MariaDB copy is judged by its own marker, not PostgreSQL's.
func TestR640_MariaDBCopyNeedsItsOwnMarker(t *testing.T) {
dir := t.TempDir()
good := filepath.Join(dir, "romm-mariadb.sql")
mustWrite(t, good, "-- MariaDB dump 10.19\nCREATE TABLE `users` (id int);\nINSERT INTO `users` VALUES (1);\n-- Dump completed on 2026-09-23 21:00:00\n")
if bad := incompleteDumps(dir); len(bad) != 0 {
t.Fatalf("a complete MariaDB copy was flagged: %v", bad)
}
mustWrite(t, good, "-- MariaDB dump 10.19\nCREATE TABLE `users` (id int);\nINSERT INTO `users` VALUES (1);\n-- PostgreSQL database dump complete\n")
if bad := incompleteDumps(dir); len(bad) != 1 {
t.Fatalf("a MariaDB copy carrying the WRONG engine's marker must be flagged, got %v", bad)
}
}