fix(metadata): sync project metadata endpoints

This commit is contained in:
2026-06-11 11:15:59 +08:00
parent 78af7ecfb3
commit f61be3685f
8 changed files with 357 additions and 12 deletions
+92
View File
@@ -0,0 +1,92 @@
from types import SimpleNamespace
from uuid import uuid4
import pytest
from fastapi import HTTPException
from sqlalchemy.exc import SQLAlchemyError
from tests.conftest import install_stub, load_module_from_path
def _load_meta_module(monkeypatch):
install_stub(monkeypatch, "app.auth", package=True)
install_stub(
monkeypatch,
"app.auth.project_dependencies",
{
"ProjectContext": object,
"get_project_context": lambda: None,
"get_project_pg_session": lambda: None,
"get_project_timescale_connection": lambda: None,
"get_metadata_repository": lambda: None,
},
)
install_stub(
monkeypatch,
"app.auth.metadata_dependencies",
{"get_current_metadata_user": lambda: None},
)
return load_module_from_path(
"tests_meta_endpoints_module",
"app/api/v1/endpoints/meta.py",
)
@pytest.mark.anyio
async def test_meta_project_returns_map_extent(monkeypatch):
module = _load_meta_module(monkeypatch)
project_id = uuid4()
repo = SimpleNamespace(
get_project_by_id=lambda _project_id: None,
get_geoserver_config=lambda _project_id: None,
)
async def get_project_by_id(_project_id):
return SimpleNamespace(
id=project_id,
name="Demo Project",
code="demo",
description="desc",
gs_workspace="workspace",
map_extent={"xmin": 1, "ymin": 2, "xmax": 3, "ymax": 4},
status="active",
)
async def get_geoserver_config(_project_id):
return None
repo.get_project_by_id = get_project_by_id
repo.get_geoserver_config = get_geoserver_config
response = await module.get_project_metadata(
ctx=SimpleNamespace(
project_id=project_id,
project_role="editor",
),
metadata_repo=repo,
)
assert response.map_extent == {"xmin": 1, "ymin": 2, "xmax": 3, "ymax": 4}
assert response.project_id == project_id
@pytest.mark.anyio
async def test_meta_db_health_returns_503_for_postgres_errors(monkeypatch):
module = _load_meta_module(monkeypatch)
class BrokenSession:
async def execute(self, _query):
raise SQLAlchemyError("pg unavailable")
class DummyTimescaleConnection:
def cursor(self):
raise AssertionError("timescale should not be queried after postgres failure")
with pytest.raises(HTTPException) as exc_info:
await module.project_db_health(
pg_session=BrokenSession(),
ts_conn=DummyTimescaleConnection(),
)
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Project PostgreSQL health check failed: pg unavailable"
+108
View File
@@ -0,0 +1,108 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock
from uuid import uuid4
import pytest
from fastapi import HTTPException
from tests.conftest import install_stub, load_module_from_path
class DummyChangeSet:
def __init__(self, operations=None):
if operations is None:
self.operations = []
elif isinstance(operations, dict):
self.operations = [operations]
else:
self.operations = operations
def _load_project_module(monkeypatch):
install_stub(monkeypatch, "app.services", package=True)
install_stub(monkeypatch, "app.services.project_info", {})
install_stub(
monkeypatch,
"app.services.tjnetwork",
{
"ChangeSet": DummyChangeSet,
"list_project": lambda: ["demo"],
"have_project": lambda network: network == "demo",
"create_project": lambda network: None,
"delete_project": lambda network: None,
"is_project_open": lambda network: False,
"open_project": lambda network: None,
"close_project": lambda network: None,
"copy_project": lambda source, target: None,
"import_inp": lambda network, cs: {"ok": True},
"export_inp": lambda network, version: DummyChangeSet({"kind": "export"}),
"read_inp": lambda network, inp: True,
"dump_inp": lambda network, inp: True,
"get_all_vertices": lambda network: [],
"get_all_scada_elements": lambda network: [],
"get_all_district_metering_areas": lambda network: [],
"get_all_service_areas": lambda network: [],
"get_all_virtual_districts": lambda network: [],
"get_extension_data": lambda network, key: None,
"convert_inp_v3_to_v2": lambda inp: DummyChangeSet({"inp": inp}),
},
)
install_stub(
monkeypatch,
"app.auth.project_dependencies",
{"get_metadata_repository": lambda: None},
)
install_stub(
monkeypatch,
"app.infra.db.postgresql.database",
{"get_database_instance": lambda network: None},
)
install_stub(
monkeypatch,
"app.infra.db.timescaledb.database",
{"get_database_instance": lambda network: None},
)
return load_module_from_path(
"tests_project_endpoints_module",
"app/api/v1/endpoints/project.py",
)
@pytest.mark.anyio
async def test_project_info_returns_404_when_missing(monkeypatch):
module = _load_project_module(monkeypatch)
repo = SimpleNamespace(get_project_detail_by_code=AsyncMock(return_value=None))
with pytest.raises(HTTPException) as exc_info:
await module.get_project_info_endpoint(network="missing", metadata_repo=repo)
assert exc_info.value.status_code == 404
assert exc_info.value.detail == "Project missing not found"
@pytest.mark.anyio
async def test_project_info_returns_geoserver_payload(monkeypatch):
module = _load_project_module(monkeypatch)
detail = SimpleNamespace(
project_id=uuid4(),
name="Demo Project",
code="demo",
description="desc",
gs_workspace="ws",
map_extent={"xmin": 1, "ymin": 2, "xmax": 3, "ymax": 4},
status="active",
geoserver=SimpleNamespace(
gs_base_url="http://gs",
gs_admin_user="admin",
gs_datastore_name="store",
default_extent={"xmin": 1, "ymin": 2, "xmax": 3, "ymax": 4},
srid=4326,
),
)
repo = SimpleNamespace(get_project_detail_by_code=AsyncMock(return_value=detail))
payload = await module.get_project_info_endpoint(network="demo", metadata_repo=repo)
assert payload.code == "demo"
assert payload.map_extent == {"xmin": 1, "ymin": 2, "xmax": 3, "ymax": 4}
assert payload.geoserver.gs_base_url == "http://gs"