dcf3a5530b
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.
110 lines
3.9 KiB
Go
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")
|
|
}
|
|
}
|