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