package store import ( "testing" "time" ) // R-1182: abandoning ONE set-aside off-site copy removes only the package that opens THAT copy. Before 2026-10-21 the // purge was customer-wide (audits/dataloss-hunt-2026-10-20, C1 reviewer 3, K2): the package of a second set-aside copy // still on the Storage Box went with it. func TestR1182_AbandoningOneCopyKeepsTheOtherCopysPackage(t *testing.T) { s := newTestStore(t) const cust = "C" const repo = "/home/felhom/C/repo" if err := s.UpsertHost(&Host{HostID: "C-h", CustomerID: cust, APIKey: "k"}); err != nil { t.Fatal(err) } // A row retained before the tag existed (legacy): unknown copy — must be kept. if _, err := s.db.Exec(`INSERT INTO host_escrow_superseded (host_id, blob, created_at, restic_pw_sha256, identity_blob) VALUES ('C-h', x'01', '2026-01-01', 'SHA_LEGACY', x'02')`); err != nil { t.Fatal(err) } save := func(sha string) { t.Helper() if _, _, err := s.SaveHostEscrow("C-h", []byte("k-"+sha), "fp", "zk", "2026-10-01T00:00:00Z", sha); err != nil { t.Fatal(err) } if err := s.SaveHostDRBundle("C-h", []byte("id-"+sha)); err != nil { t.Fatal(err) } } save("SHA_1") // key 1 writes the repository save("SHA_2") // key 1 retained ('live'); the box set the old copy aside: if n, err := s.TagRetainedEscrowSetAside(cust, repo+".orphaned-20261001"); err != nil || n != 1 { t.Fatalf("first set-aside tagged %d err=%v, want 1 (key 1)", n, err) } save("SHA_3") // key 2 retained; a second copy set aside: if n, err := s.TagRetainedEscrowSetAside(cust, repo+".orphaned-20261015"); err != nil || n != 1 { t.Fatalf("second set-aside tagged %d err=%v, want 1 (key 2)", n, err) } // The household abandons the FIRST copy; the hub deletes it. id, err := s.CreateOffsiteAbandon(cust, repo+".orphaned-20261001", time.Now()) if err != nil { t.Fatal(err) } if err := s.MarkOffsiteAbandonDeleted(id); err != nil { t.Fatal(err) } if left, _ := s.AbandonedEscrowLeft(cust); left != 1 { t.Fatalf("before the purge, packages of deleted copies = %d, want 1", left) } n, err := s.PurgeAbandonedEscrowForCustomer(cust) if err != nil || n != 1 { t.Fatalf("purge removed %d err=%v, want exactly 1 (key 1)", n, err) } kept := map[string]bool{} rows, _ := s.ListSupersededEscrow("C-h") for _, r := range rows { kept[r.ResticPwSHA256] = true } if !kept["SHA_2"] { t.Fatal("the package that opens the SECOND set-aside copy was purged — R-1182") } if !kept["SHA_LEGACY"] { t.Fatal("a legacy package (unknown copy) was purged by a guess") } if kept["SHA_1"] { t.Fatal("the abandoned copy's package survived the purge") } if cur, _ := s.GetHostEscrow("C-h"); cur == nil || cur.ResticPwSHA256 != "SHA_3" { t.Fatalf("the CURRENT package was touched: %+v", cur) } if left, _ := s.AbandonedEscrowLeft(cust); left != 0 { t.Fatalf("after the purge, packages of deleted copies = %d, want 0", left) } if n2, _ := s.PurgeAbandonedEscrowForCustomer(cust); n2 != 0 { t.Fatalf("second purge removed %d, want 0 (idempotent)", n2) } } // A package whose key is the box's CURRENT key is not tagged by a move-aside: it opens the new repository (or holds // an older whole-guest key, R-366), not the copy set aside. func TestR1182_TheCurrentKeysPackageIsNotTaggedToASetAsideCopy(t *testing.T) { s := newTestStore(t) if err := s.UpsertHost(&Host{HostID: "C-h", CustomerID: "C", APIKey: "k"}); err != nil { t.Fatal(err) } if _, _, err := s.SaveHostEscrow("C-h", []byte("k1"), "FP1", "zk", "2026-10-01T00:00:00Z", "SHA_X"); err != nil { t.Fatal(err) } // same repository password, new whole-guest key → retained (R-366) with the SAME restic hash if _, _, err := s.SaveHostEscrow("C-h", []byte("k2"), "FP2", "zk", "2026-10-02T00:00:00Z", "SHA_X"); err != nil { t.Fatal(err) } if n, err := s.TagRetainedEscrowSetAside("C", "/r.orphaned-20261003"); err != nil || n != 0 { t.Fatalf("tagged %d err=%v, want 0", n, err) } }