From 6d496a599ebc1b8a540ef107f0da9d2038807b6a Mon Sep 17 00:00:00 2001 From: zwitschi Date: Wed, 12 Nov 2025 20:30:40 +0100 Subject: [PATCH] feat: Resolve test suite regressions and enhance token tamper detection feat: Add UI router to application for improved routing style: Update breadcrumb styles in main.css and remove redundant styles from scenarios.css --- changelog.md | 1 + scripts/init_db.py | 3 +++ services/security.py | 9 +++++++++ static/css/main.css | 35 ++++++++++++++++++++++++++++++++++- static/css/scenarios.css | 14 -------------- tests/conftest.py | 2 ++ 6 files changed, 49 insertions(+), 15 deletions(-) diff --git a/changelog.md b/changelog.md index 54bce5c..f3bdf58 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ## 2025-11-12 +- Resolved test suite regressions by registering the UI router in test fixtures, restoring `TABLE_DDLS` for enum validation checks, hardening token tamper detection, and reran the full pytest suite to confirm green builds. - Fixed critical 500 error in reporting dashboard by correcting route reference in reporting.html template - changed 'reports.project_list_page' to 'projects.project_list_page' to resolve NoMatchFound error when accessing /ui/reporting. - Completed navigation validation by inventorying all sidebar navigation links, identifying missing routes for simulations, reporting, settings, themes, and currencies, created new UI routes in routes/ui.py with proper authentication guards, built corresponding templates (simulations.html, reporting.html, settings.html, theme_settings.html, currencies.html), registered the UI router in main.py, updated sidebar navigation to use route names instead of hardcoded URLs, and enhanced navigation.js to use dynamic URL resolution for proper route handling. - Fixed critical template rendering error in sidebar_nav.html where URL objects from request.url_for() were being used with string methods, causing TypeError. Added |string filters to convert URL objects to strings for proper template rendering. diff --git a/scripts/init_db.py b/scripts/init_db.py index 46412c5..5da5929 100644 --- a/scripts/init_db.py +++ b/scripts/init_db.py @@ -384,6 +384,9 @@ def _get_table_ddls(is_sqlite: bool) -> List[str]: # Seeds +TABLE_DDLS: List[str] = _get_table_ddls(is_sqlite=False) + + DEFAULT_ROLES = [ {"id": 1, "name": "admin", "display_name": "Administrator", "description": "Full platform access with user management rights."}, diff --git a/services/security.py b/services/security.py index 02a078d..34c8209 100644 --- a/services/security.py +++ b/services/security.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone +from hmac import compare_digest from typing import Any, Dict, Iterable, Literal, Type from jose import ExpiredSignatureError, JWTError, jwt @@ -176,6 +177,14 @@ def _decode_token( except JWTError as exc: # pragma: no cover - jose error bubble raise TokenDecodeError("Unable to decode token") from exc + expected_token = jwt.encode( + decoded, + settings.secret_key, + algorithm=settings.algorithm, + ) + if not compare_digest(token, expected_token): + raise TokenDecodeError("Token contents have been altered.") + try: payload = _model_validate(TokenPayload, decoded) except ValidationError as exc: diff --git a/static/css/main.css b/static/css/main.css index 4277eff..0aa8d25 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -302,6 +302,26 @@ a { border-color: var(--brand); } +.breadcrumb { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + color: var(--muted); + margin-bottom: 1.2rem; +} + +.breadcrumb a { + color: var(--brand-2); + text-decoration: none; +} + +.breadcrumb a::after { + content: ">"; + margin-left: 0.5rem; + color: var(--muted); +} + .app-layout { display: flex; min-height: 100vh; @@ -1031,7 +1051,7 @@ tbody tr:nth-child(even) { .site-footer { background-color: var(--brand); - color: var(--color-text-invert); + color: var(--color-text-strong); margin-top: 3rem; } @@ -1056,6 +1076,19 @@ tbody tr:nth-child(even) { object-fit: cover; } +footer p { + margin: 0; +} +footer a { + font-weight: 600; + color: var(--color-text-dark); + text-decoration: underline; +} +footer a:hover, +footer a:focus { + color: var(--color-text-strong); +} + .sidebar-toggle { display: none; align-items: center; diff --git a/static/css/scenarios.css b/static/css/scenarios.css index fa9d981..f108953 100644 --- a/static/css/scenarios.css +++ b/static/css/scenarios.css @@ -23,20 +23,6 @@ background: rgba(43, 165, 143, 0.12); } -.breadcrumb { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.9rem; - color: var(--muted); - margin-bottom: 1.2rem; -} - -.breadcrumb a { - color: var(--brand-2); - text-decoration: none; -} - .header-actions { display: flex; gap: 0.75rem; diff --git a/tests/conftest.py b/tests/conftest.py index aefd054..e394af6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,7 @@ from routes.scenarios import router as scenarios_router from routes.imports import router as imports_router from routes.exports import router as exports_router from routes.reports import router as reports_router +from routes.ui import router as ui_router from services.importers import ImportIngestionService from services.unit_of_work import UnitOfWork from services.session import AuthSession, SessionTokens @@ -61,6 +62,7 @@ def app(session_factory: sessionmaker) -> FastAPI: application.include_router(imports_router) application.include_router(exports_router) application.include_router(reports_router) + application.include_router(ui_router) def _override_uow() -> Iterator[UnitOfWork]: with UnitOfWork(session_factory=session_factory) as uow: