206 lines
7.4 KiB
Python
206 lines
7.4 KiB
Python
"""Tests for the BGP collector + event location services."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from app.api.v1 import bgp as bgp_api
|
|
from app.services import bgp_collector_locations
|
|
from app.services.location.llm_fallback import LocationLLMFallbackResult
|
|
from app.services.bgp_collector_locations import (
|
|
RIPE_RIS_COLLECTOR_COORDS,
|
|
collect_bgp_collector_location_candidates,
|
|
iter_known_collector_names,
|
|
resolve_bgp_collector_location,
|
|
)
|
|
from app.services.bgp_event_locations import (
|
|
resolve_bgp_event_geo_dict,
|
|
resolve_bgp_event_location,
|
|
)
|
|
|
|
|
|
def test_legacy_dict_view_preserves_backward_compatible_keys():
|
|
rrc00 = RIPE_RIS_COLLECTOR_COORDS["rrc00"]
|
|
assert rrc00["city"] == "Amsterdam"
|
|
assert rrc00["country"] == "Netherlands"
|
|
assert rrc00["latitude"] == pytest.approx(52.3676)
|
|
assert rrc00["longitude"] == pytest.approx(4.9041)
|
|
# New richer fields layered on top.
|
|
assert rrc00["precision"] == "city"
|
|
assert rrc00["source"] == "legacy_seed"
|
|
assert rrc00["needs_confirmation"] is True
|
|
|
|
|
|
def test_every_legacy_collector_present():
|
|
expected = {
|
|
"rrc00", "rrc01", "rrc03", "rrc04", "rrc05", "rrc06", "rrc07",
|
|
"rrc10", "rrc11", "rrc12", "rrc13", "rrc14", "rrc15", "rrc16",
|
|
"rrc18", "rrc19", "rrc20", "rrc21", "rrc22", "rrc23", "rrc24",
|
|
"rrc25", "rrc26",
|
|
}
|
|
assert set(iter_known_collector_names()) == expected
|
|
|
|
|
|
def test_resolve_bgp_collector_returns_stored_location():
|
|
result = resolve_bgp_collector_location("rrc12")
|
|
assert result.location is not None
|
|
assert result.location.city == "Frankfurt"
|
|
assert result.location.country == "Germany"
|
|
assert result.location.precision == "city"
|
|
assert result.location.source == "legacy_seed"
|
|
assert result.location.needs_confirmation is True
|
|
|
|
|
|
def test_resolve_unknown_bgp_collector_returns_diagnostic(monkeypatch):
|
|
monkeypatch.setattr(bgp_collector_locations, "_geocode_online", lambda q: None)
|
|
result = resolve_bgp_collector_location("rrc-doesnotexist")
|
|
assert result.location is None
|
|
assert result.diagnostic is not None
|
|
assert result.diagnostic.failure_reason
|
|
|
|
|
|
def test_collect_bgp_collector_candidates_uses_stored_context_without_registry(monkeypatch):
|
|
bgp_collector_locations._geocode_online.cache_clear()
|
|
|
|
def _fake_geocode(query):
|
|
assert "CIXP" in query or "Geneva" in query
|
|
return {
|
|
"lat": "46.2044",
|
|
"lon": "6.1432",
|
|
"display_name": "Geneva, Switzerland",
|
|
"address": {"city": "Geneva", "country": "Switzerland"},
|
|
}
|
|
|
|
monkeypatch.setattr(bgp_collector_locations, "_geocode_online", _fake_geocode)
|
|
candidates, attempted = collect_bgp_collector_location_candidates(
|
|
collector="rrc04",
|
|
)
|
|
assert attempted, "stored context should feed online query attempts"
|
|
assert candidates, "online geocoding should produce at least one candidate"
|
|
best = candidates[0]
|
|
assert best.source == "nominatim_online_geocode"
|
|
assert best.needs_confirmation is True
|
|
assert all(candidate.source != "local_registry" for candidate in candidates)
|
|
|
|
|
|
def test_collect_bgp_collector_candidates_uses_nominatim_when_registry_misses(monkeypatch):
|
|
bgp_collector_locations._geocode_online.cache_clear()
|
|
|
|
def _fake_geocode(query):
|
|
if "Lyon" not in query and "France-IX" not in query and "FR-IX" not in query:
|
|
return None
|
|
return {
|
|
"lat": "45.764",
|
|
"lon": "4.8357",
|
|
"display_name": "Lyon, Auvergne-Rhône-Alpes, France",
|
|
"address": {"city": "Lyon", "country": "France"},
|
|
}
|
|
|
|
monkeypatch.setattr(bgp_collector_locations, "_geocode_online", _fake_geocode)
|
|
candidates, attempted = collect_bgp_collector_location_candidates(
|
|
collector="rrc-mystery",
|
|
city="Lyon",
|
|
country="France",
|
|
)
|
|
assert attempted, "Nominatim plan should run"
|
|
online = [c for c in candidates if c.source == "nominatim_online_geocode"]
|
|
assert online, "online resolver must produce a candidate when registry misses"
|
|
assert online[0].needs_confirmation is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_collect_bgp_collector_location_uses_llm_when_candidates_empty(monkeypatch):
|
|
llm_candidate = bgp_collector_locations.LocationCandidate(
|
|
latitude=45.764,
|
|
longitude=4.8357,
|
|
display_name="Lyon, France",
|
|
precision="city",
|
|
confidence=0.74,
|
|
query="llm_factcheck:bgp_collector:rrc-mystery",
|
|
source="llm_location_factcheck",
|
|
source_note="LLM location factcheck fallback",
|
|
matched_fields=("collector",),
|
|
needs_confirmation=True,
|
|
city="Lyon",
|
|
country="France",
|
|
)
|
|
monkeypatch.setattr(
|
|
bgp_api,
|
|
"get_bgp_collector_location_dict",
|
|
lambda _collector: {},
|
|
)
|
|
monkeypatch.setattr(
|
|
bgp_api,
|
|
"collect_bgp_collector_location_candidates",
|
|
lambda **_kwargs: ([], ["Lyon, France"]),
|
|
)
|
|
|
|
async def _fallback(**_kwargs):
|
|
return LocationLLMFallbackResult(
|
|
candidates=[llm_candidate],
|
|
attempted_queries=["llm_factcheck:bgp_collector:rrc-mystery"],
|
|
)
|
|
|
|
monkeypatch.setattr(bgp_api, "get_ai_provider_client", AsyncMock(return_value=object()))
|
|
monkeypatch.setattr(bgp_api, "collect_llm_location_fallback_candidate", _fallback)
|
|
|
|
response = await bgp_api.collect_bgp_collector_location(
|
|
"rrc-mystery",
|
|
bgp_api.CollectBGPCollectorLocationRequest(city="Lyon", country="France"),
|
|
current_user=object(),
|
|
db=AsyncMock(),
|
|
)
|
|
|
|
assert response["success"] is True
|
|
assert response["best_candidate"]["source"] == "llm_location_factcheck"
|
|
assert response["best_candidate"]["needs_confirmation"] is True
|
|
assert response["attempted_queries"] == [
|
|
"Lyon, France",
|
|
"llm_factcheck:bgp_collector:rrc-mystery",
|
|
]
|
|
|
|
|
|
# ── BGP event resolver ─────────────────────────────────────────────
|
|
|
|
|
|
def test_event_resolver_inherits_from_owning_collector():
|
|
geo = resolve_bgp_event_geo_dict("rrc25")
|
|
assert geo["city"] == "Amsterdam"
|
|
assert geo["country"] == "Netherlands"
|
|
assert geo["source"] == "inherited_from_collector"
|
|
assert geo["precision"] == "city"
|
|
|
|
|
|
def test_event_resolver_does_not_match_unrelated_collectors():
|
|
"""Regression: passing operator=RIPE NCC must NOT make every collector match."""
|
|
rrc12 = resolve_bgp_event_geo_dict("rrc12")
|
|
rrc25 = resolve_bgp_event_geo_dict("rrc25")
|
|
assert rrc12["city"] == "Frankfurt"
|
|
assert rrc25["city"] == "Amsterdam"
|
|
assert rrc12["latitude"] != rrc25["latitude"]
|
|
|
|
|
|
def test_event_resolver_uses_source_coordinates_when_present():
|
|
geo = resolve_bgp_event_geo_dict(
|
|
"rrc12",
|
|
source_latitude=12.34,
|
|
source_longitude=56.78,
|
|
)
|
|
assert geo["latitude"] == pytest.approx(12.34)
|
|
assert geo["longitude"] == pytest.approx(56.78)
|
|
assert geo["precision"] == "precise"
|
|
assert geo["source"] == "source_coordinates"
|
|
|
|
|
|
def test_event_resolver_returns_empty_for_unknown_collector_without_source_coords():
|
|
geo = resolve_bgp_event_geo_dict("rrc-doesnotexist")
|
|
assert geo == {}
|
|
|
|
|
|
def test_event_resolver_full_result_carries_diagnostic_on_miss():
|
|
result = resolve_bgp_event_location(collector="rrc-doesnotexist")
|
|
assert result.location is None
|
|
assert result.diagnostic is not None
|