shinkan-jinkendo/backend/tests/test_security_release.py
Lars 161d520329
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 23s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 23s
feat: implement CSP and security headers for API responses
- Added Content-Security-Policy header to nginx configuration for SPA, enhancing security against XSS attacks.
- Introduced middleware in FastAPI to set X-Content-Type-Options header, preventing MIME-sniffing vulnerabilities.
- Updated production readiness audit and access layer endpoint audit to reflect security enhancements and ongoing governance practices.
- Added tests to verify the presence of security headers in API responses, ensuring compliance with security standards.
2026-05-07 11:09:06 +02:00

127 lines
3.6 KiB
Python

"""
Sicherheits-Regression für Release-Konfiguration (OpenAPI, Health-Ready, Legacy-Routen).
Läuft ohne PostgreSQL, wo möglich (TestClient + Subprocess mit frischem Import).
CI: scripts/security_release_checks.py
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
os.environ.setdefault("SKIP_DB_MIGRATE", "1")
from auth import require_auth
from main import app
BACKEND_ROOT = Path(__file__).resolve().parents[1]
@pytest.fixture
def client() -> TestClient:
return TestClient(app)
@pytest.fixture(autouse=True)
def _clear_auth_override():
yield
app.dependency_overrides.pop(require_auth, None)
def test_legacy_get_profile_uses_session_only(client: TestClient) -> None:
"""SEC-01: /api/profile darf nur das Session-Profil liefern."""
def auth_user() -> dict:
return {"profile_id": 901, "role": "trainer"}
app.dependency_overrides[require_auth] = auth_user
with patch("routers.profiles.profile_document") as pd_mock:
pd_mock.return_value = {"id": 901, "name": "T"}
r = client.get("/api/profile", headers={"X-Auth-Token": "dummy"})
assert r.status_code == 200
pd_mock.assert_called_once_with("901")
def _run_fresh_import_int(code: str, env: dict) -> subprocess.CompletedProcess:
merged = {**os.environ, **env, "SKIP_DB_MIGRATE": "1"}
return subprocess.run(
[sys.executable, "-c", code],
cwd=str(BACKEND_ROOT),
env=merged,
capture_output=True,
text=True,
check=False,
)
@pytest.mark.parametrize(
"extra_env, expect_openapi",
[
({"ENVIRONMENT": "development"}, True),
({"ENVIRONMENT": "production"}, False),
({"ENVIRONMENT": "production", "PUBLIC_OPENAPI": "true"}, True),
],
)
def test_openapi_urls_match_environment(extra_env: dict, expect_openapi: bool) -> None:
snippet = f"""
from main import app
assert (app.openapi_url is not None) == {expect_openapi}
assert (app.docs_url is not None) == {expect_openapi}
"""
proc = _run_fresh_import_int(snippet, extra_env)
assert proc.returncode == 0, proc.stderr + proc.stdout
def test_health_ready_minimal_body_in_production() -> None:
snippet = """
from fastapi.testclient import TestClient
from main import app
c = TestClient(app)
r = c.get("/api/health/ready")
r.raise_for_status()
j = r.json()
assert "tables" not in j
assert "detail" not in j
assert "schema_migrations_count" not in j
assert "status" in j and "database" in j and "schema_complete" in j
"""
proc = _run_fresh_import_int(
snippet,
{"ENVIRONMENT": "production"},
)
assert proc.returncode == 0, proc.stderr + proc.stdout
def test_health_ready_full_detail_when_flag_in_production() -> None:
snippet = """
from fastapi.testclient import TestClient
from main import app
c = TestClient(app)
r = c.get("/api/health/ready")
r.raise_for_status()
j = r.json()
assert "tables" in j
assert "schema_migrations_count" in j
"""
proc = _run_fresh_import_int(
snippet,
{"ENVIRONMENT": "production", "HEALTH_READY_PUBLIC_DETAIL": "1"},
)
assert proc.returncode == 0, proc.stderr + proc.stdout
def test_api_attachments_x_content_type_options_nosniff(client: TestClient) -> None:
"""Globales Middleware: keine MIME-Sniffing-Heuristik für API/Health."""
r = client.get("/health")
assert r.status_code == 200
assert r.headers.get("x-content-type-options") == "nosniff"
r2 = client.get("/api/version")
assert r2.status_code == 200
assert r2.headers.get("x-content-type-options") == "nosniff"