Files
felhom-controller/controller/internal/report/escrow_stale_test.go
T

150 lines
5.3 KiB
Go

package report
import (
"context"
"log"
"strings"
"testing"
"bytes"
)
// Scenario F (v0.127.0) — the escrowed-state stale-blob re-check. The §8 truth table's NEW rows:
// an ESCROWED box whose hub blob does not cover the current password (hash mismatch, or a present
// blob with an EMPTY hash — the spike's hash-less supersession) raises a display-only stale flag
// + ONE warn per distinct hub hash. State never flips; nothing blocks.
type staleHarness struct {
*confirmerHarness
escrowed bool
}
func newStaleHarness(t *testing.T) *staleHarness {
t.Helper()
h := &staleHarness{confirmerHarness: &confirmerHarness{local: hubHash, localOK: true, logbuf: &bytes.Buffer{}}}
h.escrowed = true // the box state under test
h.c = &EscrowAutoConfirmer{
Pending: func() bool { return h.pending },
Escrowed: func() bool { return h.escrowed },
LocalHash: func() (string, bool) { return h.local, h.localOK },
Flip: func() error { h.flips++; return nil },
Wipe: func(context.Context) error { h.wipes++; return nil },
Logger: log.New(h.logbuf, "", 0),
}
return h
}
// escrowed + hash mismatch → stale flag + ONE warn (deduped per distinct hub hash), no flip.
func TestEscrowStale_MismatchWarnsOnceAndFlags(t *testing.T) {
h := newStaleHarness(t)
h.local = otherHash
h.c.Reconcile(matchStatus(hubHash))
if !h.c.StaleBlob() {
t.Fatal("mismatch on an escrowed box must raise the stale flag")
}
if h.flips != 0 {
t.Fatal("the stale re-check must NEVER flip state (no auto-UN-confirm)")
}
if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 1 {
t.Fatalf("want exactly 1 STALE warn, got %d: %s", got, h.logbuf.String())
}
// Dedupe: the same hub hash again → still exactly one warn; the flag stays up.
h.c.Reconcile(matchStatus(hubHash))
if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 1 {
t.Fatalf("same stale hash must warn ONCE, got %d", got)
}
if !h.c.StaleBlob() {
t.Fatal("flag must persist across deduped ACKs")
}
// A NEW distinct stale hash → warns again.
h.c.Reconcile(matchStatus("2222222222222222222222222222222222222222222222222222222222222222"))
if got := strings.Count(h.logbuf.String(), "STALE escrow"); got != 2 {
t.Fatalf("a new distinct stale hash must warn again, got %d", got)
}
}
// escrowed + blob present with an EMPTY hash (the spike's exact hash-less supersession) → stale
// + one warn under the hashless dedupe sentinel.
func TestEscrowStale_HashlessBlobWarnsOnce(t *testing.T) {
h := newStaleHarness(t)
h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: true}) // blob present, hash empty
if !h.c.StaleBlob() {
t.Fatal("a hash-less superseding blob must raise the stale flag")
}
if got := strings.Count(h.logbuf.String(), "NO password hash"); got != 1 {
t.Fatalf("want the hash-less warn once, got %d: %s", got, h.logbuf.String())
}
h.c.Reconcile(&EscrowStatus{IdentityBlobPresent: false}) // K-only legacy shape — still hash-less
if got := strings.Count(h.logbuf.String(), "NO password hash"); got != 1 {
t.Fatalf("hash-less must dedupe under its sentinel, got %d warns", got)
}
}
// escrowed + hash MATCHES → clears an earlier stale flag; no warn on the clean path.
func TestEscrowStale_MatchClearsFlag(t *testing.T) {
h := newStaleHarness(t)
h.local = otherHash
h.c.Reconcile(matchStatus(hubHash)) // go stale
if !h.c.StaleBlob() {
t.Fatal("setup: expected stale")
}
h.local = hubHash
h.c.Reconcile(matchStatus(hubHash)) // a covering blob arrives (re-ceremony ran)
if h.c.StaleBlob() {
t.Fatal("a matching hash must CLEAR the stale flag")
}
// And a clean box never warns.
h2 := newStaleHarness(t)
h2.c.Reconcile(matchStatus(hubHash))
if h2.c.StaleBlob() || strings.Contains(h2.logbuf.String(), "STALE") {
t.Fatalf("match must be silent: %s", h2.logbuf.String())
}
}
// Not-escrowed / no-local-password / nil-status rows: the re-check never runs (silent).
func TestEscrowStale_SilentRows(t *testing.T) {
h := newStaleHarness(t)
h.escrowed = false // offbox not configured (or any non-escrowed state)
h.c.Reconcile(matchStatus(otherHash))
if h.c.StaleBlob() || h.logbuf.Len() != 0 {
t.Fatalf("non-escrowed must be silent: %s", h.logbuf.String())
}
h2 := newStaleHarness(t)
h2.localOK = false // no local repo password file
h2.c.Reconcile(matchStatus(otherHash))
if h2.c.StaleBlob() || h2.logbuf.Len() != 0 {
t.Fatal("no local password → nothing to compare → silent")
}
h3 := newStaleHarness(t)
h3.c.Reconcile(nil) // no escrow row at all (blob absent — out of the truth table)
if h3.c.StaleBlob() || h3.logbuf.Len() != 0 {
t.Fatal("nil status must be silent")
}
}
// A fresh pending→escrowed auto-confirm clears any stale leftovers (the flag must not survive a
// successful re-ceremony's confirm).
func TestEscrowStale_AutoConfirmClears(t *testing.T) {
h := newStaleHarness(t)
h.local = otherHash
h.c.Reconcile(matchStatus(hubHash)) // stale while escrowed
if !h.c.StaleBlob() {
t.Fatal("setup: expected stale")
}
// The re-ceremony re-staged + re-uploaded; the box re-enters pending (edit flow) and the new
// blob covers the local password → auto-confirm path runs and must clear the flag.
h.escrowed = false
h.pending = true
h.c.Reconcile(matchStatus(otherHash))
if h.flips != 1 {
t.Fatal("setup: auto-confirm should have flipped")
}
if h.c.StaleBlob() {
t.Fatal("a hub-verified auto-confirm must clear the stale flag")
}
}