refactor(storage): route project DSNs and remove legacy backends
This commit is contained in:
@@ -10,6 +10,8 @@ from app.auth.metadata_dependencies import (
|
||||
get_current_metadata_admin,
|
||||
get_metadata_repository,
|
||||
)
|
||||
from app.infra.db.metadb.repositories.metadata_repository import ProjectDbRouting
|
||||
from app.infra.db.project_routing import get_project_pgconn_string
|
||||
from tests.conftest import build_test_app
|
||||
|
||||
|
||||
@@ -98,3 +100,47 @@ def test_model_import_rejects_non_inp_file(monkeypatch):
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Only .inp model files are accepted"
|
||||
model_import.log_audit_event.assert_not_awaited()
|
||||
|
||||
|
||||
def test_model_update_uses_project_business_routing(monkeypatch):
|
||||
project_id = uuid4()
|
||||
project = SimpleNamespace(id=project_id, code="demo", status="active")
|
||||
repo = SimpleNamespace(
|
||||
session=object(),
|
||||
get_project_by_id=AsyncMock(return_value=project),
|
||||
get_project_db_routing=AsyncMock(
|
||||
return_value=ProjectDbRouting(
|
||||
project_id=project_id,
|
||||
db_role="biz_data",
|
||||
db_type="postgresql",
|
||||
dsn="postgresql://user:password@biz.example/routed_business",
|
||||
pool_min_size=1,
|
||||
pool_max_size=5,
|
||||
)
|
||||
),
|
||||
)
|
||||
captured: dict[str, str] = {}
|
||||
|
||||
async def fake_apply_model_update(content: bytes, project_code: str) -> None:
|
||||
assert content == VALID_INP
|
||||
captured["project_code"] = project_code
|
||||
captured["dsn"] = get_project_pgconn_string(project_code)
|
||||
|
||||
monkeypatch.setattr(model_import, "_apply_model_update", fake_apply_model_update)
|
||||
monkeypatch.setattr(model_import, "log_audit_event", AsyncMock())
|
||||
client = _client(
|
||||
admin=SimpleNamespace(id=uuid4(), role="admin", is_superuser=False),
|
||||
repo=repo,
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/admin/projects/{project_id}/model-imports",
|
||||
files={"file": ("desktop-model.inp", VALID_INP)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert captured == {
|
||||
"project_code": "demo",
|
||||
"dsn": "postgresql://user:password@biz.example/routed_business",
|
||||
}
|
||||
repo.get_project_db_routing.assert_awaited_once_with(project_id, "biz_data")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -12,15 +12,44 @@ from fastapi.testclient import TestClient
|
||||
|
||||
from app.api.v1.endpoints import schemes as schemes_endpoint
|
||||
from app.api.v1.endpoints import simulation as simulation_endpoint
|
||||
from app.api.v1.endpoints import cache as cache_endpoint
|
||||
from app.api.pagination import PaginatedList
|
||||
from app.api.v1.rest_router import api_router, build_rest_router
|
||||
from app.api.v1.router import api_router as source_api_router
|
||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||
from app.auth.project_dependencies import ProjectContext, get_project_context
|
||||
from app.auth.project_dependencies import (
|
||||
ProjectContext,
|
||||
get_project_business_routing,
|
||||
get_project_context,
|
||||
get_project_simulation_routing,
|
||||
)
|
||||
from app.infra.db.project_routing import (
|
||||
ActiveProjectRouting,
|
||||
get_project_pgconn_string,
|
||||
get_project_timescale_pgconn_string,
|
||||
)
|
||||
from scripts.check_openapi import current_contract_bytes, validate
|
||||
|
||||
|
||||
def _override_project_routing(
|
||||
app: FastAPI,
|
||||
project_context: ProjectContext,
|
||||
) -> None:
|
||||
app.dependency_overrides[get_project_context] = lambda: project_context
|
||||
business = ActiveProjectRouting(
|
||||
project_code=project_context.project_code,
|
||||
business_dsn=f"postgresql://user:password@biz/{project_context.project_code}",
|
||||
)
|
||||
simulation = ActiveProjectRouting(
|
||||
project_code=project_context.project_code,
|
||||
business_dsn=business.business_dsn,
|
||||
timescale_dsn=(
|
||||
f"postgresql://user:password@timescale/{project_context.project_code}"
|
||||
),
|
||||
)
|
||||
app.dependency_overrides[get_project_business_routing] = lambda: business
|
||||
app.dependency_overrides[get_project_simulation_routing] = lambda: simulation
|
||||
|
||||
|
||||
def test_rest_router_preserves_every_distinct_source_operation() -> None:
|
||||
skipped_names = {"fastapi_get_json", "fastapi_test_dict"}
|
||||
source_names = {
|
||||
@@ -44,6 +73,16 @@ def test_rest_router_has_unique_method_path_pairs() -> None:
|
||||
assert len(pairs) == len(set(pairs))
|
||||
|
||||
|
||||
def test_removed_redis_management_routes_are_not_published() -> None:
|
||||
published_paths = {
|
||||
route.path for route in api_router.routes if isinstance(route, APIRoute)
|
||||
}
|
||||
|
||||
assert published_paths.isdisjoint(
|
||||
{"/redis-keys/detail", "/redis-keys", "/all-redis", "/redis"}
|
||||
)
|
||||
|
||||
|
||||
def test_rest_router_rejects_duplicate_method_path_pairs() -> None:
|
||||
first = APIRoute(
|
||||
"/duplicate",
|
||||
@@ -176,6 +215,21 @@ def test_valve_isolation_route_uses_the_isolation_handler() -> None:
|
||||
assert route.name == "valve_isolation_endpoint"
|
||||
|
||||
|
||||
def test_open_project_route_requires_business_and_timescale_routing() -> None:
|
||||
route = next(
|
||||
route
|
||||
for route in api_router.routes
|
||||
if isinstance(route, APIRoute)
|
||||
and route.path == "/projects/current"
|
||||
and route.methods == {"POST"}
|
||||
)
|
||||
routing_parameter = inspect.signature(route.endpoint).parameters[
|
||||
"_rest_project_routing"
|
||||
]
|
||||
|
||||
assert routing_parameter.default.dependency is get_project_simulation_routing
|
||||
|
||||
|
||||
def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
@@ -184,6 +238,8 @@ def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
||||
network=network,
|
||||
accident_element=accident_element,
|
||||
disabled_valves=disabled_valves,
|
||||
business_dsn=get_project_pgconn_string(network),
|
||||
timescale_dsn=get_project_timescale_pgconn_string(network),
|
||||
)
|
||||
return {"isolatable": True, "must_close_valves": ["V-1"]}
|
||||
|
||||
@@ -194,12 +250,13 @@ def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
||||
)
|
||||
app = FastAPI(redirect_slashes=False)
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
app.dependency_overrides[get_project_context] = lambda: ProjectContext(
|
||||
project_context = ProjectContext(
|
||||
project_id=uuid4(),
|
||||
project_code="fengyang",
|
||||
user_id=uuid4(),
|
||||
project_role="member",
|
||||
)
|
||||
_override_project_routing(app, project_context)
|
||||
|
||||
response = TestClient(app, raise_server_exceptions=False).post(
|
||||
"/api/v1/valve-isolation-analyses",
|
||||
@@ -216,6 +273,8 @@ def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
||||
"network": "fengyang",
|
||||
"accident_element": ["P-1", "P-2"],
|
||||
"disabled_valves": ["V-9"],
|
||||
"business_dsn": "postgresql://user:password@biz/fengyang",
|
||||
"timescale_dsn": "postgresql://user:password@timescale/fengyang",
|
||||
}
|
||||
|
||||
|
||||
@@ -247,6 +306,7 @@ def test_rest_runtime_consumes_injected_project_context(monkeypatch) -> None:
|
||||
network=network,
|
||||
scheme_type=scheme_type,
|
||||
query_date=query_date,
|
||||
business_dsn=get_project_pgconn_string(network),
|
||||
)
|
||||
return [{"scheme_name": "burst_case", "scheme_type": scheme_type}]
|
||||
|
||||
@@ -263,7 +323,7 @@ def test_rest_runtime_consumes_injected_project_context(monkeypatch) -> None:
|
||||
user_id=uuid4(),
|
||||
project_role="viewer",
|
||||
)
|
||||
app.dependency_overrides[get_project_context] = lambda: project_context
|
||||
_override_project_routing(app, project_context)
|
||||
|
||||
response = TestClient(app, raise_server_exceptions=False).get(
|
||||
"/api/v1/schemes",
|
||||
@@ -275,6 +335,7 @@ def test_rest_runtime_consumes_injected_project_context(monkeypatch) -> None:
|
||||
"network": "fengyang",
|
||||
"scheme_type": "burst_analysis",
|
||||
"query_date": None,
|
||||
"business_dsn": "postgresql://user:password@biz/fengyang",
|
||||
}
|
||||
assert response.json()["items"] == [
|
||||
{"scheme_name": "burst_case", "scheme_type": "burst_analysis"}
|
||||
@@ -324,12 +385,13 @@ def test_sensor_placement_body_uses_authenticated_project_and_user(
|
||||
)
|
||||
app = FastAPI(redirect_slashes=False)
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
app.dependency_overrides[get_project_context] = lambda: ProjectContext(
|
||||
project_context = ProjectContext(
|
||||
project_id=uuid4(),
|
||||
project_code="project_a",
|
||||
user_id=uuid4(),
|
||||
project_role="member",
|
||||
)
|
||||
_override_project_routing(app, project_context)
|
||||
app.dependency_overrides[get_current_metadata_user] = lambda: type(
|
||||
"User", (), {"username": "alice"}
|
||||
)()
|
||||
@@ -353,26 +415,6 @@ def test_sensor_placement_body_uses_authenticated_project_and_user(
|
||||
}
|
||||
|
||||
|
||||
def test_cache_management_requires_environment_permission(monkeypatch) -> None:
|
||||
flushdb = Mock(return_value=True)
|
||||
monkeypatch.setattr(cache_endpoint.redis_client, "flushdb", flushdb)
|
||||
app = FastAPI(redirect_slashes=False)
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
app.dependency_overrides[get_project_context] = lambda: ProjectContext(
|
||||
project_id=uuid4(),
|
||||
project_code="project_a",
|
||||
user_id=uuid4(),
|
||||
project_role="member",
|
||||
)
|
||||
|
||||
response = TestClient(app, raise_server_exceptions=False).delete(
|
||||
"/api/v1/all-redis"
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
flushdb.assert_not_called()
|
||||
|
||||
|
||||
def test_rest_runtime_json_encodes_untyped_datetime_response() -> None:
|
||||
source_router = APIRouter()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user