All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 31s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 27s
- Added compliance implementation report detailing the status of various packages (P-03, P-04, P-05, P-07, P-23, P-24) and their technical changes, tests, and notes. - Introduced a new workspace configuration file for the project to streamline development setup.
151 lines
4.6 KiB
Python
151 lines
4.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"
|
||
|
||
|
||
def test_public_media_static_not_mounted_by_default() -> None:
|
||
"""/media/-StaticFiles-Mount darf ohne ALLOW_PUBLIC_MEDIA_STATIC nicht aktiv sein."""
|
||
snippet = """
|
||
from main import app
|
||
mounted = [getattr(r, 'path', '') for r in app.routes]
|
||
assert not any(p == '/media' for p in mounted), (
|
||
"ALLOW_PUBLIC_MEDIA_STATIC aktiv – /media oeffentlich erreichbar. Vor Deploy entfernen."
|
||
)
|
||
"""
|
||
proc = _run_fresh_import_int(snippet, {"ENVIRONMENT": "production"})
|
||
assert proc.returncode == 0, proc.stderr + proc.stdout
|
||
|
||
|
||
def test_allow_public_media_static_activates_media_mount() -> None:
|
||
"""Dokumentiert: ALLOW_PUBLIC_MEDIA_STATIC=1 aktiviert /media ohne Authentifizierung."""
|
||
snippet = """
|
||
from main import app
|
||
mounted = [getattr(r, 'path', '') for r in app.routes]
|
||
assert any(p == '/media' for p in mounted), "/media-Mount wurde nicht aktiviert"
|
||
"""
|
||
proc = _run_fresh_import_int(snippet, {"ALLOW_PUBLIC_MEDIA_STATIC": "1"})
|
||
assert proc.returncode == 0, proc.stderr + proc.stdout
|