Skip to content

Commit dd56e6d

Browse files
committed
fix(backend): validate aspatial geopackage layer writes & add test coverage
1 parent 483c663 commit dd56e6d

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

backend/geolibre_server/geolibre_server/app/vector.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ def _write_geopackage(target: Path, geojson: dict, layer: Optional[str]) -> tupl
229229
status_code=404,
230230
detail=f"Layer '{layer}' not found in {target.name}",
231231
)
232+
if layer not in spatial:
233+
raise HTTPException(
234+
status_code=400,
235+
detail=f"Layer '{layer}' is an aspatial table and cannot be written",
236+
)
232237
target_layer = layer
233238
elif spatial:
234239
# Match the reader's "first feature layer" default (gpkg-reader.ts

backend/geolibre_server/tests/test_vector.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,29 @@ def test_write_geopackage_rejects_unknown_layer(tmp_path) -> None:
315315
assert exc.value.status_code == 404
316316

317317

318+
@requires_geopandas
319+
def test_write_geopackage_rejects_aspatial_layer(tmp_path, monkeypatch) -> None:
320+
import geopandas as gpd
321+
import pandas as pd
322+
323+
src = tmp_path / "layer.gpkg"
324+
gpd.GeoDataFrame.from_features(_edited("a")["features"], crs="EPSG:4326").to_file(
325+
src, layer="places", driver="GPKG"
326+
)
327+
328+
def mock_list_layers(path):
329+
return pd.DataFrame({
330+
"name": ["places", "aspatial_data"],
331+
"geometry_type": ["Polygon", None]
332+
})
333+
monkeypatch.setattr(gpd, "list_layers", mock_list_layers)
334+
335+
with pytest.raises(HTTPException) as exc:
336+
vector_write(WriteVectorRequest(path=str(src), geojson=_edited("b"), layer="aspatial_data"))
337+
assert exc.value.status_code == 400
338+
assert "aspatial table" in exc.value.detail
339+
340+
318341
@requires_geopandas
319342
def test_write_geopackage_defaults_to_first_feature_layer(tmp_path) -> None:
320343
# With no layer specified, write-back targets the first feature layer, the

backend/geolibre_server/tests/test_vector_ops.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,39 @@ def test_fix_geometries_no_op_on_valid_layer() -> None:
11961196
assert any("already valid" in m for m in messages)
11971197

11981198

1199+
@requires_geopandas
1200+
def test_fix_geometries_leaves_unfixable_unchanged(monkeypatch) -> None:
1201+
# A degenerate polygon (e.g., <3 distinct points) that make_valid cannot repair
1202+
# into a valid polygonal area will either raise or return empty. The tool should
1203+
# leave the original unchanged and increment the unfixable count.
1204+
import shapely.validation
1205+
1206+
def mock_make_valid(geom):
1207+
raise ValueError("Simulated unfixable geometry")
1208+
1209+
monkeypatch.setattr(shapely.validation, "make_valid", mock_make_valid)
1210+
1211+
degenerate = {
1212+
"type": "FeatureCollection",
1213+
"features": [
1214+
{
1215+
"type": "Feature",
1216+
"properties": {"name": "degenerate"},
1217+
"geometry": {
1218+
"type": "Polygon",
1219+
"coordinates": [[[0, 0], [1, 1], [0, 0]]],
1220+
},
1221+
}
1222+
],
1223+
}
1224+
geojson, messages = run_vector_tool("fix-geometries", degenerate)
1225+
assert len(geojson["features"]) == 1
1226+
# GeoPandas pads the LinearRing to 4 coordinates when loading/dumping
1227+
coords = geojson["features"][0]["geometry"]["coordinates"]
1228+
assert coords == [[[0.0, 0.0], [1.0, 1.0], [0.0, 0.0], [0.0, 0.0]]]
1229+
assert any("1 could not be repaired" in m for m in messages)
1230+
1231+
11991232
@requires_geopandas
12001233
def test_validity_anchor_parses_scientific_notation() -> None:
12011234
anchor = vector_ops._validity_anchor("Self-intersection[1.5e-10 -2.3e-05]", None)

0 commit comments

Comments
 (0)