P0.1 — fail-closed secrets: - config.py: no default SECRET_KEY; refuses to boot when unset, a known placeholder, or <32 chars. Generate with: openssl rand -hex 32. - docker-compose.yml: literal secrets removed; runtime env now comes from a git-ignored .env via env_file. .env.example added as template. - .gitignore already covers .env (verified). P0.2 — locked CORS: - main.py: CORS_ORIGINS must be an explicit comma-separated allow-list. '*' or an empty value refuses to boot (was: silently ['*'] with allow_credentials=True). P0.3 — role-safe registration: - services/auth.py: client-supplied 'role' is IGNORED on POST /api/auth/register; self-registered users always get the least-privilege 'CS Rep' role. Unauthenticated callers can no longer mint Admin/Jerome, Admin/Wahab, or Director accounts. Tests: - conftest.py sets test SECRET_KEY/CORS_ORIGINS before app import. - New tests/test_p0_hardening.py (8 tests): role-escalation blocked for Admin/Jerome and Admin/Wahab, duplicate-email 409, and subprocess boot-validation for placeholder/short/missing secret + wildcard CORS. - Full suite: 44 passed. Redeploy note (per research): seed_units/seed_categories are insert-only, so the Aug-26 redeploy does NOT orphan historical tickets referencing units 103E/103W/105E/105W or the legacy 34-category tree. Pending Wahab: are 103E/103W/105E/105W real apartments dropped from the Excel regeneration? Optional follow-up: floor-number backfill for already- seeded units (mapping corrected floors; existing rows keep old values). Checks per HARDENING.md acceptance: - [x] starting without a real key fails loudly (subprocess-verified) - [x] compose carries no literal secret; secrets come from .env - [x] CORS_ORIGINS explicit allow-list, '*' rejected - [x] unauthenticated register cannot mint Admin/* or Director
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""Shared fixtures for the Denya OneCare test suite.
|
|
|
|
Sets DATABASE_URL to an isolated temp SQLite file BEFORE importing any app
|
|
module (the engine is created at import time), then provisions tables and
|
|
seed data per test.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
_TMP_DIR = tempfile.mkdtemp(prefix="denya-test-")
|
|
os.environ["DATABASE_URL"] = f"sqlite+aiosqlite:///{_TMP_DIR}/test.db"
|
|
# HARDENING.md P0.1/P0.2: the app now fails closed without a real SECRET_KEY
|
|
# and an explicit CORS allow-list — tests must satisfy both.
|
|
os.environ.setdefault("SECRET_KEY", "test-secret-key-not-for-production-0123456789abcdef")
|
|
os.environ.setdefault("CORS_ORIGINS", "http://test")
|
|
|
|
import pytest_asyncio # noqa: E402 (DATABASE_URL must be set before app imports)
|
|
from httpx import ASGITransport, AsyncClient # noqa: E402
|
|
|
|
from app.core.database import Base, async_session_factory, engine # noqa: E402
|
|
from app.main import app # noqa: E402
|
|
from app.models.ticket import Ticket # noqa: E402
|
|
from app.services.seed import seed_categories, seed_units, seed_users # noqa: E402
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def client():
|
|
"""Async test client with a fresh, seeded database per test."""
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
async with async_session_factory() as session:
|
|
await seed_users(session)
|
|
await session.commit()
|
|
# json_path=None → built-in fallback units (same data as the committed
|
|
# apartment_mapping.json, kept deterministic for tests)
|
|
await seed_units(session, json_path=None)
|
|
await session.commit()
|
|
await seed_categories(session)
|
|
await session.commit()
|
|
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
|
yield c
|
|
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.drop_all)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def seed_tickets():
|
|
"""Insert `n` tickets directly into the DB; returns the count inserted."""
|
|
|
|
async def _seed(n: int) -> int:
|
|
async with async_session_factory() as session:
|
|
for i in range(n):
|
|
session.add(
|
|
Ticket(
|
|
ticket_number=f"PAV-TEST-{i:05d}",
|
|
status="Logged",
|
|
priority="medium",
|
|
description=f"Test ticket {i}",
|
|
)
|
|
)
|
|
await session.commit()
|
|
return n
|
|
|
|
return _seed
|