package httpapi import ( "evobgp/internal/birdfmt" "evobgp/internal/store" "testing" "time" ) func TestTelemetryHydrationFreshnessAndConfiguration(t *testing.T) { now := time.Now().UTC() sp := &store.Speaker{ID: "replica", Role: "replica", Endpoint: "old"} observation := store.TelemetryObservation{ObservedAt: now.Add(-time.Hour), ReceivedAt: now, LastSuccessAt: now, Source: "agent"} base := store.TelemetrySnapshot{SpeakerID: sp.ID, Kind: "remote_agent", ConfigFingerprint: store.TelemetrySpeakerFingerprint(sp), BGP: store.TelemetryBGP{TelemetryObservation: observation, Sessions: []birdfmt.BGPSession{{Name: "peer", State: "Established"}}}, Health: store.TelemetryHealth{TelemetryObservation: observation, OK: true}} cases := []struct { name string mutate func(*store.TelemetrySnapshot) bgp, health bool quality string }{ {"fresh receipt despite remote clock", func(*store.TelemetrySnapshot) {}, true, true, "ok"}, {"expired BGP independent of health", func(s *store.TelemetrySnapshot) { s.BGP.ReceivedAt = now.Add(-2 * time.Minute) }, false, true, "stale"}, {"expired health independent of BGP", func(s *store.TelemetrySnapshot) { s.Health.ReceivedAt = now.Add(-2 * time.Minute) }, true, false, "ok"}, {"future receipt is not fresh", func(s *store.TelemetrySnapshot) { s.BGP.ReceivedAt = now.Add(time.Hour) }, false, true, "stale"}, {"configuration changed", func(s *store.TelemetrySnapshot) { s.ConfigFingerprint = "old" }, false, false, "unknown"}, {"failed BGP", func(s *store.TelemetrySnapshot) { s.BGP.Error = "timeout" }, false, true, "stale"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { snapshot := base tc.mutate(&snapshot) views, statuses := hydrateTelemetry([]*store.Speaker{sp}, []store.TelemetrySnapshot{snapshot}, now, time.Minute) status := statuses[sp.ID] if status["bgp_poll_ok"] != tc.bgp || status["agent_ok"] != tc.health || views[0].observation().Quality != tc.quality { t.Fatalf("status=%+v observation=%+v", status, views[0].observation()) } if !tc.bgp && (status["bgp_established"] != nil || len(views[0].Sessions) != 0) { t.Fatal("stale BGP exposed live sessions") } }) } } func TestTelemetryHydrationMissingAndAmbiguousLocal(t *testing.T) { now := time.Now() sp := &store.Speaker{ID: "master", Role: "master"} for _, rows := range [][]store.TelemetrySnapshot{nil, {{SpeakerID: sp.ID, Kind: "local_bird", LocalAuthority: "a"}, {SpeakerID: sp.ID, Kind: "local_bird", LocalAuthority: "b"}}} { views, statuses := hydrateTelemetry([]*store.Speaker{sp}, rows, now, time.Minute) if views[0].observation().Quality != "unknown" || statuses[sp.ID]["agent_ok"] == true || statuses[sp.ID]["bgp_poll_ok"] == true { t.Fatalf("unexpected healthy master: %+v", statuses) } } } func TestTelemetryHydrationLocalAuthorityRestart(t *testing.T) { now := time.Now() sp := &store.Speaker{ID: "master", Role: "master"} fresh := store.TelemetrySnapshot{SpeakerID: sp.ID, Kind: "local_bird", LocalAuthority: "new", ConfigFingerprint: store.TelemetrySpeakerFingerprint(sp), BGP: store.TelemetryBGP{TelemetryObservation: store.TelemetryObservation{ReceivedAt: now}}} old := fresh old.LocalAuthority = "old" old.BGP.ReceivedAt = now.Add(-2 * time.Minute) _, status := hydrateTelemetry([]*store.Speaker{sp}, []store.TelemetrySnapshot{old, fresh}, now, time.Minute) if status[sp.ID]["bgp_poll_ok"] != true { t.Fatal("expired authority hides current local snapshot") } old.BGP.ReceivedAt = now _, status = hydrateTelemetry([]*store.Speaker{sp}, []store.TelemetrySnapshot{old, fresh}, now, time.Minute) if status[sp.ID]["bgp_poll_ok"] != false { t.Fatal("two current local authorities are ambiguous") } } func TestTelemetryPeerObservationPreservesUnknownConfiguration(t *testing.T) { now := time.Now() sp := &store.Speaker{ID: "replica", Role: "replica"} row := store.TelemetrySnapshot{SpeakerID: sp.ID, Kind: "remote_agent", ConfigFingerprint: "previous", BGP: store.TelemetryBGP{TelemetryObservation: store.TelemetryObservation{ReceivedAt: now, LastSuccessAt: now}}} views, _ := hydrateTelemetry([]*store.Speaker{sp}, []store.TelemetrySnapshot{row}, now, time.Minute) observation := peerLiveObservation(views) if observation.Quality != "unknown" || observation.ErrorCode != "configuration_changed" { t.Fatalf("changed config is not a failed current poll: %+v", observation) } } func TestTelemetryExpiryUsesConfiguredIntervalAndEarliestSource(t *testing.T) { now := time.Now().UTC() old := now.Add(-30 * time.Second) a := snapshotObservation(store.TelemetryObservation{ReceivedAt: old}, now, 5*time.Minute, "") b := snapshotObservation(store.TelemetryObservation{ReceivedAt: now}, now, 5*time.Minute, "") want := old.Add(5 * time.Minute).Format(time.RFC3339Nano) if a.ExpiresAt == nil || *a.ExpiresAt != want { t.Fatalf("expiry ignores configured interval: %+v", a) } views := []speakerBGPLive{{Polled: true, ObservationOverride: &b}, {Polled: true, ObservationOverride: &a}} combined := peerLiveObservation(views) if combined.ExpiresAt == nil || *combined.ExpiresAt != want { t.Fatalf("aggregate extends oldest source: %+v", combined) } missing := snapshotObservation(store.TelemetryObservation{}, now, time.Minute, "") if missing.ExpiresAt != nil { t.Fatal("unobserved source has expiry") } }