hub v0.60.1: host deletion demotes escrow custody (never destroys) + customer-delete purge point + S6b obsolete
- DeleteHost(deleteEscrow) demotes current host_escrow into host_escrow_superseded (copy-before-delete, same tx), spares existing; one shared demoteCurrentEscrowTx (reused by SaveHostEscrow). F-14 provenance/gate unchanged. - DeleteCustomerConfig now purges both escrow tables for all the customer's hosts incl. already-deleted (F-14 provenance UNION) — the one true purge point. - Wording: checkbox/refusal/Danger-zone → demotion. S6b OBSOLETE. Red-proofs TestDeleteHost_Demotes + TestDeleteCustomer_Purges + wording guard.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Scenario A (v0.60.1) — host deletion DEMOTES the current escrow blob to retained custody and SPARES
|
||||
// existing superseded blobs; it never destroys custody. RED-PROOF: the v0.60.0 code deleted both escrow
|
||||
// tables → after DeleteHost the blobs are gone → the retrieval assertions FAIL.
|
||||
func TestDeleteHost_DemotesEscrowNeverDestroys(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
const hostID, cust = "dh1", "cust-dh"
|
||||
if err := s.UpsertHost(&Host{HostID: hostID, CustomerID: cust, APIKey: "k"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// current escrow = SHA_A, one superseded = SHA_OLD (two uploads with different passphrases).
|
||||
if _, err := s.SaveHostEscrow(hostID, []byte("blob-old"), "fp", "zk", "2026-07-09T00:00:00Z", "SHA_OLD"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.SaveHostEscrow(hostID, []byte("blob-A"), "fp", "zk", "2026-07-16T00:00:00Z", "SHA_A"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, _ := s.CountSupersededEscrow(hostID); n != 1 {
|
||||
t.Fatalf("precondition superseded=%d, want 1", n)
|
||||
}
|
||||
|
||||
// Refusal without the flag is unchanged (escrow present + !deleteEscrow → ErrHostEscrowPresent).
|
||||
if err := s.DeleteHost(hostID, false); !errors.Is(err, ErrHostEscrowPresent) {
|
||||
t.Fatalf("delete without the flag: got %v, want ErrHostEscrowPresent", err)
|
||||
}
|
||||
|
||||
if err := s.DeleteHost(hostID, true); err != nil {
|
||||
t.Fatalf("DeleteHost: %v", err)
|
||||
}
|
||||
if h, _ := s.GetHost(hostID); h != nil {
|
||||
t.Fatal("host row survived the delete")
|
||||
}
|
||||
// The current row is DEMOTED (moved), not kept.
|
||||
if cur, _ := s.GetHostEscrow(hostID); cur != nil {
|
||||
t.Fatal("current escrow row survived — should be demoted into superseded, not left as current")
|
||||
}
|
||||
// RED-PROOF: BOTH blobs are RETAINED (SHA_A demoted + SHA_OLD spared) — host delete never destroys.
|
||||
sup, err := s.ListSupersededEscrow(hostID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sup) != 2 {
|
||||
t.Fatalf("retained superseded = %d, want 2 (SHA_A demoted + SHA_OLD spared) — host delete DESTROYED custody", len(sup))
|
||||
}
|
||||
shas := map[string]bool{}
|
||||
for _, e := range sup {
|
||||
shas[e.ResticPwSHA256] = true
|
||||
}
|
||||
if !shas["SHA_A"] || !shas["SHA_OLD"] {
|
||||
t.Fatalf("retained shas = %v, want both SHA_A + SHA_OLD", shas)
|
||||
}
|
||||
|
||||
// EDGE: a host with NO escrow → DeleteHost unchanged (no demote row, no error).
|
||||
const h2 = "dh2"
|
||||
if err := s.UpsertHost(&Host{HostID: h2, CustomerID: cust, APIKey: "k2"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.DeleteHost(h2, true); err != nil {
|
||||
t.Fatalf("no-escrow delete: %v", err)
|
||||
}
|
||||
if n, _ := s.CountSupersededEscrow(h2); n != 0 {
|
||||
t.Fatalf("a no-escrow host delete created a superseded row: %d", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user