fix(metadata): sync project metadata endpoints
This commit is contained in:
@@ -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"
|
||||
@@ -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"
|
||||
+49
-2
@@ -1,9 +1,16 @@
|
||||
import pytest
|
||||
import importlib
|
||||
import importlib.util
|
||||
import sys
|
||||
import os
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
|
||||
# 自动添加项目根目录到路径(处理项目结构)
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
def run_this_test(test_file):
|
||||
@@ -12,3 +19,43 @@ def run_this_test(test_file):
|
||||
test_name = os.path.splitext(os.path.basename(test_file))[0]
|
||||
# 使用pytest运行(自动处理导入)
|
||||
pytest.main([test_file, "-v"])
|
||||
|
||||
|
||||
def build_test_app(router, prefix: str = "") -> FastAPI:
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix=prefix)
|
||||
return app
|
||||
|
||||
|
||||
def load_module_from_path(module_name: str, relative_path: str):
|
||||
module_path = PROJECT_ROOT / relative_path
|
||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def install_stub(monkeypatch, name: str, attrs: dict | None = None, package: bool = False):
|
||||
module = types.ModuleType(name)
|
||||
if package:
|
||||
module.__path__ = []
|
||||
if attrs:
|
||||
for key, value in attrs.items():
|
||||
setattr(module, key, value)
|
||||
|
||||
monkeypatch.setitem(sys.modules, name, module)
|
||||
|
||||
parent_name, _, child_name = name.rpartition(".")
|
||||
if parent_name:
|
||||
parent = sys.modules.get(parent_name)
|
||||
if parent is None:
|
||||
try:
|
||||
parent = importlib.import_module(parent_name)
|
||||
except Exception:
|
||||
parent = types.ModuleType(parent_name)
|
||||
parent.__path__ = []
|
||||
monkeypatch.setitem(sys.modules, parent_name, parent)
|
||||
setattr(parent, child_name, module)
|
||||
|
||||
return module
|
||||
|
||||
Reference in New Issue
Block a user