Files
felhom.eu/hub/internal/store/r1254_escrow_one_tx_test.go
T

71 lines
3.4 KiB
Go

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