feat(navigation): Enhance navigation links and add legacy route redirects
- Updated navigation links in `init_db.py` to include href overrides and parent slugs for profitability, opex, and capex planners. - Modified `NavigationService` to handle child links and href overrides, ensuring proper routing when context is missing. - Adjusted scenario detail and list templates to use new route names for opex and capex forms, with legacy fallbacks. - Introduced integration tests for legacy calculation routes to ensure proper redirection and error handling. - Added tests for navigation sidebar to validate role-based access and link visibility. - Enhanced navigation sidebar tests to include calculation links and contextual URLs based on project and scenario IDs.
This commit is contained in:
@@ -18,6 +18,7 @@ from models import User
|
||||
from routes.auth import router as auth_router
|
||||
from routes.dashboard import router as dashboard_router
|
||||
from routes.calculations import router as calculations_router
|
||||
from routes.navigation import router as navigation_router
|
||||
from routes.projects import router as projects_router
|
||||
from routes.scenarios import router as scenarios_router
|
||||
from routes.imports import router as imports_router
|
||||
@@ -29,6 +30,11 @@ from services.unit_of_work import UnitOfWork
|
||||
from services.session import AuthSession, SessionTokens
|
||||
from tests.utils.security import random_password, random_token
|
||||
|
||||
BASE_TESTSERVER_URL = "http://testserver"
|
||||
|
||||
|
||||
TEST_USER_HEADER = "X-Test-User"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def engine() -> Iterator[Engine]:
|
||||
@@ -60,6 +66,7 @@ def app(session_factory: sessionmaker) -> FastAPI:
|
||||
application.include_router(dashboard_router)
|
||||
application.include_router(calculations_router)
|
||||
application.include_router(projects_router)
|
||||
application.include_router(navigation_router)
|
||||
application.include_router(scenarios_router)
|
||||
application.include_router(imports_router)
|
||||
application.include_router(exports_router)
|
||||
@@ -85,26 +92,64 @@ def app(session_factory: sessionmaker) -> FastAPI:
|
||||
] = _override_ingestion_service
|
||||
|
||||
with UnitOfWork(session_factory=session_factory) as uow:
|
||||
assert uow.users is not None
|
||||
uow.ensure_default_roles()
|
||||
user = User(
|
||||
assert uow.users is not None and uow.roles is not None
|
||||
roles = {role.name: role for role in uow.ensure_default_roles()}
|
||||
admin_user = User(
|
||||
email="test-superuser@example.com",
|
||||
username="test-superuser",
|
||||
password_hash=User.hash_password(random_password()),
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
uow.users.create(user)
|
||||
user = uow.users.get(user.id, with_roles=True)
|
||||
viewer_user = User(
|
||||
email="test-viewer@example.com",
|
||||
username="test-viewer",
|
||||
password_hash=User.hash_password(random_password()),
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
)
|
||||
uow.users.create(admin_user)
|
||||
uow.users.create(viewer_user)
|
||||
uow.users.assign_role(
|
||||
user_id=admin_user.id,
|
||||
role_id=roles["admin"].id,
|
||||
granted_by=admin_user.id,
|
||||
)
|
||||
uow.users.assign_role(
|
||||
user_id=viewer_user.id,
|
||||
role_id=roles["viewer"].id,
|
||||
granted_by=admin_user.id,
|
||||
)
|
||||
admin_user = uow.users.get(admin_user.id, with_roles=True)
|
||||
viewer_user = uow.users.get(viewer_user.id, with_roles=True)
|
||||
|
||||
application.state.test_users = {
|
||||
"admin": admin_user,
|
||||
"viewer": viewer_user,
|
||||
}
|
||||
|
||||
def _resolve_user(alias: str) -> tuple[User, tuple[str, ...]]:
|
||||
normalised = alias.strip().lower()
|
||||
user = application.state.test_users.get(normalised)
|
||||
if user is None:
|
||||
raise ValueError(f"Unknown test user alias: {alias}")
|
||||
roles = tuple(role.name for role in user.roles)
|
||||
return user, roles
|
||||
|
||||
def _override_auth_session(request: Request) -> AuthSession:
|
||||
session = AuthSession(
|
||||
tokens=SessionTokens(
|
||||
access_token=random_token(),
|
||||
refresh_token=random_token(),
|
||||
alias = request.headers.get(TEST_USER_HEADER, "admin").strip().lower()
|
||||
if alias == "anonymous":
|
||||
session = AuthSession.anonymous()
|
||||
else:
|
||||
user, role_slugs = _resolve_user(alias or "admin")
|
||||
session = AuthSession(
|
||||
tokens=SessionTokens(
|
||||
access_token=random_token(),
|
||||
refresh_token=random_token(),
|
||||
),
|
||||
user=user,
|
||||
)
|
||||
)
|
||||
session.user = user
|
||||
session.set_role_slugs(role_slugs)
|
||||
request.state.auth_session = session
|
||||
return session
|
||||
|
||||
@@ -114,7 +159,7 @@ def app(session_factory: sessionmaker) -> FastAPI:
|
||||
|
||||
@pytest.fixture()
|
||||
def client(app: FastAPI) -> Iterator[TestClient]:
|
||||
test_client = TestClient(app)
|
||||
test_client = TestClient(app, headers={TEST_USER_HEADER: "admin"})
|
||||
try:
|
||||
yield test_client
|
||||
finally:
|
||||
@@ -124,13 +169,52 @@ def client(app: FastAPI) -> Iterator[TestClient]:
|
||||
@pytest_asyncio.fixture()
|
||||
async def async_client(app: FastAPI) -> AsyncClient:
|
||||
return AsyncClient(
|
||||
transport=ASGITransport(app=app), base_url="http://testserver"
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
headers={TEST_USER_HEADER: "admin"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_user_headers() -> Callable[[str | None], dict[str, str]]:
|
||||
def _factory(alias: str | None = "admin") -> dict[str, str]:
|
||||
if alias is None:
|
||||
return {}
|
||||
return {TEST_USER_HEADER: alias.lower()}
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def unit_of_work_factory(session_factory: sessionmaker) -> Callable[[], UnitOfWork]:
|
||||
def _factory() -> UnitOfWork:
|
||||
return UnitOfWork(session_factory=session_factory)
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def app_url_for(app: FastAPI) -> Callable[..., str]:
|
||||
def _builder(route_name: str, **path_params: object) -> str:
|
||||
normalised_params = {
|
||||
key: str(value)
|
||||
for key, value in path_params.items()
|
||||
if value is not None
|
||||
}
|
||||
return f"{BASE_TESTSERVER_URL}{app.url_path_for(route_name, **normalised_params)}"
|
||||
|
||||
return _builder
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def scenario_calculation_url(
|
||||
app_url_for: Callable[..., str]
|
||||
) -> Callable[[str, int, int], str]:
|
||||
def _builder(route_name: str, project_id: int, scenario_id: int) -> str:
|
||||
return app_url_for(
|
||||
route_name,
|
||||
project_id=project_id,
|
||||
scenario_id=scenario_id,
|
||||
)
|
||||
|
||||
return _builder
|
||||
|
||||
Reference in New Issue
Block a user