Files
felhom-controller/controller/internal/backup/r1175_shares_kept_test.go
T

169 lines
6.2 KiB
Go

package backup
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// R-1175 + D25 (operator ruling 2026-10-21), scenario B. The first two tests are the data-loss hunt's C3 cases
// (felhom.eu audits/dataloss-hunt-2026-10-20/C3-D1-shares-tier2/), now asserting instead of logging.
func sharesCopy(env *sharesEnv, target, srcDrive, share string) string {
return filepath.Join(NamespaceRoot(env.drives[target], true), "backups", "secondary", SharesPseudoStack,
sharesDriveKey(env.drives[srcDrive]), share)
}
func mustExist(t *testing.T, p, why string) {
t.Helper()
if _, err := os.Stat(p); err != nil {
t.Fatalf("%s: %s is gone (%v)", why, p, err)
}
}
// Shares on hdd_2 and hdd_3 both copy to hdd_1: after EVERY run both groups' copies exist. Before the fix each group's
// reconcile walked every drive key and removed the other group's copy.
func TestR1175_TwoDrivesSharingOneTargetKeepBothCopies(t *testing.T) {
env := newSharesEnv(t, "hdd_1", "hdd_2", "hdd_3")
env.addShare(t, "hdd_2", "b_share", true)
env.addShare(t, "hdd_3", "c_share", true)
for run := 1; run <= 2; run++ {
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
mustExist(t, filepath.Join(sharesCopy(env, "hdd_1", "hdd_2", "b_share"), "b_share.txt"), "run "+string(rune('0'+run))+": hdd_2's share copy")
mustExist(t, filepath.Join(sharesCopy(env, "hdd_1", "hdd_3", "c_share"), "c_share.txt"), "run "+string(rune('0'+run))+": hdd_3's share copy")
}
}
// The night a drive is away its copy is NOT deleted: it stays, and it is listed as kept (drive_away).
func TestR1175_AnAwayDrivesShareCopyIsKeptAndListed(t *testing.T) {
env := newSharesEnv(t, "hdd_1", "hdd_2", "hdd_3")
env.addShare(t, "hdd_2", "b_share", true)
env.addShare(t, "hdd_3", "csaladi_fotok", true)
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
if err := env.sett.SetDisconnected(env.drives["hdd_3"], true, nil); err != nil {
t.Fatal(err)
}
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
cp := sharesCopy(env, "hdd_1", "hdd_3", "csaladi_fotok")
mustExist(t, filepath.Join(cp, "csaladi_fotok.txt"), "the away drive's only second copy")
kept := env.m.KeptCopies()
if len(kept) != 1 || kept[0].Source != "csaladi_fotok" || kept[0].Reason != "drive_away" || kept[0].Path != filepath.Clean(cp) {
t.Fatalf("kept copies = %+v, want csaladi_fotok (drive_away) at %s", kept, cp)
}
// The drive comes back: the copy is live again and its record goes.
if err := env.sett.SetDisconnected(env.drives["hdd_3"], false, nil); err != nil {
t.Fatal(err)
}
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
if k := env.m.KeptCopies(); len(k) != 0 {
t.Fatalf("after the drive came back, kept copies = %+v, want none", k)
}
}
// A registered share whose FOLDER is missing (drive present) keeps its copy; a share the household DELETED is pruned.
func TestR1175_MissingFolderKeptDeletedSharePruned(t *testing.T) {
env := newSharesEnv(t, "hdd_1", "hdd_2")
gone := env.addShare(t, "hdd_1", "eltunt", true)
env.addShare(t, "hdd_1", "torolt", true)
env.addShare(t, "hdd_1", "marad", true)
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
if err := os.RemoveAll(gone); err != nil {
t.Fatal(err)
}
if err := env.sett.RemoveSMBShare("torolt"); err != nil {
t.Fatal(err)
}
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
mustExist(t, filepath.Join(sharesCopy(env, "hdd_2", "hdd_1", "eltunt"), "eltunt.txt"), "a missing folder's copy")
if _, err := os.Stat(sharesCopy(env, "hdd_2", "hdd_1", "torolt")); !os.IsNotExist(err) {
t.Fatalf("a deleted share's copy was not pruned (stat err %v)", err)
}
if k := env.m.KeptCopies(); len(k) != 1 || k[0].Source != "eltunt" || k[0].Reason != "folder_missing" {
t.Fatalf("kept copies = %+v, want eltunt (folder_missing)", k)
}
}
// An EMPTIED present folder is not mirrored over a copy that holds files: the copy stays and the run warns.
func TestR1181_AnEmptiedShareDoesNotEmptyItsCopy(t *testing.T) {
env := newSharesEnv(t, "hdd_1", "hdd_2")
if _, err := exec.LookPath("rsync"); err == nil {
env.m.tier2Mirror = rsyncMirror // the production mirror: `rsync -a --delete` is what empties a copy
}
p := env.addShare(t, "hdd_1", "fotok", true)
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
if err := os.Remove(filepath.Join(p, "fotok.txt")); err != nil {
t.Fatal(err)
}
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
mustExist(t, filepath.Join(sharesCopy(env, "hdd_2", "hdd_1", "fotok"), "fotok.txt"), "an emptied folder's copy")
st := env.m.SharesTier2Status()
if st == nil || !strings.Contains(st.LastWarning, "fotok") {
t.Fatalf("the run did not warn about the emptied folder: %+v", st)
}
}
// „Eltávolítás" removes ONE kept copy by its record, and refuses a directory that is not the recorded one.
func TestD25_RemovingAKeptCopyIsByRecordAndIdentity(t *testing.T) {
env := newSharesEnv(t, "hdd_1", "hdd_2", "hdd_3")
env.addShare(t, "hdd_2", "b_share", true)
env.addShare(t, "hdd_3", "c_share", true)
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
if err := env.sett.SetDecommissioned(env.drives["hdd_3"], ""); err != nil {
t.Fatal(err)
}
if err := env.m.RunSharesTier2(); err != nil {
t.Fatal(err)
}
k := env.m.KeptCopies()
if len(k) != 1 || k[0].Reason == "" {
t.Fatalf("kept = %+v", k)
}
// Swap the directory for another one at the same path: same name, different identity → refused, nothing removed.
cp := k[0].Path
if err := os.Rename(cp, cp+".old"); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(cp, 0o755); err != nil {
t.Fatal(err)
}
if err := env.m.RemoveKeptCopy(k[0].ID); err == nil {
t.Fatal("a directory that is not the recorded one was removed")
}
mustExist(t, cp, "the refused removal")
// Put the recorded one back → removed, and only it.
_ = os.Remove(cp)
if err := os.Rename(cp+".old", cp); err != nil {
t.Fatal(err)
}
if err := env.m.RemoveKeptCopy(k[0].ID); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(cp); !os.IsNotExist(err) {
t.Fatalf("the kept copy is still there: %v", err)
}
mustExist(t, filepath.Join(sharesCopy(env, "hdd_1", "hdd_2", "b_share"), "b_share.txt"), "the other drive's copy")
if len(env.m.KeptCopies()) != 0 {
t.Fatal("the record outlived the removal")
}
}