package backup import ( "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // felhom.eu architecture/16-storage-physical-identity.md §4 — the agent's verdict decides Tier-2 and the // one-copy notice. Each test asserts the CONSEQUENCE (is a target picked, does it count, what does the page // say), not the mechanism. // judgeBy answers per target path; paths not in the map are "unmapped" (→ the old st_dev rule). func judgeBy(m map[string]DiskVerdict) func(a, b string) (DiskVerdict, bool) { return func(a, b string) (DiskVerdict, bool) { if v, ok := m[b]; ok { return v, true } return DiskVerdict{Reason: "unmapped"}, true } } var ( provenDifferent = DiskVerdict{Different: true, Proven: true} provenSame = DiskVerdict{Different: false, Proven: true, Reason: "same_disk"} unknownVM = DiskVerdict{Different: true, Proven: false, Reason: "vm"} ) func addDrive(t *testing.T, sett *settings.Settings, sp settings.StoragePath) { t.Helper() sp.Schedulable = true if err := sett.AddStoragePath(sp); err != nil { t.Fatal(err) } } // Row 1: different + proven → made, counted, notice hidden. func TestDisk16_ProvenDifferent_CountedNoNotice(t *testing.T) { m, sett := newTestManager(t, "/srv/sys") addDrive(t, sett, settings.StoragePath{Path: "/mnt/b", Label: "B"}) m.SetDiskJudge(judgeBy(map[string]DiskVerdict{"/mnt/b": provenDifferent})) tg, err := m.selectTier2TargetFrom("app", "/srv/sys", 1, 1) if err != nil || tg.Unproven || tg.NamespaceRoot != "/mnt/b" { t.Fatalf("want /mnt/b counted, got %+v err=%v", tg, err) } if w := m.singleCopyWarning(); w != "" { t.Fatalf("a proven second disk must hide the notice, got %q", w) } } // Row 2: same disk (two partitions) → NOT made (D1), notice shown — even though st_dev would say "different". func TestDisk16_SameDisk_NotMadeAndNoticeShown(t *testing.T) { m, sett := newTestManager(t, "/srv/sys") addDrive(t, sett, settings.StoragePath{Path: "/mnt/part2", Label: "P2"}) m.SetDiskJudge(func(a, b string) (DiskVerdict, bool) { return provenSame, true }) if tg, err := m.selectTier2TargetFrom("app", "/mnt/part1", 1, 1); err == nil { t.Fatalf("a same-disk partition must not be a Tier-2 target, got %+v", tg) } if w := m.singleCopyWarning(); w != m.note("note.backup.single_copy") { t.Fatalf("same disk: want the one-copy notice, got %q", w) } } // Row 3: unknown (a VM) → made, NOT counted (D2), notice shown with the "cannot check" sentence. func TestDisk16_Unknown_MadeNotCountedCannotCheck(t *testing.T) { m, sett := newTestManager(t, "/srv/sys") addDrive(t, sett, settings.StoragePath{Path: "/mnt/felhom-drives/media_a", Label: "A"}) m.SetDiskJudge(func(a, b string) (DiskVerdict, bool) { return unknownVM, true }) tg, err := m.selectTier2TargetFrom("app", "/mnt/felhom-drives/adatlemez", 1, 1) if err != nil || !tg.Unproven || tg.UnprovenReason != "vm" { t.Fatalf("unknown: want the copy made and marked unproven, got %+v err=%v", tg, err) } w := m.singleCopyWarning() if w != m.note("note.backup.single_copy_unproven") || !strings.Contains(w, "nem tudja ellenőrizni") { t.Fatalf("unknown: want the cannot-check notice, got %q", w) } // A proven-different drive beats an unknown one. addDrive(t, sett, settings.StoragePath{Path: "/mnt/felhom-drives/real", Label: "R"}) m.SetDiskJudge(judgeBy(map[string]DiskVerdict{"/mnt/felhom-drives/media_a": unknownVM, "/mnt/felhom-drives/real": provenDifferent})) if tg, _ := m.selectTier2TargetFrom("app", "/mnt/felhom-drives/adatlemez", 1, 1); tg == nil || tg.NamespaceRoot != "/mnt/felhom-drives/real" || tg.Unproven { t.Fatalf("a proven drive must win over an unknown one, got %+v", tg) } } // Row 4: a network share → never a Tier-2 target and never hides the notice (R-1075). func TestDisk16_NetworkShare_NeverHidesTheNotice(t *testing.T) { m, sett := newTestManager(t, "/srv/sys") addDrive(t, sett, settings.StoragePath{Path: "/mnt/felhom-drives/nas", Label: "NAS", Kind: settings.StorageKindNetwork}) // No judge at all — the OLD st_dev rule calls the NAS "different", which is exactly R-1075. if m.hasOffDriveTarget() { t.Fatal("R-1075: a NAS made the box claim an off-drive copy") } if w := m.singleCopyWarning(); w != m.note("note.backup.single_copy") { t.Fatalf("a NAS must not hide the one-copy notice, got %q", w) } if tg, err := m.selectTier2TargetFrom("app", "/srv/sys", 1, 1); err == nil && !tg.IsSystemDrive { t.Fatalf("a NAS must never be a Tier-2 target, got %+v", tg) } } // An older agent (supported=false) keeps today's behaviour exactly: the st_dev seam decides. func TestDisk16_OldAgent_KeepsTheStDevRule(t *testing.T) { m, sett := newTestManager(t, "/srv/sys") addDrive(t, sett, settings.StoragePath{Path: "/mnt/b", Label: "B"}) calls := 0 m.SetDiskJudge(func(a, b string) (DiskVerdict, bool) { calls++; return DiskVerdict{}, false }) tg, err := m.selectTier2TargetFrom("app", "/srv/sys", 1, 1) if err != nil || tg.NamespaceRoot != "/mnt/b" || tg.Unproven { t.Fatalf("old agent: want today's pick (/mnt/b, counted), got %+v err=%v", tg, err) } if calls == 0 { t.Fatal("the judge was never asked") } if m.singleCopyWarning() != "" { t.Fatal("old agent: a second drive by st_dev must still hide the notice (today's behaviour)") } // And the st_dev rule's own same-disk answer still stands. m.samePhysicalDevice = func(a, b string) bool { return true } if _, err := m.selectTier2TargetFrom("app", "/srv/x", 1, 1); err == nil { t.Fatal("old agent: st_dev 'same' must still refuse the target") } } // The success record carries the mark the app row renders. func TestDisk16_UnprovenCopyIsRecorded(t *testing.T) { m, sett := newTestManager(t, "/srv/sys") m.recordTier2Success("app", &Tier2Target{NamespaceRoot: "/mnt/a", Unproven: true, UnprovenReason: "vm"}, 1, "", 0) cd := sett.GetCrossDriveConfig("app") if cd == nil || !cd.Unproven || cd.UnprovenReason != "vm" { t.Fatalf("want the unproven mark recorded, got %+v", cd) } m.recordTier2Success("app", &Tier2Target{NamespaceRoot: "/mnt/a"}, 1, "", 0) if cd := sett.GetCrossDriveConfig("app"); cd.Unproven { t.Fatal("a later proven copy must clear the mark") } }