Initial public release
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""The paged readers' loop protections, exercised rather than assumed.
|
||||
|
||||
GRB and official vector both refuse a repeated page URL and bound the page
|
||||
count, and GRB deduplicates on feature identity. None of that had a test, so
|
||||
none of it was known to work — the same category as the redirect handler that
|
||||
turned out to be dead code while looking like protection.
|
||||
|
||||
A provider that answers every page with a "next" link pointing back at itself
|
||||
is not hypothetical: it is what a misconfigured cursor or a caching proxy in
|
||||
front of an OGC endpoint produces.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.core.errors import AppError
|
||||
from app.services.grb_acquisition_service import GrbAcquisitionService
|
||||
from tests.test_sprint239_bounded_grb_acquisition import JsonResponse, polygon_feature
|
||||
|
||||
COLLECTION_ITEMS = "https://geo.api.vlaanderen.be/GRB/ogc/features/v1/collections/GBG/items"
|
||||
SCOPE = Polygon([(5.15, 51.18), (5.17, 51.18), (5.17, 51.20), (5.15, 51.20), (5.15, 51.18)])
|
||||
|
||||
|
||||
def _building(feature_id: str) -> dict:
|
||||
return polygon_feature(
|
||||
feature_id,
|
||||
[(5.155, 51.185), (5.175, 51.185), (5.175, 51.195), (5.155, 51.195), (5.155, 51.185)],
|
||||
)
|
||||
|
||||
|
||||
def _fetch(opener):
|
||||
return GrbAcquisitionService._fetch_features(
|
||||
GrbAcquisitionService._product("buildings"),
|
||||
SCOPE,
|
||||
SCOPE.bounds,
|
||||
"bounded_selection",
|
||||
Settings(_env_file=None),
|
||||
opener,
|
||||
)
|
||||
|
||||
|
||||
def test_a_next_link_pointing_at_itself_is_refused() -> None:
|
||||
requests: list[str] = []
|
||||
|
||||
def opener(request, timeout): # noqa: ARG001
|
||||
requests.append(request.full_url)
|
||||
return JsonResponse(
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [_building("GBG.1")],
|
||||
"links": [{"rel": "next", "href": f"{COLLECTION_ITEMS}?cursor=stuck"}],
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(AppError) as exc_info:
|
||||
_fetch(opener)
|
||||
|
||||
assert exc_info.value.code == "GRB_PROVIDER_PAGINATION_LOOP"
|
||||
# Refused on the second sighting, not after exhausting the page budget.
|
||||
assert len(requests) == 2
|
||||
|
||||
|
||||
def test_an_endless_chain_of_fresh_pages_stops_at_the_page_limit() -> None:
|
||||
"""Distinct URLs defeat the loop check, so the page budget is the backstop."""
|
||||
|
||||
settings = Settings(_env_file=None)
|
||||
requests: list[str] = []
|
||||
|
||||
def opener(request, timeout): # noqa: ARG001
|
||||
requests.append(request.full_url)
|
||||
cursor = len(requests)
|
||||
return JsonResponse(
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [_building(f"GBG.{cursor}")],
|
||||
"links": [{"rel": "next", "href": f"{COLLECTION_ITEMS}?cursor=p{cursor}"}],
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(AppError) as exc_info:
|
||||
_fetch(opener)
|
||||
|
||||
assert exc_info.value.code == "GRB_SELECTION_TOO_LARGE"
|
||||
assert exc_info.value.status_code == 422
|
||||
assert len(requests) == settings.grb_max_pages
|
||||
|
||||
|
||||
def test_a_repeated_feature_across_pages_is_counted_once() -> None:
|
||||
"""Two pages, distinct URLs, overlapping content.
|
||||
|
||||
Unlike a repeated URL this is not necessarily provider misbehaviour — a
|
||||
cursor over a changing table can hand back a record twice — so the reader
|
||||
keeps it once rather than failing the acquisition.
|
||||
"""
|
||||
|
||||
def opener(request, timeout): # noqa: ARG001
|
||||
if "cursor=next" in request.full_url:
|
||||
return JsonResponse(
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [_building("GBG.1"), _building("GBG.2")],
|
||||
"links": [],
|
||||
}
|
||||
)
|
||||
return JsonResponse(
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [_building("GBG.1")],
|
||||
"links": [{"rel": "next", "href": f"{COLLECTION_ITEMS}?cursor=next"}],
|
||||
}
|
||||
)
|
||||
|
||||
features, transfer = _fetch(opener)
|
||||
|
||||
assert {feature["id"] for feature in features} == {"GBG:GBG.1", "GBG:GBG.2"}
|
||||
assert transfer["candidate_feature_count"] == 3
|
||||
assert transfer["feature_count"] == 2
|
||||
Reference in New Issue
Block a user