- Talks to the OFFICIAL GOV.UK Fuel Finder API instead of the public CSV mirror: OAuth token (/api/v1/oauth/generate_access_token), batched paging (500/page via batch-number) over /api/v1/pfs and /api/v1/pfs/fuel-prices, joined on node_id. - Price strings parsed to pence with pounds->pence correction (< 2.0); 50-500p sane band keeps junk out of the cheapest reference. - Fuel mapping: E5/E10 as-is; B7/B7_STANDARD/B7S/B7P/B10 -> DIESEL; HVO/SDV dropped (app tracks E5/E10/DIESEL only). - lat/lng parsed from nested location strings. - Output contract IDENTICAL to fuelboard-relay (/health + /api/v1/stations), so the app/widget/alerts work unchanged; switch = point app at :8789. - CSV mirror kept as automatic fallback (FUEL_SOURCE=auto) so the full path is testable before GOV.UK One Login creds exist. - 12 unit tests against documented API shapes (no network).
161 lines
6.3 KiB
Python
161 lines
6.3 KiB
Python
"""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}
|