Feat: Enforce PostgreSQL for integration tests; add Docker test stack

- conftest.py: pytest_configure guard rejects non-postgresql+asyncpg:// URLs
  before any test collects (per constitution §2.5/§5.2 v1.3.0)
- docker-compose.test.yml: isolated postgres-test (5433) + minio-test (9002)
  + api-test runner; one command runs the full suite against real PostgreSQL
- Makefile: test-unit and test-integration targets
- .env.test.example: documents variables needed to run tests outside Docker
- Fix pre-existing test bug: integration tests using client fixture (NoOpAuthProvider)
  for write operations (upload/delete/patch) now use authed_client with Bearer
  token — these were never caught because tests never ran against a live stack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 19:14:12 +00:00
parent 354c85292d
commit f3e0021ee8
17 changed files with 761 additions and 39 deletions

View File

@@ -1,5 +1,6 @@
import os
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
@@ -9,11 +10,11 @@ os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-testing-only")
os.environ.setdefault("OWNER_USERNAME", "testowner")
os.environ.setdefault("OWNER_PASSWORD", "testpassword")
from app.auth.jwt_provider import JWTAuthProvider
from app.config import get_settings
from app.database import Base
from app.dependencies import get_auth, get_db, get_storage
from app.main import app
from app.auth.jwt_provider import JWTAuthProvider # noqa: E402
from app.config import get_settings # noqa: E402
from app.database import Base # noqa: E402
from app.dependencies import get_auth, get_db, get_storage # noqa: E402
from app.main import app # noqa: E402
# Bust the LRU cache so get_settings() picks up the env vars set above
get_settings.cache_clear()
@@ -26,8 +27,6 @@ _TEST_OWNER_PASSWORD = os.environ["OWNER_PASSWORD"]
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def engine():
settings = get_settings()
# Use a separate test database URL if TEST_DATABASE_URL is set
import os
db_url = os.getenv("TEST_DATABASE_URL", settings.database_url)
eng = create_async_engine(db_url, echo=False)
async with eng.begin() as conn:
@@ -108,3 +107,15 @@ async def authed_client(db_session, jwt_auth_provider):
yield c, valid_token
app.dependency_overrides.clear()
def pytest_configure(config):
db_url = os.getenv("TEST_DATABASE_URL") or os.getenv("DATABASE_URL", "")
if not db_url.startswith("postgresql+asyncpg://"):
pytest.exit(
"Integration tests require a PostgreSQL database "
"(postgresql+asyncpg://...). "
"Set TEST_DATABASE_URL or DATABASE_URL accordingly. "
f"Got: {db_url!r}",
returncode=1,
)