"""Unit tests for the FuelBoard Relay API adapter. These exercise the normalisation layer against the DOCUMENTED Fuel Finder API shapes (developer.fuel-finder.service.gov.uk) without any network access: - prices arrive as decimal strings in pence ("0120.0000" = 120.0p) - some stations report pounds (< 2.0 → multiply by 100) - lat/lng are strings inside a nested location object - two batched endpoints (station info + fuel prices) join on node_id - fuel types include B7_STANDARD / B7P / B10 (diesel variants), HVO, SDV """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "app")) import main as relay # --------------------------------------------------------------------------- # _parse_price # --------------------------------------------------------------------------- class TestParsePrice: def test_pence_decimal_string(self): assert relay._parse_price("0120.0000") == 120.0 def test_plain_number(self): assert relay._parse_price(137.9) == 137.9 def test_pounds_denominated_multiplied(self): # Values below 2.0 are pounds: 1.299 → 129.9p assert relay._parse_price("1.299") == 129.9 def test_null_price(self): assert relay._parse_price(None) is None def test_out_of_band_rejected(self): # 1589p / 1.3p-style garbage must not poison the cheapest reference assert relay._parse_price("1589.0000") is None assert relay._parse_price("0.013") is None def test_garbage_string(self): assert relay._parse_price("abc") is None assert relay._parse_price("") is None # --------------------------------------------------------------------------- # _normalise_api_payload (the node_id join) # --------------------------------------------------------------------------- class TestNormaliseApiPayload: def test_join_and_fuel_mapping(self): stations_info = [{ "node_id": "abc123", "trading_name": "MORRISONS HALIFAX", "brand_name": "Morrisons", "location": { "address_line_1": "Haugh Shaw Road", "address_line_2": None, "postcode": "HX1 3TU", "latitude": "53.7265", "longitude": "-1.8580", }, "is_motorway_service_station": False, }] fuel_prices = [{ "node_id": "abc123", "trading_name": "MORRISONS HALIFAX", "fuel_prices": [ {"price": "137.9000", "fuel_type": "E10", "price_last_updated": "2026-08-12T08:15:23"}, {"price": "144.9000", "fuel_type": "E5", "price_last_updated": "2026-08-12T08:15:24"}, {"price": "149.9000", "fuel_type": "B7_STANDARD", "price_last_updated": "2026-08-12T08:15:25"}, ], }] stations = relay._normalise_api_payload(stations_info, fuel_prices) assert len(stations) == 1 s = stations[0] assert s["id"] == "abc123" assert s["name"] == "MORRISONS HALIFAX" assert s["brand"] == "Morrisons" assert s["lat"] == 53.7265 assert s["lng"] == -1.8580 assert s["prices"] == {"E10": 137.9, "E5": 144.9, "DIESEL": 149.9} assert s["price_updated"] == "2026-08-12T08:15:25" def test_unpriced_fuel_dropped(self): stations_info = [{ "node_id": "x1", "trading_name": "TEST", "location": {"latitude": "51.0", "longitude": "-1.0"}, }] fuel_prices = [{ "node_id": "x1", "fuel_prices": [ {"price": None, "fuel_type": "E10", "price_last_updated": None}, ], }] stations = relay._normalise_api_payload(stations_info, fuel_prices) # Station registered but no usable price → excluded assert stations == [] def test_unmapped_fuel_types_ignored(self): stations_info = [{ "node_id": "x2", "trading_name": "TEST", "location": {"latitude": "52.0", "longitude": "0.0"}, }] fuel_prices = [{ "node_id": "x2", "fuel_prices": [ {"price": "180.0000", "fuel_type": "HVO", "price_last_updated": None}, {"price": "190.0000", "fuel_type": "SDV", "price_last_updated": None}, ], }] stations = relay._normalise_api_payload(stations_info, fuel_prices) assert stations == [] # nothing the app tracks def test_missing_station_info_skipped(self): stations_info = [] # info not yet in the join table fuel_prices = [{ "node_id": "ghost", "fuel_prices": [{"price": "137.9000", "fuel_type": "E10"}], }] stations = relay._normalise_api_payload(stations_info, fuel_prices) assert stations == [] def test_bad_lat_lng_strings(self): stations_info = [{ "node_id": "y1", "trading_name": "BROKEN", "location": {"latitude": "not-a-number", "longitude": "-1.0"}, }] fuel_prices = [{ "node_id": "y1", "fuel_prices": [{"price": "137.9000", "fuel_type": "E10"}], }] stations = relay._normalise_api_payload(stations_info, fuel_prices) assert stations == [] # --------------------------------------------------------------------------- # CSV fallback still parses (sibling relay behaviour preserved) # --------------------------------------------------------------------------- class TestCsvFallback: def test_parse_csv_to_stations(self): text = ( "forecourts.node_id,forecourts.trading_name,forecourts.brand_name," "forecourts.location.address_line_1,forecourts.location.postcode," "forecourts.location.latitude,forecourts.location.longitude," "forecourts.fuel_price.E10,forecourts.fuel_price.E5,forecourts.fuel_price.B7S\n" "c1,Morrisons Halifax,Morrisons,Haugh Shaw Road,HX1 3TU," "53.7265,-1.8580,137.9,144.9,144.9\n" "c2,Tesco Express,Asda,,YO10 4AB,53.95,-1.08,,,\n" ) stations = relay._parse_csv_to_stations(text) assert len(stations) == 1 # c2 sells nothing we track assert stations[0]["id"] == "c1" assert stations[0]["prices"] == {"E10": 137.9, "E5": 144.9, "DIESEL": 144.9}