- 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.
147 lines
4.2 KiB
Python
147 lines
4.2 KiB
Python
"""Integration coverage for the /navigation/sidebar endpoint.
|
|
|
|
These tests validate role-based filtering, ordering, and disabled-link handling
|
|
through the full FastAPI stack so future changes keep navigation behaviour under
|
|
explicit test coverage.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from typing import Any, Mapping
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from models.navigation import NavigationGroup, NavigationLink
|
|
from services.unit_of_work import UnitOfWork
|
|
|
|
|
|
@pytest.fixture()
|
|
def seed_navigation(unit_of_work_factory: Callable[[], UnitOfWork]) -> Callable[[], None]:
|
|
def _seed() -> None:
|
|
with unit_of_work_factory() as uow:
|
|
repo = uow.navigation
|
|
assert repo is not None
|
|
|
|
workspace = repo.add_group(
|
|
NavigationGroup(
|
|
slug="workspace",
|
|
label="Workspace",
|
|
sort_order=10,
|
|
)
|
|
)
|
|
insights = repo.add_group(
|
|
NavigationGroup(
|
|
slug="insights",
|
|
label="Insights",
|
|
sort_order=20,
|
|
)
|
|
)
|
|
|
|
repo.add_link(
|
|
NavigationLink(
|
|
group_id=workspace.id,
|
|
slug="projects",
|
|
label="Projects",
|
|
href_override="/projects",
|
|
sort_order=5,
|
|
required_roles=[],
|
|
)
|
|
)
|
|
repo.add_link(
|
|
NavigationLink(
|
|
group_id=workspace.id,
|
|
slug="admin-tools",
|
|
label="Admin Tools",
|
|
href_override="/admin/tools",
|
|
sort_order=10,
|
|
required_roles=["admin"],
|
|
)
|
|
)
|
|
repo.add_link(
|
|
NavigationLink(
|
|
group_id=workspace.id,
|
|
slug="disabled-link",
|
|
label="Hidden",
|
|
href_override="/hidden",
|
|
sort_order=15,
|
|
required_roles=[],
|
|
is_enabled=False,
|
|
)
|
|
)
|
|
repo.add_link(
|
|
NavigationLink(
|
|
group_id=insights.id,
|
|
slug="reports",
|
|
label="Reports",
|
|
href_override="/reports",
|
|
sort_order=1,
|
|
required_roles=[],
|
|
)
|
|
)
|
|
|
|
return _seed
|
|
|
|
|
|
def _link_labels(group_json: Mapping[str, Any]) -> list[str]:
|
|
return [link["label"] for link in group_json["links"]]
|
|
|
|
|
|
def test_admin_session_receives_all_enabled_links(
|
|
client: TestClient,
|
|
seed_navigation: Callable[[], None],
|
|
) -> None:
|
|
seed_navigation()
|
|
|
|
response = client.get("/navigation/sidebar")
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
|
|
assert [group["label"] for group in payload["groups"]] == [
|
|
"Workspace",
|
|
"Insights",
|
|
]
|
|
workspace, insights = payload["groups"]
|
|
assert _link_labels(workspace) == ["Projects", "Admin Tools"]
|
|
assert _link_labels(insights) == ["Reports"]
|
|
assert payload["roles"] == ["admin"]
|
|
|
|
|
|
def test_viewer_session_filters_admin_only_links(
|
|
client: TestClient,
|
|
seed_navigation: Callable[[], None],
|
|
test_user_headers: Callable[[str | None], dict[str, str]],
|
|
) -> None:
|
|
seed_navigation()
|
|
|
|
response = client.get(
|
|
"/navigation/sidebar",
|
|
headers=test_user_headers("viewer"),
|
|
)
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
|
|
assert [group["label"] for group in payload["groups"]] == [
|
|
"Workspace",
|
|
"Insights",
|
|
]
|
|
workspace, insights = payload["groups"]
|
|
assert _link_labels(workspace) == ["Projects"]
|
|
assert _link_labels(insights) == ["Reports"]
|
|
assert payload["roles"] == ["viewer"]
|
|
|
|
|
|
def test_anonymous_access_is_rejected(
|
|
client: TestClient,
|
|
seed_navigation: Callable[[], None],
|
|
test_user_headers: Callable[[str | None], dict[str, str]],
|
|
) -> None:
|
|
seed_navigation()
|
|
|
|
response = client.get(
|
|
"/navigation/sidebar",
|
|
headers=test_user_headers("anonymous"),
|
|
)
|
|
assert response.status_code == 401
|