Files
admin dcf3a5530b v0.91.1 — wire the credential probe (v0.91.0 shipped the seam inert)
v0.91.0 built the AuthSink seam and the NoteAuthResult consumer, and main.go never
called SetAuthSink. The reporter skips probing when no sink is attached, so the entire
auth-honesty leg was silently inert — no probe, no auth_failed, no self-heal — and
nothing failed, because every unit test injected the sink directly.

Caught during STOP-1 live verification by checking the wiring instead of trusting it.
Same class as the controller v0.154.0 defect the day before: a table test over a seam
proves the seam, not the caller.

The published 0.91.0 artifact is superseded, not overwritten — a published version stays
immutable. TestLiveReporter_NoSinkMeansNoProbe pins the no-sink-no-probe contract so the
inert case is documented behaviour rather than an accident; only live evidence can prove
the wiring itself.
2026-07-21 10:18:41 +02:00

110 lines
3.9 KiB
Go

package pbs
import (
"context"
"errors"
"io"
"log/slog"
"testing"
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
)
type recordingSink struct {
calls []struct {
storage string
unauthorized bool
detail string
}
}
func (s *recordingSink) NoteAuthResult(storageID string, unauthorized bool, detail string) {
s.calls = append(s.calls, struct {
storage string
unauthorized bool
detail string
}{storageID, unauthorized, detail})
}
func newProbeReporter(t *testing.T, sink AuthSink, probeErr error) *LiveSnapshotReporter {
t.Helper()
targets := func(ctx context.Context) ([]Target, error) {
return []Target{{Datastore: "felhom-offsite", StorageID: "felhom-pbs"}}, nil
}
r := NewLiveSnapshotReporter(targets, NewSnapshotStore(), 0, slog.New(slog.NewTextHandler(io.Discard, nil)))
r.listSnapshots = func(context.Context, Target) ([]hub.PBSSnapshot, error) { return nil, nil }
r.probeAuth = func(context.Context, Target) error { return probeErr }
if sink != nil {
r.SetAuthSink(sink)
}
return r
}
// R-39 leg (c): a 401 must reach the sink as UNAUTHORIZED — the signal the DR bridge turns into a
// loud auth_failed state.
func TestLiveReporter_ForwardsUnauthorized(t *testing.T) {
sink := &recordingSink{}
r := newProbeReporter(t, sink, ErrUnauthorized)
r.PBSSnapshots(context.Background())
if len(sink.calls) != 1 {
t.Fatalf("sink calls = %d, want 1 (the probe must run on every collect)", len(sink.calls))
}
c := sink.calls[0]
if !c.unauthorized || c.storage != "felhom-pbs" {
t.Errorf("got %+v, want unauthorized for felhom-pbs", c)
}
}
// A TRANSPORT error is UNKNOWN, never a rejection: reporting it as unauthorized would re-key a
// perfectly good credential on every network blip.
func TestLiveReporter_TransportErrorIsNotUnauthorized(t *testing.T) {
sink := &recordingSink{}
r := newProbeReporter(t, sink, errors.New("dial tcp 10.77.0.1:8007: connect: connection refused"))
r.PBSSnapshots(context.Background())
if len(sink.calls) != 1 {
t.Fatalf("sink calls = %d, want 1", len(sink.calls))
}
if sink.calls[0].unauthorized {
t.Error("an unreachable PBS was reported as unauthorized — every blip would burn a credential")
}
if sink.calls[0].detail == "" {
t.Error("an inconclusive probe must carry a detail so the state is explainable")
}
}
// A healthy credential clears: unauthorized=false with an EMPTY detail is the recovery signal the
// bridge keys on.
func TestLiveReporter_HealthyProbeIsACleanClear(t *testing.T) {
sink := &recordingSink{}
r := newProbeReporter(t, sink, nil)
r.PBSSnapshots(context.Background())
if len(sink.calls) != 1 || sink.calls[0].unauthorized || sink.calls[0].detail != "" {
t.Fatalf("got %+v, want a clean clear (unauthorized=false, detail=\"\")", sink.calls)
}
}
// THE WIRING GUARD. With no sink attached the reporter must not probe at all — and this test exists
// because that is exactly how the leg shipped inert the first time: the seam was built and main.go
// never called SetAuthSink, so probeAuth was never invoked and nothing failed.
//
// A unit test cannot assert main.go's wiring; the live STOP-1/STOP-2 evidence does that. What it CAN
// pin is the contract this depends on — no sink means no probe — so the inert case stays a
// deliberate, documented behaviour rather than an accident nobody notices twice.
func TestLiveReporter_NoSinkMeansNoProbe(t *testing.T) {
probed := false
targets := func(ctx context.Context) ([]Target, error) {
return []Target{{Datastore: "d", StorageID: "s"}}, nil
}
r := NewLiveSnapshotReporter(targets, NewSnapshotStore(), 0, slog.New(slog.NewTextHandler(io.Discard, nil)))
r.listSnapshots = func(context.Context, Target) ([]hub.PBSSnapshot, error) { return nil, nil }
r.probeAuth = func(context.Context, Target) error { probed = true; return nil }
// deliberately no SetAuthSink
r.PBSSnapshots(context.Background())
if probed {
t.Error("probed with no sink attached — a request nothing consumes")
}
}