"""Coverage tests for adapter enrichment_locations declarations (PR L-a). Every adapter must make a conscious enrichment_locations declaration — a non-empty [(lat_field, lon_field)] for point adapters, or an explicit [] for adapters with no point coordinate. Registry-derived (iterates discover_adapters()), no hardcoded adapter lists. Plus synthetic-event tests for the two adapters with isolated event builders (usgs_quake._feature_to_event, nwis._build_event) proving the declared latitude/longitude paths actually resolve on event.data. The four inline-build point adapters (eonet, gdacs, wfigs_incidents, inciweb) construct events only inside poll() and are covered by the live end-to-end smoke instead. """ from datetime import datetime, timezone from pathlib import Path from unittest.mock import MagicMock from central.adapter_discovery import discover_adapters from central.config_models import AdapterConfig def test_every_adapter_explicitly_declares_enrichment_locations(): """Each adapter declares enrichment_locations in its OWN class body (not just inheriting the SourceAdapter default) — a conscious choice per adapter.""" missing = [ name for name, cls in discover_adapters().items() if "enrichment_locations" not in cls.__dict__ ] assert not missing, f"adapters missing an explicit enrichment_locations: {missing}" def test_enrichment_locations_shape_is_valid(): """Each declaration is a list of (str, str) tuples (possibly empty).""" for name, cls in discover_adapters().items(): locs = cls.enrichment_locations assert isinstance(locs, list), f"{name}: not a list" for tup in locs: assert isinstance(tup, tuple) and len(tup) == 2, f"{name}: bad tuple {tup!r}" assert all(isinstance(p, str) for p in tup), f"{name}: non-str path {tup!r}" def test_point_adapters_use_canonical_lat_lon_paths(): """Every point adapter (non-empty declaration) uses the same protocol keys ('latitude', 'longitude') — the convention FIRMS established.""" for name, cls in discover_adapters().items(): if cls.enrichment_locations: assert cls.enrichment_locations == [("latitude", "longitude")], ( f"{name} uses non-canonical paths: {cls.enrichment_locations}" ) def test_at_least_the_known_point_adapters_are_non_empty(): """Registry-derived sanity: the adapters that carry a point coordinate have a non-empty declaration. Derived by probing enrichment_locations, not a hardcoded list — guards against a regression that blanks them all.""" non_empty = { name for name, cls in discover_adapters().items() if cls.enrichment_locations } # firms is the original; there must be several point adapters now. assert "firms" in non_empty assert len(non_empty) >= 5, f"expected several point adapters, got {sorted(non_empty)}" # --- synthetic-event tests for the two isolated builders -------------------- def test_usgs_quake_event_exposes_top_level_latlon(): from central.adapters.usgs_quake import USGSQuakeAdapter config = AdapterConfig( name="usgs_quake", enabled=True, cadence_s=60, settings={}, updated_at=datetime.now(timezone.utc), ) adapter = USGSQuakeAdapter(config, MagicMock(), Path("/tmp/never_used.db")) feature = { "type": "Feature", "id": "test_q1", "properties": {"mag": 2.5, "place": "X", "time": 1715000000000, "updated": 1715000000000}, "geometry": {"type": "Point", "coordinates": [-116.2, 43.7, 10.5]}, } event = adapter._feature_to_event(feature) assert event is not None assert event.data["latitude"] == 43.7 assert event.data["longitude"] == -116.2 def test_nwis_event_mirrors_centroid_into_data(): from central.adapters.nwis import NWISAdapter config = AdapterConfig( name="nwis", enabled=True, cadence_s=900, settings={}, updated_at=datetime.now(timezone.utc), ) adapter = NWISAdapter(config, MagicMock(), Path("/tmp/never_used.db")) feature = { "geometry": {"type": "Point", "coordinates": [-90.25, 41.78]}, # (lon, lat) "properties": { "monitoring_location_id": "USGS-05420500", "time": "2026-05-20T00:00:00Z", "value": "123.0", "unit_of_measure": "ft", }, } event = adapter._build_event(feature, "00060") assert event is not None # latitude = centroid[1], longitude = centroid[0]; no axis swap. assert event.data["latitude"] == 41.78 assert event.data["longitude"] == -90.25 # Geo.centroid retained for existing rendering uses. assert event.geo.centroid == (-90.25, 41.78)