package settings import ( "log" "os" "path/filepath" "testing" ) func roleTestSettings(t *testing.T) *Settings { t.Helper() s, err := Load(filepath.Join(t.TempDir(), "settings.json"), log.New(os.Stderr, "", 0)) if err != nil { t.Fatalf("load: %v", err) } return s } // SCENARIO C, the core of E-2 §3: a drive NEVER acquires the backup-target role by appearing. // // This is the invariant the whole role model exists for. Registration is the path a newly-plugged // drive travels, and the failure it guards against is a USB stick silently becoming the destination // of the whole-system backup. The test asserts the CONSEQUENCE (no target is assigned after // registering drives) rather than the mechanism, so it still fails if a future caller sets the field // through some other route in the registration path. func TestRegisteringDrivesNeverAssignsTheBackupTarget(t *testing.T) { s := roleTestSettings(t) for _, p := range []string{"/mnt/hdd_1", "/mnt/usb_stick", "/mnt/nvme-1tb"} { if err := s.AddStoragePath(StoragePath{Path: p, Label: p, Schedulable: true}); err != nil { t.Fatalf("add %s: %v", p, err) } } if got := s.BackupTargetPath(); got != "" { t.Fatalf("registering drives assigned the backup target %q — a drive must never acquire a role "+ "by appearing (E-2 §3); only an explicit customer choice may assign it", got) } if s.BackupTargetAssigned() { t.Fatal("BackupTargetAssigned() is true after registration alone") } } // Exactly one target at a time — assigning a second must move the role, not duplicate it. Two paths // both claiming to be the destination is not a cosmetic defect: the renderer and the absent-check // would each pick a different one. func TestBackupTargetIsExclusive(t *testing.T) { s := roleTestSettings(t) for _, p := range []string{"/mnt/a", "/mnt/b"} { if err := s.AddStoragePath(StoragePath{Path: p, Schedulable: true}); err != nil { t.Fatalf("add: %v", err) } } if err := s.SetBackupTarget("/mnt/a"); err != nil { t.Fatalf("set a: %v", err) } if err := s.SetBackupTarget("/mnt/b"); err != nil { t.Fatalf("set b: %v", err) } var carriers []string for _, sp := range s.GetStoragePaths() { if sp.BackupTarget { carriers = append(carriers, sp.Path) } } if len(carriers) != 1 || carriers[0] != "/mnt/b" { t.Fatalf("expected exactly one carrier (/mnt/b), got %v", carriers) } } // STICKINESS (E-2 §3): a new drive appearing must not move an assigned target. This is the other half // of "never by appearing" — the first covers an unassigned box, this covers an assigned one. func TestNewDriveDoesNotStealAnAssignedTarget(t *testing.T) { s := roleTestSettings(t) if err := s.AddStoragePath(StoragePath{Path: "/mnt/hdd_1", Schedulable: true}); err != nil { t.Fatalf("add: %v", err) } if err := s.SetBackupTarget("/mnt/hdd_1"); err != nil { t.Fatalf("set: %v", err) } // A bigger, faster, newer drive appears. It must change nothing. if err := s.AddStoragePath(StoragePath{Path: "/mnt/nvme_huge", Schedulable: true}); err != nil { t.Fatalf("add: %v", err) } if got := s.BackupTargetPath(); got != "/mnt/hdd_1" { t.Fatalf("target moved to %q when a new drive appeared — the role is sticky", got) } } // An ABSENT target stays assigned. Clearing it on disconnect would be a silent retarget by omission: // the box would read as "no target configured" (degraded) instead of "your target drive is missing" // (alarm), and reconnecting would not restore the assignment. func TestDisconnectDoesNotClearTheTarget(t *testing.T) { s := roleTestSettings(t) if err := s.AddStoragePath(StoragePath{Path: "/mnt/hdd_1", Schedulable: true}); err != nil { t.Fatalf("add: %v", err) } if err := s.SetBackupTarget("/mnt/hdd_1"); err != nil { t.Fatalf("set: %v", err) } if err := s.SetDisconnected("/mnt/hdd_1", true, nil); err != nil { t.Fatalf("disconnect: %v", err) } if got := s.BackupTargetPath(); got != "/mnt/hdd_1" { t.Fatalf("disconnect cleared the target (got %q) — an absent target must stay assigned and "+ "alarm, never silently become 'unconfigured'", got) } } // A network share can never be the target: the role exists to survive a LOCAL disk failure, and a // remote, credential-bound share mounted at its own root (R-108) is a different risk model. func TestNetworkShareRefusedAsBackupTarget(t *testing.T) { s := roleTestSettings(t) if err := s.AddStoragePath(StoragePath{Path: "/mnt/felhom-drives/nas", Kind: StorageKindNetwork}); err != nil { t.Fatalf("add: %v", err) } if err := s.SetBackupTarget("/mnt/felhom-drives/nas"); err == nil { t.Fatal("a network share was accepted as the backup target") } if s.BackupTargetAssigned() { t.Fatal("a refused assignment still set the role") } }