package backup import ( "context" "errors" "os" "path/filepath" "strings" "testing" ) // R-1214: the second-drive copy runs in a memory-capped container ONLY for paths the controller and the Docker daemon // see identically (/mnt/…); every other path, a box with no readable image, and a container that cannot start keep the // plain in-process rsync; an rsync failure inside the container is an error, never a silent second copy. // RED-PROOF: with the tier2CappedPath guard removed, TestR1214_APathOutsideMntNeverGoesToDocker fails (the copy of a // controller-volume path would go to the container, where it is an EMPTY folder and `--delete` empties the copy); // with the capped branch removed, TestR1214_MntPathsAreCopiedCapped fails — both seen failing 2026-10-22. type cappedRec struct{ calls [][]string } func withSeams(t *testing.T, root string, img string, run func(args []string) ([]byte, int, error)) *cappedRec { t.Helper() rec := &cappedRec{} oldRoot, oldImg, oldRun, oldLog, oldDocker := tier2CappedRoot, tier2SelfImage, tier2RunCapped, tier2Logf, tier2Docker // The copy's lifecycle commands (rm -f by name) never reach Docker from a test (they would act on the host). tier2Docker = func(context.Context, ...string) ([]byte, error) { return nil, nil } tier2RetryOff = true tier2CappedRoot = root tier2SelfImage = func() string { return img } tier2RunCapped = func(_ context.Context, args []string) ([]byte, int, error) { rec.calls = append(rec.calls, args) return run(args) } tier2Logf = func(string, ...any) {} t.Cleanup(func() { tier2CappedRoot, tier2SelfImage, tier2RunCapped, tier2Logf, tier2Docker = oldRoot, oldImg, oldRun, oldLog, oldDocker tier2RetryOff = false }) return rec } func TestR1214_MntPathsAreCopiedCapped(t *testing.T) { root := t.TempDir() + "/" src, dst := filepath.Join(root, "drive1", "unit"), filepath.Join(root, "drive2", "unit") os.MkdirAll(src, 0755) rec := withSeams(t, root, "gitea.dooplex.hu/admin/felhom-controller:9.9.9", func([]string) ([]byte, int, error) { return nil, 0, nil }) if err := rsyncMirror(src, dst); err != nil { t.Fatal(err) } if len(rec.calls) != 1 { t.Fatalf("docker calls = %d, want 1", len(rec.calls)) } got := strings.Join(rec.calls[0], " ") for _, want := range []string{"run --rm", "--memory 512m", "--memory-swap 512m", "--network none", "--pull never", "--no-healthcheck", "--mount type=bind,src=" + src + ",dst=" + src + ",readonly", "--mount type=bind,src=" + dst + ",dst=" + dst + " ", "--entrypoint rsync gitea.dooplex.hu/admin/felhom-controller:9.9.9 -a --delete " + src + "/ " + dst + "/"} { if !strings.Contains(got, want) { t.Fatalf("the capped copy lacks %q:\n%s", want, got) } } } func TestR1214_APathOutsideMntNeverGoesToDocker(t *testing.T) { root := t.TempDir() + "/" other := t.TempDir() // a path the daemon would see differently (the controller's own data volume) src, dst := filepath.Join(other, "unit"), filepath.Join(root, "drive2", "unit") os.MkdirAll(src, 0755) os.WriteFile(filepath.Join(src, "photo.jpg"), []byte("x"), 0644) rec := withSeams(t, root, "img", func([]string) ([]byte, int, error) { return nil, 0, nil }) if err := rsyncMirror(src, dst); err != nil { t.Fatal(err) } if len(rec.calls) != 0 { t.Fatalf("a path outside %s went to the container: %v", root, rec.calls) } if _, err := os.Stat(filepath.Join(dst, "photo.jpg")); err != nil { t.Fatal("the plain copy did not run") } } func TestR1214_ANotStartedContainerFallsBackAnRsyncErrorDoesNot(t *testing.T) { root := t.TempDir() + "/" src, dst := filepath.Join(root, "a", "unit"), filepath.Join(root, "b", "unit") os.MkdirAll(src, 0755) os.WriteFile(filepath.Join(src, "f"), []byte("x"), 0644) withSeams(t, root, "img", func([]string) ([]byte, int, error) { return []byte("no such image"), 125, errors.New("exit 125") }) if err := rsyncMirror(src, dst); err != nil { t.Fatalf("a container that cannot start must fall back to the plain copy: %v", err) } if _, err := os.Stat(filepath.Join(dst, "f")); err != nil { t.Fatal("the fallback copy did not run") } withSeams(t, root, "img", func([]string) ([]byte, int, error) { return []byte("rsync error"), 23, errors.New("exit 23") }) if err := rsyncMirror(src, dst); err == nil { t.Fatal("an rsync failure inside the container must be an error") } withSeams(t, root, "", func([]string) ([]byte, int, error) { t.Fatal("no image → no container"); return nil, 0, nil }) if err := rsyncMirror(src, dst); err != nil { t.Fatalf("no readable image → the plain copy: %v", err) } } // A symlink under the capped root that points elsewhere must never reach the container (the daemon would resolve it // somewhere else, and `--delete` would mirror that over the copy) — the security review's case. // RED-PROOF: with the EvalSymlinks check removed, this fails — seen failing 2026-10-22. func TestR1214_ASymlinkedPathNeverGoesToDocker(t *testing.T) { root := t.TempDir() + "/" elsewhere := t.TempDir() os.WriteFile(filepath.Join(elsewhere, "f"), []byte("x"), 0644) link := filepath.Join(root, "drive1", "unit") os.MkdirAll(filepath.Dir(link), 0755) if err := os.Symlink(elsewhere, link); err != nil { t.Fatal(err) } rec := withSeams(t, root, "img", func([]string) ([]byte, int, error) { return nil, 0, nil }) if err := rsyncMirror(link, filepath.Join(root, "drive2", "unit")); err != nil { t.Fatal(err) } if len(rec.calls) != 0 { t.Fatalf("a symlinked path went to the container: %v", rec.calls) } }