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) } }