feat: production-harden app and finalize merge-ready UX/theme baseline

- complete tokenized theme preset system and admin style guide visibility
- refine dashboard layout/scroll behavior and shared async/request UI states
- add user nav-visibility settings across API, migration, and frontend stores
- harden security headers/CSP handling and related test coverage
- enforce CI repository hygiene + frontend contract/theme/build quality gates
- update docs/env references and make migration smoke test head-aware
This commit is contained in:
Justin Visser 2026-02-19 15:43:20 +01:00
parent ab1d29b203
commit ae2ca8f149
66 changed files with 5655 additions and 853 deletions

View file

@ -593,6 +593,7 @@ async def test_api_settings_get_and_update(db_session: AsyncSession) -> None:
get_response = client.get("/api/v1/settings")
assert get_response.status_code == 200
assert "request_delay_seconds" in get_response.json()["data"]
assert "nav_visible_pages" in get_response.json()["data"]
update_response = client.put(
"/api/v1/settings",
@ -600,6 +601,7 @@ async def test_api_settings_get_and_update(db_session: AsyncSession) -> None:
"auto_run_enabled": True,
"run_interval_minutes": 45,
"request_delay_seconds": 6,
"nav_visible_pages": ["dashboard", "scholars", "publications", "settings", "runs"],
},
headers=headers,
)
@ -608,6 +610,13 @@ async def test_api_settings_get_and_update(db_session: AsyncSession) -> None:
assert updated["auto_run_enabled"] is True
assert updated["run_interval_minutes"] == 45
assert updated["request_delay_seconds"] == 6
assert updated["nav_visible_pages"] == [
"dashboard",
"scholars",
"publications",
"settings",
"runs",
]
@pytest.mark.integration

View file

@ -14,7 +14,7 @@ EXPECTED_TABLES = {
}
EXPECTED_ENUMS = {"run_status", "run_trigger_type"}
EXPECTED_REVISION = "20260217_0007"
EXPECTED_REVISION = "20260219_0008"
@pytest.mark.integration
@ -182,3 +182,20 @@ async def test_scholar_profiles_has_initial_page_snapshot_columns(db_session: As
"last_initial_page_fingerprint_sha256",
"last_initial_page_checked_at",
}
@pytest.mark.integration
@pytest.mark.db
@pytest.mark.migrations
@pytest.mark.asyncio
async def test_user_settings_has_nav_visible_pages_column(db_session: AsyncSession) -> None:
result = await db_session.execute(
text(
"""
SELECT 1
FROM information_schema.columns
WHERE table_name = 'user_settings' AND column_name = 'nav_visible_pages'
"""
)
)
assert result.scalar_one() == 1

View file

@ -1,15 +1,24 @@
from alembic.config import Config
from alembic.script import ScriptDirectory
import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
def expected_head_revision() -> str:
script = ScriptDirectory.from_config(Config("alembic.ini"))
head = script.get_current_head()
assert head is not None
return head
@pytest.mark.integration
@pytest.mark.smoke
@pytest.mark.db
@pytest.mark.asyncio
async def test_schema_head_revision_is_available(db_session: AsyncSession) -> None:
result = await db_session.execute(text("SELECT version_num FROM alembic_version"))
assert result.scalar_one() == "20260217_0007"
assert result.scalar_one() == expected_head_revision()
@pytest.mark.integration

View file

@ -0,0 +1,36 @@
from unittest.mock import AsyncMock
from fastapi.testclient import TestClient
from app.main import app
def test_security_headers_are_set_for_api_responses(monkeypatch) -> None:
monkeypatch.setattr("app.main.check_database", AsyncMock(return_value=True))
client = TestClient(app)
response = client.get("/healthz")
assert response.status_code == 200
assert response.headers["x-content-type-options"] == "nosniff"
assert response.headers["x-frame-options"] == "DENY"
assert response.headers["referrer-policy"] == "strict-origin-when-cross-origin"
assert response.headers["cross-origin-opener-policy"] == "same-origin"
assert response.headers["cross-origin-resource-policy"] == "same-origin"
assert "permissions-policy" in response.headers
assert "content-security-policy" in response.headers
assert "script-src 'self'" in response.headers["content-security-policy"]
assert "frame-ancestors 'none'" in response.headers["content-security-policy"]
assert "strict-transport-security" not in response.headers
def test_docs_route_uses_docs_csp_policy(monkeypatch) -> None:
monkeypatch.setattr("app.main.check_database", AsyncMock(return_value=True))
client = TestClient(app)
response = client.get("/docs")
assert response.status_code == 200
csp = response.headers["content-security-policy"]
assert "script-src 'self' 'unsafe-inline'" in csp
assert "style-src 'self' 'unsafe-inline'" in csp

View file

@ -0,0 +1,70 @@
from __future__ import annotations
import pytest
from app.services.user_settings import (
DEFAULT_NAV_VISIBLE_PAGES,
UserSettingsServiceError,
parse_nav_visible_pages,
parse_request_delay_seconds,
parse_run_interval_minutes,
)
def test_parse_run_interval_minutes_accepts_valid_value() -> None:
assert parse_run_interval_minutes("30") == 30
def test_parse_run_interval_minutes_rejects_below_minimum() -> None:
with pytest.raises(
UserSettingsServiceError,
match="Check interval must be at least 15 minutes.",
):
parse_run_interval_minutes("14")
def test_parse_request_delay_seconds_accepts_valid_value() -> None:
assert parse_request_delay_seconds("2") == 2
def test_parse_request_delay_seconds_rejects_below_minimum() -> None:
with pytest.raises(
UserSettingsServiceError,
match="Request delay must be at least 2 seconds.",
):
parse_request_delay_seconds("1")
def test_parse_nav_visible_pages_accepts_valid_pages() -> None:
parsed = parse_nav_visible_pages(
[
"dashboard",
"scholars",
"publications",
"settings",
"runs",
]
)
assert parsed == [
"dashboard",
"scholars",
"publications",
"settings",
"runs",
]
def test_parse_nav_visible_pages_rejects_missing_required_pages() -> None:
with pytest.raises(
UserSettingsServiceError,
match="Dashboard, Scholars, and Settings must remain visible.",
):
parse_nav_visible_pages(["dashboard", "publications"])
def test_parse_nav_visible_pages_rejects_unknown_page() -> None:
with pytest.raises(
UserSettingsServiceError,
match="Unsupported navigation page id: reports",
):
parse_nav_visible_pages(DEFAULT_NAV_VISIBLE_PAGES + ["reports"])