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:
parent
ab1d29b203
commit
ae2ca8f149
66 changed files with 5655 additions and 853 deletions
36
tests/unit/test_security_headers.py
Normal file
36
tests/unit/test_security_headers.py
Normal 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
|
||||
70
tests/unit/test_user_settings.py
Normal file
70
tests/unit/test_user_settings.py
Normal 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"])
|
||||
Loading…
Add table
Add a link
Reference in a new issue