@@ -0,0 +1,210 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from django.core.management import CommandError
|
||||
|
||||
from apps.jobs.models import GeocodeLocationLookup
|
||||
from apps.jobs.services.geocoding import (
|
||||
CsvGeocodeProvider,
|
||||
GeoPoint,
|
||||
LocationMatch,
|
||||
LocationMatchResult,
|
||||
import_csv_geodata,
|
||||
parse_belgian_location_query,
|
||||
resolve_location,
|
||||
validate_csv_geodata,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_parse_belgian_location_query():
|
||||
postal, municipality = parse_belgian_location_query("Gent, 9000")
|
||||
assert postal == "9000"
|
||||
assert municipality == "gent"
|
||||
|
||||
postal, municipality = parse_belgian_location_query("1050 Ixelles")
|
||||
assert postal == "1050"
|
||||
assert municipality == "ixelles"
|
||||
|
||||
postal, municipality = parse_belgian_location_query("Brussel")
|
||||
assert postal is None
|
||||
assert municipality == "brussel"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_geocode_provider_resolves_postal_and_municipality_exactly():
|
||||
GeocodeLocationLookup.objects.create(
|
||||
source_name="fixture",
|
||||
source_version="2026-01-01",
|
||||
source_license_name="",
|
||||
source_license_url="",
|
||||
source_metadata={},
|
||||
query_kind="postal",
|
||||
query_value="9000",
|
||||
postal_code="9000",
|
||||
municipality="Gent",
|
||||
region="Vlaams-Brabant",
|
||||
latitude=Decimal("51.0500"),
|
||||
longitude=Decimal("3.7300"),
|
||||
confidence=Decimal("0.90"),
|
||||
)
|
||||
GeocodeLocationLookup.objects.create(
|
||||
source_name="fixture",
|
||||
source_version="2026-01-01",
|
||||
source_license_name="",
|
||||
source_license_url="",
|
||||
source_metadata={},
|
||||
query_kind="municipality",
|
||||
query_value="gent",
|
||||
postal_code="9000",
|
||||
municipality="Gent",
|
||||
region="Vlaams-Brabant",
|
||||
latitude=Decimal("51.0500"),
|
||||
longitude=Decimal("3.7300"),
|
||||
confidence=Decimal("0.90"),
|
||||
)
|
||||
|
||||
provider = CsvGeocodeProvider(source_name="fixture", source_version="2026-01-01")
|
||||
result = resolve_location("9000 Gent", provider)
|
||||
assert result.ambiguous is False
|
||||
assert result.location is not None
|
||||
assert result.location.postal_code == "9000"
|
||||
assert result.location.municipality == "Gent"
|
||||
assert result.location.point == GeoPoint(latitude=51.05, longitude=3.73)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_geocode_provider_marks_ambiguous_as_unknown():
|
||||
GeocodeLocationLookup.objects.create(
|
||||
source_name="fixture",
|
||||
source_version="2026-01-01",
|
||||
source_license_name="",
|
||||
source_license_url="",
|
||||
source_metadata={},
|
||||
query_kind="municipality",
|
||||
query_value="brussel",
|
||||
postal_code="1000",
|
||||
municipality="Brussel",
|
||||
region="Brussels Hoofdstedelijk Gewest",
|
||||
latitude=Decimal("50.8500"),
|
||||
longitude=Decimal("4.3500"),
|
||||
confidence=Decimal("0.85"),
|
||||
)
|
||||
GeocodeLocationLookup.objects.create(
|
||||
source_name="fixture",
|
||||
source_version="2026-01-01",
|
||||
source_license_name="",
|
||||
source_license_url="",
|
||||
source_metadata={},
|
||||
query_kind="municipality",
|
||||
query_value="brussel",
|
||||
postal_code="1000-200",
|
||||
municipality="Brussel",
|
||||
region="Brussels Hoofdstedelijk Gewest",
|
||||
latitude=Decimal("50.8400"),
|
||||
longitude=Decimal("4.3400"),
|
||||
confidence=Decimal("0.85"),
|
||||
)
|
||||
|
||||
provider = CsvGeocodeProvider(source_name="fixture", source_version="2026-01-01")
|
||||
result = resolve_location("Brussel", provider)
|
||||
assert result.ambiguous is True
|
||||
assert result.location is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_validate_csv_geodata_flags_duplicates_and_invalid_coordinates(tmp_path: Path):
|
||||
source = tmp_path / "geodata.csv"
|
||||
source.write_text(
|
||||
"postal_code,municipality,region,latitude,longitude\n"
|
||||
"9000,Gent,Vlaams-Brabant,51.05,3.73\n"
|
||||
"9000,Gent,Vlaams-Brabant,51.05,3.74\n"
|
||||
)
|
||||
with pytest.raises(CommandError, match="dubbel record"):
|
||||
validate_csv_geodata(source)
|
||||
|
||||
source.write_text(
|
||||
"postal_code,municipality,region,latitude,longitude\n"
|
||||
"9000,Gent,Vlaams-Brabant,99,3.73\n"
|
||||
)
|
||||
with pytest.raises(CommandError, match="latitude buiten bereik"):
|
||||
validate_csv_geodata(source)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_import_csv_geodata_is_atomic_without_replace(tmp_path: Path):
|
||||
source = tmp_path / "geodata.csv"
|
||||
source.write_text(
|
||||
"postal_code,municipality,region,latitude,longitude\n"
|
||||
"9000,Gent,Vlaams-Brabant,51.05,3.73\n"
|
||||
"1040,Brussel,Brussels Hoofdstedelijk Gewest,50.85,4.35\n"
|
||||
)
|
||||
GeocodeLocationLookup.objects.create(
|
||||
source_name="fixture",
|
||||
source_version="manual",
|
||||
source_license_name="",
|
||||
source_license_url="",
|
||||
source_metadata={},
|
||||
query_kind="municipality",
|
||||
query_value="gent",
|
||||
postal_code="9000",
|
||||
municipality="Gent",
|
||||
region="Vlaams-Brabant",
|
||||
latitude=Decimal("51.05"),
|
||||
longitude=Decimal("3.73"),
|
||||
confidence=Decimal("1.00"),
|
||||
)
|
||||
GeocodeLocationLookup.objects.create(
|
||||
source_name="fixture",
|
||||
source_version="manual",
|
||||
source_license_name="",
|
||||
source_license_url="",
|
||||
source_metadata={},
|
||||
query_kind="postal",
|
||||
query_value="9000",
|
||||
postal_code="9000",
|
||||
municipality="Gent",
|
||||
region="Vlaams-Brabant",
|
||||
latitude=Decimal("51.05"),
|
||||
longitude=Decimal("3.73"),
|
||||
confidence=Decimal("1.00"),
|
||||
)
|
||||
|
||||
with pytest.raises(CommandError, match="overschrijven zonder --replace"):
|
||||
import_csv_geodata(source, source_name="fixture", source_version="manual")
|
||||
assert GeocodeLocationLookup.objects.count() == 2
|
||||
|
||||
|
||||
def test_resolve_location_keeps_ambiguous_unknown() -> None:
|
||||
class StubProvider:
|
||||
def resolve(self, query: str):
|
||||
return [
|
||||
LocationMatch(
|
||||
postal_code="1000",
|
||||
municipality="Brussel",
|
||||
region="Brussels",
|
||||
point=None,
|
||||
confidence=0.85,
|
||||
source="csv",
|
||||
source_version="manual",
|
||||
metadata={},
|
||||
),
|
||||
LocationMatch(
|
||||
postal_code="1001",
|
||||
municipality="Brussel",
|
||||
region="Brussels",
|
||||
point=None,
|
||||
confidence=0.85,
|
||||
source="csv",
|
||||
source_version="manual",
|
||||
metadata={},
|
||||
),
|
||||
]
|
||||
|
||||
result = resolve_location("Brussel", StubProvider())
|
||||
assert result.ambiguous is True
|
||||
assert isinstance(result, LocationMatchResult)
|
||||
assert result.location is None
|
||||
Reference in New Issue
Block a user