"""Tests for mcgibs.geo — geocoding and bbox helpers.""" import httpx import respx from mcgibs.geo import bbox_from_point, expand_bbox, geocode from mcgibs.models import BBox # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- NOMINATIM_URL = "https://nominatim.openstreetmap.org/search" TOKYO_HIT = { "display_name": "Tokyo, Japan", "lat": "35.6762", "lon": "139.6503", "boundingbox": ["35.5191", "35.8170", "139.5601", "139.9200"], "osm_type": "relation", "importance": 0.82, } # --------------------------------------------------------------------------- # geocode() tests # --------------------------------------------------------------------------- @respx.mock async def test_geocode_success(): respx.get(NOMINATIM_URL).mock( return_value=httpx.Response(200, json=[TOKYO_HIT]), ) async with httpx.AsyncClient() as client: result = await geocode(client, "Tokyo") assert result is not None assert result.display_name == "Tokyo, Japan" assert result.lat == 35.6762 assert result.lon == 139.6503 assert result.osm_type == "relation" assert result.importance == 0.82 # BBox built from Nominatim's [south, north, west, east] order assert result.bbox.south == 35.5191 assert result.bbox.north == 35.8170 assert result.bbox.west == 139.5601 assert result.bbox.east == 139.9200 @respx.mock async def test_geocode_no_results(): respx.get(NOMINATIM_URL).mock( return_value=httpx.Response(200, json=[]), ) async with httpx.AsyncClient() as client: result = await geocode(client, "xyznonexistent") assert result is None @respx.mock async def test_geocode_repeated_calls(): """Each geocode() call makes an HTTP request (caching is caller's responsibility).""" route = respx.get(NOMINATIM_URL).mock( return_value=httpx.Response(200, json=[TOKYO_HIT]), ) async with httpx.AsyncClient() as client: first = await geocode(client, "Tokyo") second = await geocode(client, "Tokyo") # geocode() no longer caches — GIBSClient.resolve_place() handles that assert route.call_count == 2 assert first is not None assert second is not None assert first.display_name == second.display_name @respx.mock async def test_geocode_http_error(): """A 500 response should return None without raising an exception.""" respx.get(NOMINATIM_URL).mock( return_value=httpx.Response(500, text="Internal Server Error"), ) async with httpx.AsyncClient() as client: result = await geocode(client, "ServerError") assert result is None # --------------------------------------------------------------------------- # expand_bbox() tests # --------------------------------------------------------------------------- def test_expand_bbox(): bbox = BBox(west=10.0, south=20.0, east=20.0, north=30.0) expanded = expand_bbox(bbox, factor=0.1) # Width is 10 deg, so 10% padding = 1 deg each side. assert expanded.west < bbox.west assert expanded.east > bbox.east assert expanded.south < bbox.south assert expanded.north > bbox.north # Verify exact amounts (width = 10, dlon = 1; height = 10, dlat = 1). assert expanded.west == 9.0 assert expanded.east == 21.0 assert expanded.south == 19.0 assert expanded.north == 31.0 def test_expand_bbox_clamping(): """Expanding a bbox near the poles must clamp latitude to [-90, 90].""" polar = BBox(west=-170.0, south=85.0, east=170.0, north=89.5) expanded = expand_bbox(polar, factor=0.5) assert expanded.north <= 90.0 assert expanded.south >= -90.0 # Longitude should also stay within bounds. assert expanded.west >= -180.0 assert expanded.east <= 180.0 # --------------------------------------------------------------------------- # bbox_from_point() tests # --------------------------------------------------------------------------- def test_bbox_from_point(): bbox = bbox_from_point(35.0, 139.0, radius_deg=1.0) assert bbox.south == 34.0 assert bbox.north == 36.0 assert bbox.west == 138.0 assert bbox.east == 140.0 def test_bbox_from_point_clamping(): """A point near the pole should have its bbox clamped to valid lat range.""" bbox = bbox_from_point(89.5, 0.0, radius_deg=2.0) assert bbox.north == 90.0 # clamped, not 91.5 assert bbox.south == 87.5 assert bbox.west == -2.0 assert bbox.east == 2.0