package store import ( "errors" "testing" ) // R-1254 (review round 2, HUNT-3 H1): the hub saved a household's new escrow package in TWO writes — the key and its // sealed-password hash committed, then the identity blob in a second statement. A failure between them left the new // hash beside the OLD identity package; the box read the hash, marked itself escrowed, and on a total loss the written // recovery code opened nothing. Now one transaction. RED-PROOF (2026-10-24): with SaveHostEscrowWithIdentity committing // the key before the identity write (the old two-step shape), this fails on „the NEW key is stored beside the OLD // identity package". func TestR1254_AFailedIdentityWriteStoresNothingNew(t *testing.T) { s := newTestStore(t) if err := s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "K"}); err != nil { t.Fatal(err) } if _, _, err := s.SaveHostEscrowWithIdentity("h1", []byte("key-gen1"), []byte("identity-gen1"), "fp1", "zero_knowledge", "2026-10-24T20:00:00Z", "SHA_GEN1"); err != nil { t.Fatal(err) } identityWriteFault = func() error { return errors.New("disk full") } t.Cleanup(func() { identityWriteFault = nil }) _, _, err := s.SaveHostEscrowWithIdentity("h1", []byte("key-gen2"), []byte("identity-gen2"), "fp2", "zero_knowledge", "2026-10-24T21:00:00Z", "SHA_GEN2") if err == nil { t.Fatal("the failed identity write must fail the save") } e, _ := s.GetHostEscrow("h1") b, _ := s.GetHostDRBundle("h1") if e == nil || b == nil { t.Fatalf("the old package must still be there: escrow=%v bundle=%v", e, b) } if string(e.Blob) != "key-gen1" || e.ResticPwSHA256 != "SHA_GEN1" || string(b.IdentityBlob) != "identity-gen1" { t.Fatalf("the NEW key is stored beside the OLD identity package (or a mix): blob=%q sha=%q identity=%q", e.Blob, e.ResticPwSHA256, b.IdentityBlob) } if n, _ := s.CountSupersededEscrow("h1"); n != 0 { t.Fatalf("a rolled-back save must not leave a superseded copy behind, got %d", n) } // and the success path: all new, the old one retained as superseded identityWriteFault = nil if _, _, err := s.SaveHostEscrowWithIdentity("h1", []byte("key-gen2"), []byte("identity-gen2"), "fp2", "zero_knowledge", "2026-10-24T21:00:00Z", "SHA_GEN2"); err != nil { t.Fatal(err) } e, _ = s.GetHostEscrow("h1") b, _ = s.GetHostDRBundle("h1") if string(e.Blob) != "key-gen2" || e.ResticPwSHA256 != "SHA_GEN2" || string(b.IdentityBlob) != "identity-gen2" { t.Fatalf("all new after a good save: blob=%q sha=%q identity=%q", e.Blob, e.ResticPwSHA256, b.IdentityBlob) } if n, _ := s.CountSupersededEscrow("h1"); n != 1 { t.Fatalf("the gen-1 package must be retained (R-366), got %d", n) } } // An upload without an identity blob (slice 7) leaves the row's identity as it was — the old behaviour, unchanged. func TestR1254_AnUploadWithoutIdentityLeavesItAlone(t *testing.T) { s := newTestStore(t) if err := s.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "K"}); err != nil { t.Fatal(err) } if _, _, err := s.SaveHostEscrowWithIdentity("h1", []byte("k"), []byte("id"), "fp", "zero_knowledge", "2026-10-24T20:00:00Z", "SHA"); err != nil { t.Fatal(err) } if _, _, err := s.SaveHostEscrow("h1", []byte("k"), "fp", "zero_knowledge", "2026-10-24T20:05:00Z", "SHA"); err != nil { t.Fatal(err) } if b, _ := s.GetHostDRBundle("h1"); b == nil || string(b.IdentityBlob) != "id" { t.Fatalf("the identity blob must stay, got %+v", b) } }