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
349 lines
12 KiB
Python
349 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from uuid import UUID
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.core.config import get_settings
|
|
from app.core.public_demo import PUBLIC_DEMO_PROJECT_ID
|
|
from app.db.session import get_db
|
|
from app.main import create_app
|
|
from app.models import AnalysisRun, Dataset, Detection, Export, Job, Segmentation
|
|
from app.schemas import (
|
|
DetectionRunListResponse,
|
|
DetectionRunResponse,
|
|
SegmentationRunListResponse,
|
|
SegmentationRunResponse,
|
|
)
|
|
from app.services.auth_service import AuthService
|
|
from app.services.detection_service import DetectionService
|
|
from app.services.segmentation_service import SegmentationService
|
|
|
|
|
|
GUEST_PROJECT_ID = PUBLIC_DEMO_PROJECT_ID
|
|
OTHER_PROJECT_ID = UUID("00000000-0000-0000-0000-000000000999")
|
|
DATASET_ID = UUID("00000000-0000-0000-0000-000000000201")
|
|
DETECTION_RUN_ID = UUID("00000000-0000-0000-0000-000000000202")
|
|
SEGMENTATION_RUN_ID = UUID("00000000-0000-0000-0000-000000000203")
|
|
DETECTION_ID = UUID("00000000-0000-0000-0000-000000000204")
|
|
SEGMENTATION_ID = UUID("00000000-0000-0000-0000-000000000205")
|
|
EXPORT_ID = UUID("00000000-0000-0000-0000-000000000206")
|
|
JOB_ID = UUID("00000000-0000-0000-0000-000000000207")
|
|
|
|
|
|
class FakeSession:
|
|
def __init__(self, objects: dict[tuple[type, UUID], object]) -> None:
|
|
self.objects = objects
|
|
|
|
def get(self, model, row_id):
|
|
return self.objects.get((model, row_id))
|
|
|
|
|
|
def _guest_client(monkeypatch, db: FakeSession) -> TestClient:
|
|
password_hash = AuthService.hash_password(
|
|
"operator-password",
|
|
salt=b"guest-scope-test-salt",
|
|
iterations=100_000,
|
|
)
|
|
monkeypatch.setenv("GEOINTEL_AUTH_ENABLED", "true")
|
|
monkeypatch.setenv("GEOINTEL_AUTH_USERNAME", "operator")
|
|
monkeypatch.setenv("GEOINTEL_AUTH_PASSWORD_HASH", password_hash)
|
|
monkeypatch.setenv(
|
|
"GEOINTEL_AUTH_SESSION_SECRET",
|
|
"guest-scope-test-session-secret-value",
|
|
)
|
|
monkeypatch.setenv("GEOINTEL_GUEST_ACCESS_ENABLED", "true")
|
|
monkeypatch.setenv("GEOINTEL_GUEST_DISPLAY_NAME", "Gast")
|
|
|
|
client = TestClient(create_app())
|
|
|
|
def fake_db():
|
|
yield db
|
|
|
|
client.app.dependency_overrides[get_db] = fake_db
|
|
token = AuthService.create_session_token(
|
|
"Gast",
|
|
get_settings(),
|
|
role="guest",
|
|
project_id=GUEST_PROJECT_ID,
|
|
)
|
|
client.cookies.set("geointel_session", token)
|
|
return client
|
|
|
|
|
|
def _project_objects(project_id: UUID, export_path: Path) -> dict[tuple[type, UUID], object]:
|
|
return {
|
|
(Dataset, DATASET_ID): Dataset(
|
|
id=DATASET_ID,
|
|
project_id=project_id,
|
|
name="scope-test.tif",
|
|
dataset_type="raster",
|
|
source="fixture",
|
|
),
|
|
(AnalysisRun, DETECTION_RUN_ID): AnalysisRun(
|
|
id=DETECTION_RUN_ID,
|
|
project_id=project_id,
|
|
dataset_id=DATASET_ID,
|
|
analysis_type="detection",
|
|
status="success",
|
|
parameters_json={},
|
|
),
|
|
(AnalysisRun, SEGMENTATION_RUN_ID): AnalysisRun(
|
|
id=SEGMENTATION_RUN_ID,
|
|
project_id=project_id,
|
|
dataset_id=DATASET_ID,
|
|
analysis_type="segmentation",
|
|
status="success",
|
|
parameters_json={},
|
|
),
|
|
(Detection, DETECTION_ID): Detection(
|
|
id=DETECTION_ID,
|
|
project_id=project_id,
|
|
dataset_id=DATASET_ID,
|
|
analysis_run_id=DETECTION_RUN_ID,
|
|
model_name="fixture-detector",
|
|
class_name="building",
|
|
confidence=0.9,
|
|
geometry="SRID=4326;POINT (5 51)",
|
|
),
|
|
(Segmentation, SEGMENTATION_ID): Segmentation(
|
|
id=SEGMENTATION_ID,
|
|
project_id=project_id,
|
|
dataset_id=DATASET_ID,
|
|
analysis_run_id=SEGMENTATION_RUN_ID,
|
|
model_name="fixture-segmenter",
|
|
class_name="building",
|
|
confidence=0.9,
|
|
geometry="SRID=4326;MULTIPOLYGON (((5 51, 5.1 51, 5.1 51.1, 5 51)))",
|
|
),
|
|
(Export, EXPORT_ID): Export(
|
|
id=EXPORT_ID,
|
|
project_id=project_id,
|
|
export_type="dataset_geojson",
|
|
storage_path=str(export_path),
|
|
metadata_json={},
|
|
),
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
f"/api/v1/detection/runs/{DETECTION_RUN_ID}",
|
|
f"/api/v1/detection/runs/{DETECTION_RUN_ID}/detections",
|
|
f"/api/v1/detection/runs/{DETECTION_RUN_ID}/geojson",
|
|
f"/api/v1/detection/datasets/{DATASET_ID}/detections",
|
|
f"/api/v1/detection/datasets/{DATASET_ID}/geojson",
|
|
f"/api/v1/detection/detections/{DETECTION_ID}",
|
|
f"/api/v1/segmentation/runs/{SEGMENTATION_RUN_ID}",
|
|
f"/api/v1/segmentation/runs/{SEGMENTATION_RUN_ID}/segmentations",
|
|
f"/api/v1/segmentation/runs/{SEGMENTATION_RUN_ID}/geojson",
|
|
f"/api/v1/segmentation/datasets/{DATASET_ID}/segmentations",
|
|
f"/api/v1/segmentation/datasets/{DATASET_ID}/geojson",
|
|
f"/api/v1/segmentation/segmentations/{SEGMENTATION_ID}",
|
|
f"/api/v1/exports/{EXPORT_ID}",
|
|
f"/api/v1/exports/{EXPORT_ID}/content",
|
|
f"/api/v1/exports/{EXPORT_ID}/download",
|
|
f"/api/v1/exports/projects/{OTHER_PROJECT_ID}/exports",
|
|
],
|
|
)
|
|
def test_matching_guest_query_cannot_authorize_another_projects_resource(
|
|
path: str,
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
artifact = tmp_path / "other-project.geojson"
|
|
artifact.write_text('{"type":"FeatureCollection","features":[]}', encoding="utf-8")
|
|
client = _guest_client(monkeypatch, FakeSession(_project_objects(OTHER_PROJECT_ID, artifact)))
|
|
|
|
response = client.get(f"{path}?project_id={GUEST_PROJECT_ID}")
|
|
|
|
assert response.status_code == 403
|
|
assert response.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("path", "payload"),
|
|
[
|
|
(
|
|
"/api/v1/detection/run",
|
|
{"project_id": str(OTHER_PROJECT_ID), "dataset_id": str(DATASET_ID), "model_id": "fixture"},
|
|
),
|
|
(
|
|
"/api/v1/detection/run-async",
|
|
{"project_id": str(OTHER_PROJECT_ID), "dataset_id": str(DATASET_ID), "model_id": "fixture"},
|
|
),
|
|
(
|
|
"/api/v1/segmentation/run",
|
|
{"project_id": str(OTHER_PROJECT_ID), "dataset_id": str(DATASET_ID), "model_id": "fixture"},
|
|
),
|
|
(
|
|
"/api/v1/segmentation/run-async",
|
|
{"project_id": str(OTHER_PROJECT_ID), "dataset_id": str(DATASET_ID), "model_id": "fixture"},
|
|
),
|
|
(
|
|
"/api/v1/detection/runs/{run_id}/qa/reference".format(run_id=DETECTION_RUN_ID),
|
|
{"reference_dataset_id": str(DATASET_ID)},
|
|
),
|
|
(
|
|
"/api/v1/segmentation/runs/{run_id}/qa/reference".format(run_id=SEGMENTATION_RUN_ID),
|
|
{"reference_dataset_id": str(DATASET_ID)},
|
|
),
|
|
(
|
|
"/api/v1/exports/geojson",
|
|
{"export_kind": "dataset", "dataset_id": str(DATASET_ID)},
|
|
),
|
|
(
|
|
"/api/v1/exports/geojson",
|
|
{"export_kind": "detection_run", "analysis_run_id": str(DETECTION_RUN_ID)},
|
|
),
|
|
(
|
|
"/api/v1/exports/geojson",
|
|
{"export_kind": "segmentation_run", "analysis_run_id": str(SEGMENTATION_RUN_ID)},
|
|
),
|
|
(
|
|
"/api/v1/exports/metadata",
|
|
{"project_id": str(OTHER_PROJECT_ID)},
|
|
),
|
|
(
|
|
"/api/v1/exports/report",
|
|
{"project_id": str(OTHER_PROJECT_ID)},
|
|
),
|
|
(
|
|
"/api/v1/exports/map-result",
|
|
{
|
|
"project_id": str(OTHER_PROJECT_ID),
|
|
"mode": "current",
|
|
"dataset_id": str(DATASET_ID),
|
|
"bbox": {"min_x": 5.0, "min_y": 51.0, "max_x": 5.1, "max_y": 51.1, "crs": "EPSG:4326"},
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def test_matching_guest_query_cannot_override_post_body_or_target_scope(
|
|
path: str,
|
|
payload: dict,
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
artifact = tmp_path / "other-project.geojson"
|
|
artifact.write_text("{}", encoding="utf-8")
|
|
client = _guest_client(monkeypatch, FakeSession(_project_objects(OTHER_PROJECT_ID, artifact)))
|
|
|
|
response = client.post(f"{path}?project_id={GUEST_PROJECT_ID}", json=payload)
|
|
|
|
assert response.status_code == 403
|
|
assert response.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED"
|
|
|
|
|
|
def test_guest_can_still_read_and_download_its_own_resources(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
artifact = tmp_path / "demo.geojson"
|
|
artifact.write_text('{"type":"FeatureCollection","features":[]}', encoding="utf-8")
|
|
client = _guest_client(monkeypatch, FakeSession(_project_objects(GUEST_PROJECT_ID, artifact)))
|
|
suffix = f"?project_id={GUEST_PROJECT_ID}"
|
|
|
|
detection = client.get(f"/api/v1/detection/runs/{DETECTION_RUN_ID}{suffix}")
|
|
segmentation = client.get(f"/api/v1/segmentation/runs/{SEGMENTATION_RUN_ID}{suffix}")
|
|
export = client.get(f"/api/v1/exports/{EXPORT_ID}{suffix}")
|
|
download = client.get(f"/api/v1/exports/{EXPORT_ID}/download{suffix}")
|
|
|
|
assert detection.status_code == 200
|
|
assert segmentation.status_code == 200
|
|
assert export.status_code == 200
|
|
assert download.status_code == 200
|
|
assert download.json()["type"] == "FeatureCollection"
|
|
|
|
|
|
def test_guest_run_lists_and_new_runs_remain_bound_to_the_session_project(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
artifact = tmp_path / "demo.geojson"
|
|
artifact.write_text("{}", encoding="utf-8")
|
|
client = _guest_client(monkeypatch, FakeSession(_project_objects(GUEST_PROJECT_ID, artifact)))
|
|
observed: list[UUID] = []
|
|
|
|
def detection_list(_db, *, project_id, **_kwargs):
|
|
observed.append(project_id)
|
|
return DetectionRunListResponse(items=[], total=0, limit=50, offset=0, truncated=False)
|
|
|
|
def segmentation_list(_db, *, project_id, **_kwargs):
|
|
observed.append(project_id)
|
|
return SegmentationRunListResponse(items=[], total=0, limit=50, offset=0, truncated=False)
|
|
|
|
def detection_run(**kwargs):
|
|
observed.append(kwargs["project_id"])
|
|
return DetectionRunResponse(
|
|
analysis_run_id=DETECTION_RUN_ID,
|
|
job_id=JOB_ID,
|
|
project_id=kwargs["project_id"],
|
|
dataset_id=kwargs["dataset_id"],
|
|
model_id=kwargs["model_id"],
|
|
status="success",
|
|
detection_count=0,
|
|
message="Demo run completed",
|
|
)
|
|
|
|
def segmentation_run(**kwargs):
|
|
observed.append(kwargs["project_id"])
|
|
return SegmentationRunResponse(
|
|
analysis_run_id=SEGMENTATION_RUN_ID,
|
|
job_id=JOB_ID,
|
|
project_id=kwargs["project_id"],
|
|
dataset_id=kwargs["dataset_id"],
|
|
model_id=kwargs["model_id"],
|
|
status="success",
|
|
segmentation_count=0,
|
|
message="Demo run completed",
|
|
)
|
|
|
|
monkeypatch.setattr(DetectionService, "list_runs", detection_list)
|
|
monkeypatch.setattr(SegmentationService, "list_runs", segmentation_list)
|
|
monkeypatch.setattr(DetectionService, "run_detection", detection_run)
|
|
monkeypatch.setattr(SegmentationService, "run_segmentation", segmentation_run)
|
|
|
|
def enqueue_detection(**kwargs):
|
|
observed.append(kwargs["project_id"])
|
|
return Job(
|
|
id=JOB_ID,
|
|
job_type="detection.run",
|
|
status="queued",
|
|
project_id=kwargs["project_id"],
|
|
dataset_id=kwargs["dataset_id"],
|
|
parameters_json={},
|
|
)
|
|
|
|
monkeypatch.setattr(DetectionService, "enqueue_detection", enqueue_detection)
|
|
|
|
def enqueue_segmentation(**kwargs):
|
|
observed.append(kwargs["project_id"])
|
|
return Job(
|
|
id=JOB_ID,
|
|
job_type="segmentation.run",
|
|
status="queued",
|
|
project_id=kwargs["project_id"],
|
|
dataset_id=kwargs["dataset_id"],
|
|
parameters_json={},
|
|
)
|
|
|
|
monkeypatch.setattr(SegmentationService, "enqueue_segmentation", enqueue_segmentation)
|
|
query = f"?project_id={GUEST_PROJECT_ID}"
|
|
payload = {"project_id": str(GUEST_PROJECT_ID), "dataset_id": str(DATASET_ID), "model_id": "fixture"}
|
|
|
|
responses = [
|
|
client.get(f"/api/v1/detection/runs{query}"),
|
|
client.get(f"/api/v1/segmentation/runs{query}"),
|
|
client.post(f"/api/v1/detection/run{query}", json=payload),
|
|
client.post(f"/api/v1/detection/run-async{query}", json=payload),
|
|
client.post(f"/api/v1/segmentation/run{query}", json=payload),
|
|
client.post(f"/api/v1/segmentation/run-async{query}", json=payload),
|
|
]
|
|
|
|
assert all(response.status_code == 200 for response in responses)
|
|
assert observed == [GUEST_PROJECT_ID] * 6
|