gates / gates (push) Successful in 1m52s
Tests red before the fix: TestR1255_TheCarryTakesTheNewestPackageThatHasAHash, TestR1255_APurgeNeverDeletesAPackageAReusedPathStillNeeds, TestR1255_TwoSetAsidesOnOneDayGetTwoNames. Commit only; rides the next hub release.
126 lines
5.3 KiB
Go
126 lines
5.3 KiB
Go
package store
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// R-1255 (review round 2, HUNT-3 H3/H4): two escrow edges at the store.
|
|
|
|
// H3: the R-1176 carry took the deleted host's NEWEST retained row and checked only its identity blob. A newer
|
|
// row with an identity but NO repository-password hash (a package that seals no off-site key) was carried, and
|
|
// the household's real package stayed behind under the deleted host. The carry takes the newest row that has
|
|
// BOTH.
|
|
// RED-PROOF: drop `AND restic_pw_sha256 != ”` from the carry's retained lookup → the hash-less row is carried → FAILS.
|
|
func TestR1255_TheCarryTakesTheNewestPackageThatHasAHash(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "T"
|
|
identity := []byte("sealed-identity-opening-the-repo")
|
|
carrySetupOldBox(t, s, cust, "T-old", "SHA_REAL", identity)
|
|
if err := s.DeleteHost("T-old", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A newer retained row of the same deleted host: an identity, no repository-password hash.
|
|
if _, err := s.db.Exec(`INSERT INTO host_escrow_superseded (host_id, blob, key_fingerprint, posture, created_at, restic_pw_sha256, identity_blob, repo_tag)
|
|
VALUES ('T-old', x'0101', 'fp-x', 'zero_knowledge', '2026-10-19T00:00:00Z', '', x'0202', 'live')`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.UpsertHost(&Host{HostID: "T-new", CustomerID: cust, APIKey: "k2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, err := s.ReattachRetainedEscrow(cust)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Outcome != CarryDone || res.ResticPwSHA256 != "SHA_REAL" {
|
|
t.Fatalf("carry = %+v — want the package with the household's hash (SHA_REAL)", res)
|
|
}
|
|
b, err := s.GetHostDRBundle("T-new")
|
|
if err != nil || b == nil || !bytes.Equal(b.IdentityBlob, identity) {
|
|
t.Fatalf("the new host holds %+v (err %v) — want the household's identity package", b, err)
|
|
}
|
|
// The hash-less row stays retained under the deleted host (nothing is thrown away).
|
|
ret, _ := s.ListSupersededEscrow("T-old")
|
|
if len(ret) != 1 || ret[0].ResticPwSHA256 != "" {
|
|
t.Fatalf("retained under the deleted host: %+v — want only the hash-less row", ret)
|
|
}
|
|
}
|
|
|
|
// Only hash-less rows (a legacy package: its identity may still seal the password, the hash was never recorded): the
|
|
// newest with an identity is still carried, as before — never fewer ways back than before the fix.
|
|
func TestR1255_WithNoHashedPackageTheNewestIdentityIsStillCarried(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "U"
|
|
carrySetupOldBox(t, s, cust, "U-old", "", []byte("id"))
|
|
if err := s.DeleteHost("U-old", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.UpsertHost(&Host{HostID: "U-new", CustomerID: cust, APIKey: "k2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res, err := s.ReattachRetainedEscrow(cust); err != nil || res.Outcome != CarryDone || res.FromHost != "U-old" {
|
|
t.Fatalf("carry = %+v err=%v — want the legacy package carried, as before", res, err)
|
|
}
|
|
}
|
|
|
|
// H4: two set-asides of one customer can carry the SAME path (the first was abandoned and deleted, then a second
|
|
// copy was set aside under the free name). The purge deleted every row tagged with a deleted path — so the SECOND
|
|
// copy's package went too, while that copy is still stored. A row is purged only when it was tagged no later than
|
|
// the deletion of its path.
|
|
// RED-PROOF: drop the tagged-before-the-deletion condition from PurgeAbandonedEscrowForCustomer → the second copy's
|
|
// package is purged → FAILS.
|
|
func TestR1255_APurgeNeverDeletesAPackageAReusedPathStillNeeds(t *testing.T) {
|
|
s := newTestStore(t)
|
|
const cust = "R"
|
|
const repo = "/home/felhom/R/repo"
|
|
path := repo + ".orphaned-20261028"
|
|
if err := s.UpsertHost(&Host{HostID: "R-h", CustomerID: cust, APIKey: "k"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
save := func(sha string) {
|
|
t.Helper()
|
|
if _, _, err := s.SaveHostEscrow("R-h", []byte("k-"+sha), "fp", "zk", "2026-10-01T00:00:00Z", sha); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.SaveHostDRBundle("R-h", []byte("id-"+sha)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
save("SHA_1")
|
|
save("SHA_2") // key 1 retained; its copy set aside:
|
|
if n, err := s.TagRetainedEscrowSetAside(cust, path); err != nil || n != 1 {
|
|
t.Fatalf("first set-aside tagged %d err=%v", n, err)
|
|
}
|
|
id, err := s.CreateOffsiteAbandon(cust, path, time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.MarkOffsiteAbandonDeleted(id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(1100 * time.Millisecond) // the hub's clock has second precision; a real reuse is hours later
|
|
save("SHA_3") // key 2 retained; a second copy is set aside under the same (now free) name:
|
|
if n, err := s.TagRetainedEscrowSetAside(cust, path); err != nil || n != 1 {
|
|
t.Fatalf("second set-aside tagged %d err=%v", n, err)
|
|
}
|
|
n, err := s.PurgeAbandonedEscrowForCustomer(cust)
|
|
if err != nil || n != 1 {
|
|
t.Fatalf("purge removed %d err=%v — want exactly the deleted copy's package (key 1)", n, err)
|
|
}
|
|
kept := map[string]bool{}
|
|
rows, _ := s.ListSupersededEscrow("R-h")
|
|
for _, r := range rows {
|
|
kept[r.ResticPwSHA256] = true
|
|
}
|
|
if !kept["SHA_2"] {
|
|
t.Fatal("the package of the copy STILL STORED under the reused name was purged — R-1255 H4")
|
|
}
|
|
if kept["SHA_1"] {
|
|
t.Fatal("the deleted copy's package survived the purge")
|
|
}
|
|
if left, _ := s.AbandonedEscrowLeft(cust); left != 0 {
|
|
t.Fatalf("after the purge, packages of deleted copies = %d, want 0 (the reused path's package is not one)", left)
|
|
}
|
|
}
|