package backup import ( "context" "strings" "testing" ) // ── THE CONTROL — R-359's single most important property ───────────────────────────────────────── // // `resticStep` self-heals a crash lock: on "repository is already locked" it runs // **`unlock --remove-all`** and retries. Its own doc comment states why that is safe — *"the in-process // single-flight mutex (held by every caller of this method) proves no sibling operation is live"*. // // **An integrity check that did not take that flag would break the invariant.** It could meet the lock // of a `forget --prune` running from this same box, remove it, and retry over the top of a live prune — // on the tier holding the customer's documents and photos. // // So these assert NON-EFFECTS, which is the only way to test "it did not do the dangerous thing": // restic was never invoked at all, and `unlock` never appeared in any argument list. A test that only // checked `Skipped == true` would pass against an implementation that skipped AFTER running the check. func TestR359_SkipsWhenRunningFlagHeld(t *testing.T) { m, cap := newIntegrityManager(t, okRepo(nil)) // A sibling operation is live — exactly the state a nightly off-site backup produces. if err := m.AcquireRunningForTest(); err != nil { t.Fatalf("fixture: %v", err) } defer m.ReleaseRunningForTest() res := m.CheckOffboxIntegrity(context.Background()) // THE TWO NON-EFFECTS. These are the assertions that fail against a missing guard; the verdict // fields below would not. if len(cap.argvs) != 0 { t.Fatalf("RESTIC WAS INVOKED while another operation held the single-writer flag: %v — this is "+ "the hazard: the check can meet a live prune's lock, and resticStep escalates to "+ "`unlock --remove-all` and retries over the top of it", cap.argvs) } if cap.sawVerb("unlock") { t.Fatal("`unlock` was invoked beside a live sibling operation — the exact act that must never happen") } if !res.Skipped { t.Fatalf("the check did not report a skip: %+v", res) } if res.OK { t.Fatal("a skip was reported as a passing check — nothing was checked, and recording it as a " + "pass would let a store go unverified while the record says otherwise") } // Due-ness must NOT advance: tomorrow has to try again. if tgt := m.settings.GetOffboxTarget(); tgt != nil && tgt.LastIntegrityCheck != "" { t.Fatalf("a SKIPPED check advanced due-ness (%q) — the store would then wait a full period "+ "before anything looked at it again, which is R-341's failure with extra steps", tgt.LastIntegrityCheck) } } func TestR359_ReleasesTheFlagOnEveryPath(t *testing.T) { // A check that leaks the flag stops every backup on the box until restart. Each outcome is walked. for _, tc := range []struct { name string reply func(args []string) ([]byte, error) }{ {"ok", okRepo(nil)}, {"damaged", okRepo(func([]string) ([]byte, error) { return []byte("repository contains errors"), errFake })}, {"unreachable", func([]string) ([]byte, error) { return []byte("connection refused"), errFake }}, } { t.Run(tc.name, func(t *testing.T) { m, _ := newIntegrityManager(t, tc.reply) m.CheckOffboxIntegrity(context.Background()) // If the flag leaked, this acquire fails. if err := m.AcquireRunningForTest(); err != nil { t.Fatalf("the single-writer flag was NOT released after a %s outcome (%v) — every "+ "backup and restore on this box would refuse until it restarts", tc.name, err) } m.ReleaseRunningForTest() }) } } func TestR359_DoesNotBlockAConcurrentBackup(t *testing.T) { // The complement: a check that skipped must leave the flag free for the operation it yielded to. m, _ := newIntegrityManager(t, okRepo(nil)) if err := m.AcquireRunningForTest(); err != nil { t.Fatal(err) } m.CheckOffboxIntegrity(context.Background()) // skips m.ReleaseRunningForTest() // the sibling finishes if err := m.AcquireRunningForTest(); err != nil { t.Fatalf("after a skip the flag could not be acquired: %v — the check held something it "+ "never took", err) } m.ReleaseRunningForTest() } func TestR359_NeverWritesToTheRepository(t *testing.T) { // `check` is a read verb. This pins that the check's argv carries no verb that could modify the // store — the tier holds real customer data and the box's own credential can already delete from // it (R-95, open and ranked). m, cap := newIntegrityManager(t, okRepo(nil)) m.CheckOffboxIntegrity(context.Background()) for _, verb := range []string{"forget", "prune", "backup", "init", "unlock", "restore"} { if cap.sawVerb(verb) { t.Fatalf("the integrity check invoked `%s` — it must only ever READ (argv=%v)", verb, cap.argvs) } } argv := strings.Join(cap.checkArgv(), " ") if !strings.Contains(argv, "check") { t.Fatalf("no check verb in %q", argv) } }