Initial GeoIntel V1 foundation
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.main import app
|
||||
from app.models import Dataset, Export, Project, QualityCheck
|
||||
from app.schemas.export import ExportCreateResponse
|
||||
from app.services.export_service import ExportService
|
||||
from app.services.storage_service import StorageService
|
||||
|
||||
|
||||
class FakeQuery:
|
||||
def __init__(self, rows):
|
||||
self.rows = rows
|
||||
|
||||
def filter(self, *_args):
|
||||
return self
|
||||
|
||||
def order_by(self, *_args):
|
||||
return self
|
||||
|
||||
def offset(self, _offset):
|
||||
return self
|
||||
|
||||
def limit(self, _limit):
|
||||
return self
|
||||
|
||||
def count(self):
|
||||
return len(self.rows)
|
||||
|
||||
def all(self):
|
||||
return self.rows
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self, rows):
|
||||
self.rows = rows
|
||||
self.added = []
|
||||
|
||||
def get(self, model, row_id):
|
||||
row = self.rows.get((model, row_id))
|
||||
if row is not None:
|
||||
return row
|
||||
for item in self.added:
|
||||
if isinstance(item, model) and item.id == row_id:
|
||||
return item
|
||||
return None
|
||||
|
||||
def query(self, model):
|
||||
rows = [row for (row_model, _row_id), row in self.rows.items() if row_model is model]
|
||||
rows.extend([row for row in self.added if isinstance(row, model)])
|
||||
return FakeQuery(rows)
|
||||
|
||||
def add(self, row):
|
||||
self.added.append(row)
|
||||
|
||||
def commit(self):
|
||||
return None
|
||||
|
||||
def refresh(self, row):
|
||||
return row
|
||||
|
||||
|
||||
def test_dataset_geojson_export_persists_export_and_writes_artifact(tmp_path, monkeypatch) -> None:
|
||||
project_id = uuid4()
|
||||
dataset_id = uuid4()
|
||||
dataset_path = tmp_path / "input.geojson"
|
||||
dataset_path.write_text(json.dumps({"type": "FeatureCollection", "features": []}), encoding="utf-8")
|
||||
export_path = tmp_path / "exports" / "buildings.geojson"
|
||||
dataset = Dataset(
|
||||
id=dataset_id,
|
||||
project_id=project_id,
|
||||
name="buildings.geojson",
|
||||
dataset_type="vector",
|
||||
source="fixture",
|
||||
storage_path=str(dataset_path),
|
||||
status="ready",
|
||||
)
|
||||
db = FakeSession({(Dataset, dataset_id): dataset})
|
||||
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
|
||||
|
||||
response = ExportService.export_dataset_geojson(db, dataset_id, name="buildings")
|
||||
|
||||
exports = [item for item in db.added if isinstance(item, Export)]
|
||||
assert len(exports) == 1
|
||||
assert response.export_id == exports[0].id
|
||||
assert response.export_type == "dataset_geojson"
|
||||
assert response.metadata_json["feature_count"] == 0
|
||||
assert json.loads(export_path.read_text(encoding="utf-8"))["type"] == "FeatureCollection"
|
||||
|
||||
|
||||
def test_dataset_geojson_export_rejects_raster_dataset(tmp_path, monkeypatch) -> None:
|
||||
dataset_id = uuid4()
|
||||
dataset = Dataset(
|
||||
id=dataset_id,
|
||||
project_id=uuid4(),
|
||||
name="ortho.tif",
|
||||
dataset_type="raster",
|
||||
source="fixture",
|
||||
storage_path=str(tmp_path / "ortho.tif"),
|
||||
status="ready",
|
||||
)
|
||||
db = FakeSession({(Dataset, dataset_id): dataset})
|
||||
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(tmp_path / "unused.geojson"))
|
||||
|
||||
try:
|
||||
ExportService.export_dataset_geojson(db, dataset_id)
|
||||
except AppError as exc:
|
||||
assert exc.code == "INVALID_DATASET_TYPE"
|
||||
else:
|
||||
raise AssertionError("Raster datasets must not be exported as dataset GeoJSON")
|
||||
|
||||
|
||||
def test_project_metadata_export_persists_json_summary(tmp_path, monkeypatch) -> None:
|
||||
project_id = uuid4()
|
||||
dataset_id = uuid4()
|
||||
quality_check_id = uuid4()
|
||||
project = Project(id=project_id, name="Demo", region="Kempen", status="active")
|
||||
dataset = Dataset(
|
||||
id=dataset_id,
|
||||
project_id=project_id,
|
||||
name="reference.geojson",
|
||||
dataset_type="vector",
|
||||
source="fixture",
|
||||
dataset_role="reference",
|
||||
source_name="fixture",
|
||||
status="ready",
|
||||
metadata_json={"feature_count": 2},
|
||||
)
|
||||
quality_check = QualityCheck(
|
||||
id=quality_check_id,
|
||||
project_id=project_id,
|
||||
reference_dataset_id=dataset_id,
|
||||
check_type="demo_candidate_vs_reference",
|
||||
status="ok",
|
||||
score=0.5,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
previous_export_id = uuid4()
|
||||
previous_export = Export(
|
||||
id=previous_export_id,
|
||||
project_id=project_id,
|
||||
export_type="dataset_geojson",
|
||||
storage_path="storage/exports/previous.geojson",
|
||||
metadata_json={"source": "dataset"},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
export_path = tmp_path / "metadata.json"
|
||||
db = FakeSession(
|
||||
{
|
||||
(Project, project_id): project,
|
||||
(Dataset, dataset_id): dataset,
|
||||
(QualityCheck, quality_check_id): quality_check,
|
||||
(Export, previous_export_id): previous_export,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
|
||||
|
||||
response = ExportService.export_project_metadata(db, project_id)
|
||||
|
||||
payload = json.loads(export_path.read_text(encoding="utf-8"))
|
||||
assert response.export_type == "project_metadata_json"
|
||||
assert payload["project"]["id"] == str(project_id)
|
||||
assert payload["datasets"][0]["id"] == str(dataset_id)
|
||||
assert payload["quality_checks"][0]["id"] == str(quality_check_id)
|
||||
assert payload["exports"][0]["id"] == str(previous_export_id)
|
||||
assert response.metadata_json["export_count"] == 1
|
||||
|
||||
|
||||
def test_project_report_export_persists_html_artifact(tmp_path, monkeypatch) -> None:
|
||||
project_id = uuid4()
|
||||
dataset_id = uuid4()
|
||||
project = Project(id=project_id, name="Demo <Kempen>", description="QA report", region="Kempen", status="active")
|
||||
dataset = Dataset(
|
||||
id=dataset_id,
|
||||
project_id=project_id,
|
||||
name="reference.geojson",
|
||||
dataset_type="vector",
|
||||
source="fixture",
|
||||
dataset_role="reference",
|
||||
status="ready",
|
||||
metadata_json={"feature_count": 2},
|
||||
)
|
||||
previous_export_id = uuid4()
|
||||
previous_export = Export(
|
||||
id=previous_export_id,
|
||||
project_id=project_id,
|
||||
export_type="project_metadata_json",
|
||||
storage_path="storage/exports/metadata.json",
|
||||
metadata_json={"source": "project_metadata"},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
export_path = tmp_path / "report.html"
|
||||
db = FakeSession({(Project, project_id): project, (Dataset, dataset_id): dataset, (Export, previous_export_id): previous_export})
|
||||
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
|
||||
|
||||
response = ExportService.export_project_report(db, project_id)
|
||||
|
||||
html = export_path.read_text(encoding="utf-8")
|
||||
assert response.export_type == "project_report_html"
|
||||
assert response.metadata_json["format"] == "html"
|
||||
assert "<!doctype html>" in html
|
||||
assert "Demo <Kempen>" in html
|
||||
assert "reference.geojson" in html
|
||||
assert "Export History (1)" in html
|
||||
assert "project_metadata_json" in html
|
||||
|
||||
|
||||
def test_export_content_reads_persisted_artifact(tmp_path) -> None:
|
||||
export_id = uuid4()
|
||||
export_path = tmp_path / "artifact.json"
|
||||
export_path.write_text(json.dumps({"hello": "world"}), encoding="utf-8")
|
||||
export = Export(
|
||||
id=export_id,
|
||||
project_id=uuid4(),
|
||||
export_type="project_metadata_json",
|
||||
storage_path=str(export_path),
|
||||
metadata_json={},
|
||||
)
|
||||
db = FakeSession({(Export, export_id): export})
|
||||
|
||||
response = ExportService.get_export_content(db, export_id)
|
||||
|
||||
assert response.export_id == export_id
|
||||
assert response.content == {"hello": "world"}
|
||||
|
||||
|
||||
def test_export_download_path_rejects_missing_artifact(tmp_path) -> None:
|
||||
export_id = uuid4()
|
||||
export = Export(
|
||||
id=export_id,
|
||||
project_id=uuid4(),
|
||||
export_type="dataset_geojson",
|
||||
storage_path=str(tmp_path / "missing.geojson"),
|
||||
metadata_json={},
|
||||
)
|
||||
db = FakeSession({(Export, export_id): export})
|
||||
|
||||
try:
|
||||
ExportService.get_export_download_path(db, export_id)
|
||||
except AppError as exc:
|
||||
assert exc.code == "EXPORT_CONTENT_NOT_FOUND"
|
||||
else:
|
||||
raise AssertionError("Missing export artifacts must fail clearly")
|
||||
|
||||
|
||||
def test_export_geojson_endpoint_returns_canonical_envelope(monkeypatch) -> None:
|
||||
export_id = uuid4()
|
||||
dataset_id = uuid4()
|
||||
|
||||
monkeypatch.setattr(
|
||||
ExportService,
|
||||
"export_dataset_geojson",
|
||||
lambda *_args, **_kwargs: ExportCreateResponse(
|
||||
export_id=export_id,
|
||||
path="storage/exports/demo.geojson",
|
||||
status="ready",
|
||||
export_type="dataset_geojson",
|
||||
metadata_json={"source": "dataset"},
|
||||
),
|
||||
)
|
||||
|
||||
response = TestClient(app).post("/api/v1/exports/geojson", json={"dataset_id": str(dataset_id)})
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert set(payload) == {"data"}
|
||||
assert payload["data"]["export_id"] == str(export_id)
|
||||
assert payload["data"]["export_type"] == "dataset_geojson"
|
||||
|
||||
|
||||
def test_export_download_endpoint_returns_file_response(tmp_path, monkeypatch) -> None:
|
||||
export_id = uuid4()
|
||||
export_path = tmp_path / "download.geojson"
|
||||
export_path.write_text(json.dumps({"type": "FeatureCollection", "features": []}), encoding="utf-8")
|
||||
monkeypatch.setattr(ExportService, "get_export_download_path", lambda *_args, **_kwargs: export_path)
|
||||
|
||||
response = TestClient(app).get(f"/api/v1/exports/{export_id}/download")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"].startswith("application/json")
|
||||
assert "download.geojson" in response.headers["content-disposition"]
|
||||
assert response.json()["type"] == "FeatureCollection"
|
||||
|
||||
|
||||
def test_export_download_endpoint_returns_html_media_type(tmp_path, monkeypatch) -> None:
|
||||
export_id = uuid4()
|
||||
export_path = tmp_path / "report.html"
|
||||
export_path.write_text("<!doctype html><html><body>report</body></html>", encoding="utf-8")
|
||||
monkeypatch.setattr(ExportService, "get_export_download_path", lambda *_args, **_kwargs: export_path)
|
||||
|
||||
response = TestClient(app).get(f"/api/v1/exports/{export_id}/download")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"].startswith("text/html")
|
||||
assert "report.html" in response.headers["content-disposition"]
|
||||
assert "report" in response.text
|
||||
Reference in New Issue
Block a user