"""P0 hardening regression tests (HARDENING.md P0.1 / P0.2 / P0.3). Covers: - P0.3: self-registration CANNOT mint a privileged role (role field ignored) - P0.3: duplicate email still 409s - P0.1: app fails to import/boot with placeholder or missing SECRET_KEY - P0.2: app fails to boot with CORS_ORIGINS="*" """ from __future__ import annotations import os import subprocess import sys from pathlib import Path import pytest from httpx import ASGITransport, AsyncClient REPO_ROOT = Path(__file__).resolve().parent.parent pytestmark = pytest.mark.asyncio # ── P0.3: registration role-escalation ──────────────────────────────── async def test_register_cannot_mint_admin_role(client: AsyncClient): """A raw unauthenticated register call must NOT be able to mint Admin/*.""" resp = await client.post( "/api/auth/register", json={ "email": "attacker@example.com", "password": "Sup3rSecret!", "full_name": "Attacker", "role": "Admin/Jerome", }, ) assert resp.status_code == 201, resp.text created = resp.json() assert created["role"] == "CS Rep", ( f"self-registration minted privileged role: {created['role']}" ) async def test_register_role_wahab_also_blocked(client: AsyncClient): resp = await client.post( "/api/auth/register", json={ "email": "attacker2@example.com", "password": "Sup3rSecret!", "full_name": "Attacker Two", "role": "Admin/Wahab", }, ) assert resp.status_code == 201 assert resp.json()["role"] == "CS Rep" async def test_register_duplicate_email_conflict(client: AsyncClient): payload = { "email": "dupe@example.com", "password": "Sup3rSecret!", "full_name": "Dupe", } r1 = await client.post("/api/auth/register", json=payload) assert r1.status_code == 201 r2 = await client.post("/api/auth/register", json=payload) assert r2.status_code == 409 # ── P0.1 / P0.2: fail-closed boot validation ────────────────────────── def _boot_with_env(env_overrides: dict[str, str]) -> subprocess.CompletedProcess: """Try importing app.main in a subprocess with the given env; the import must fail (non-zero) when fail-closed validation trips.""" env = os.environ.copy() env["DATABASE_URL"] = "sqlite+aiosqlite:///:memory:" env.pop("SECRET_KEY", None) env.pop("CORS_ORIGINS", None) env.update(env_overrides) script = ( "import sys; sys.path.insert(0, ''); " "import app.main" # noqa ) return subprocess.run( [sys.executable, "-c", script], cwd=str(REPO_ROOT), env=env, capture_output=True, text=True, timeout=60, ) def test_boot_fails_with_placeholder_secret(): result = _boot_with_env({"SECRET_KEY": "change-me-in-production"}) assert result.returncode != 0, "app booted with placeholder SECRET_KEY!" assert "SECRET_KEY" in result.stderr def test_boot_fails_with_short_secret(): result = _boot_with_env({"SECRET_KEY": "tooshort"}) assert result.returncode != 0, "app booted with a <32-char SECRET_KEY!" assert "SECRET_KEY" in result.stderr def test_boot_fails_without_secret(): result = _boot_with_env({"SECRET_KEY": ""}) assert result.returncode != 0, "app booted without a SECRET_KEY!" assert "SECRET_KEY" in result.stderr def test_boot_fails_with_wildcard_cors(): result = _boot_with_env( { "SECRET_KEY": "test-secret-key-not-for-production-0123456789abcdef", "CORS_ORIGINS": "*", } ) assert result.returncode != 0, "app booted with CORS_ORIGINS=* !" assert "CORS_ORIGINS" in result.stderr def test_boot_succeeds_with_valid_env(): result = _boot_with_env( { "SECRET_KEY": "test-secret-key-not-for-production-0123456789abcdef", "CORS_ORIGINS": "http://test", } ) assert result.returncode == 0, result.stderr