diff --git a/app/api/v1/endpoints/meta.py b/app/api/v1/endpoints/meta.py index 455189e..c6a6f45 100644 --- a/app/api/v1/endpoints/meta.py +++ b/app/api/v1/endpoints/meta.py @@ -1,5 +1,6 @@ import logging from fastapi import APIRouter, Depends, HTTPException, status, Query, Path +import psycopg from psycopg import AsyncConnection from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError @@ -58,6 +59,7 @@ async def get_project_metadata( code=project.code, description=project.description, gs_workspace=project.gs_workspace, + map_extent=project.map_extent, status=project.status, project_role=ctx.project_role, geoserver=geoserver_payload, @@ -110,7 +112,23 @@ async def project_db_health( 检查PostgreSQL和TimescaleDB数据库的连接状态 """ - await pg_session.execute(text("SELECT 1")) - async with ts_conn.cursor() as cur: - await cur.execute("SELECT 1") + try: + await pg_session.execute(text("SELECT 1")) + except SQLAlchemyError as exc: + logger.error("Project PostgreSQL health check failed", exc_info=True) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Project PostgreSQL health check failed: {exc}", + ) from exc + + try: + async with ts_conn.cursor() as cur: + await cur.execute("SELECT 1") + except psycopg.Error as exc: + logger.error("Project TimescaleDB health check failed", exc_info=True) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Project TimescaleDB health check failed: {exc}", + ) from exc + return {"postgres": "ok", "timescale": "ok"} diff --git a/app/domain/schemas/metadata.py b/app/domain/schemas/metadata.py index 91dc4c3..db3220a 100644 --- a/app/domain/schemas/metadata.py +++ b/app/domain/schemas/metadata.py @@ -5,10 +5,10 @@ from pydantic import BaseModel class GeoServerConfigResponse(BaseModel): - gs_base_url: Optional[str] - gs_admin_user: Optional[str] + gs_base_url: Optional[str] = None + gs_admin_user: Optional[str] = None gs_datastore_name: str - default_extent: Optional[dict] + default_extent: Optional[dict] = None srid: int @@ -16,19 +16,19 @@ class ProjectMetaResponse(BaseModel): project_id: UUID name: str code: str - description: Optional[str] + description: Optional[str] = None gs_workspace: str - map_extent: Optional[dict] + map_extent: Optional[dict] = None status: str project_role: str - geoserver: Optional[GeoServerConfigResponse] + geoserver: Optional[GeoServerConfigResponse] = None class ProjectSummaryResponse(BaseModel): project_id: UUID name: str code: str - description: Optional[str] + description: Optional[str] = None gs_workspace: str status: str project_role: str diff --git a/cli/tjwater_cli/core.py b/cli/tjwater_cli/core.py index 3fdd5da..34eaeb4 100644 --- a/cli/tjwater_cli/core.py +++ b/cli/tjwater_cli/core.py @@ -119,7 +119,7 @@ def load_auth_context(auth_stdin: bool = False) -> AuthContext: project_id=_pick(raw, "project_id", "projectId", "x_project_id"), user_id=_pick(raw, "user_id", "userId", "x_user_id"), username=_pick(raw, "username", "preferred_username"), - network="tjwater", + network=_pick(raw, "network", "project_code", "projectCode", "project"), headers={str(key): str(value) for key, value in headers.items()}, ) diff --git a/tests/api/test_meta_endpoints.py b/tests/api/test_meta_endpoints.py new file mode 100644 index 0000000..2313b03 --- /dev/null +++ b/tests/api/test_meta_endpoints.py @@ -0,0 +1,92 @@ +from types import SimpleNamespace +from uuid import uuid4 + +from fastapi.testclient import TestClient +from sqlalchemy.exc import SQLAlchemyError + +from tests.conftest import build_test_app, 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", + ) + + +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 + + app = build_test_app(module.router, "/api/v1") + app.dependency_overrides[module.get_project_context] = lambda: SimpleNamespace( + project_id=project_id, + project_role="editor", + ) + app.dependency_overrides[module.get_metadata_repository] = lambda: repo + client = TestClient(app) + + response = client.get("/api/v1/meta/project") + + assert response.status_code == 200 + assert response.json()["map_extent"] == {"xmin": 1, "ymin": 2, "xmax": 3, "ymax": 4} + + +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") + + app = build_test_app(module.router, "/api/v1") + app.dependency_overrides[module.get_project_pg_session] = lambda: BrokenSession() + app.dependency_overrides[module.get_project_timescale_connection] = lambda: DummyTimescaleConnection() + client = TestClient(app) + + response = client.get("/api/v1/meta/db/health") + + assert response.status_code == 503 + assert response.json()["detail"] == "Project PostgreSQL health check failed: pg unavailable"