"""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"]), ) from app.services.location.llm_fallback import LocationSearchEvidenceResult async def _search_evidence(**_kwargs): return LocationSearchEvidenceResult( evidence=[ { "title": "RRC source", "url": "https://example.test/rrc", "snippet": "rrc-mystery is in Lyon.", } ], attempted_queries=["web_search:bgp_collector:rrc-mystery Lyon France physical location route collector city"], ) 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, "get_web_search_client", AsyncMock(return_value=object())) monkeypatch.setattr(bgp_api, "collect_location_search_evidence", _search_evidence) 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", "web_search:bgp_collector:rrc-mystery Lyon France physical location route collector city", "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