150 lines
5.3 KiB
Go
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")
|
|
}
|
|
}
|