"""P0 hardening regression tests (HARDENING.md P0.1 / P0.2 / P0.3). Covers: - P0.3: self-registration is REMOVED — POST /api/auth/register returns 404 and no public path can mint a user at all (users are admin-created only). - 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 AsyncClient REPO_ROOT = Path(__file__).resolve().parent.parent pytestmark = pytest.mark.asyncio # ── P0.3: self-registration is removed entirely ─────────────────────── async def test_register_endpoint_is_removed(client: AsyncClient): """A raw unauthenticated register call must 404 — no public signup path.""" resp = await client.post( "/api/auth/register", json={ "email": "attacker@example.com", "password": "Sup3rSecret!", "full_name": "Attacker", "role": "Admin/Jerome", }, ) assert resp.status_code == 404, resp.text async def test_register_endpoint_removed_regardless_of_role(client: AsyncClient): """Attempts to mint privileged (or any) roles via register all 404.""" for role in ("Admin/Jerome", "Admin/Wahab", "admin", "superadmin", "CS Rep"): resp = await client.post( "/api/auth/register", json={ "email": f"attacker-{role.lower().replace('/', '-')}@example.com", "password": "Sup3rSecret!", "full_name": "Attacker", "role": role, }, ) assert resp.status_code == 404, (role, resp.text) async def test_register_does_not_create_user(client: AsyncClient): """No user row is ever created through the removed register endpoint.""" await client.post( "/api/auth/register", json={ "email": "ghost@example.com", "password": "Sup3rSecret!", "full_name": "Ghost", }, ) # The ghost account must not be able to log in. resp = await client.post( "/api/auth/login", json={"email": "ghost@example.com", "password": "Sup3rSecret!"}, ) assert resp.status_code == 401, resp.text 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