# Implementation Plan: PostgreSQL Integration Test Infrastructure **Branch**: `master` | **Date**: 2026-05-06 | **Spec**: specs/008-postgres-integration-tests/spec.md **Input**: Feature specification from `specs/008-postgres-integration-tests/spec.md` --- ## Summary Enforce the constitution's PostgreSQL mandate (§2.5, §5.2 v1.3.0) for integration tests. Three concrete deliverables: (1) a fast-fail guard in `conftest.py` that rejects non-PostgreSQL URLs before any test collects, (2) a `docker-compose.test.yml` that provides isolated `postgres-test` and `minio-test` services and an `api-test` runner, and (3) a `Makefile` + `.env.test.example` that document the canonical test commands. --- ## Technical Context **Language/Version**: Python 3.12, Docker Compose v2 **Primary Dependencies**: pytest, pytest-asyncio, asyncpg, SQLAlchemy 2.x (all already in `pyproject.toml [dev]`) **Storage**: PostgreSQL 16-alpine (test instance), MinIO (test instance) **Testing**: pytest — this feature *is* the test infrastructure change **Target Platform**: Developer workstation (Linux/macOS) with Docker **Project Type**: Infrastructure / developer-experience **Performance Goals**: Guard exits in < 2 s; full integration suite continues to run in < 60 s **Constraints**: Must not break the existing dev compose stack; no changes to application source code **Scale/Scope**: One guard, one compose file, one Makefile, one env example --- ## Constitution Check | Principle | Status | Notes | |-----------|--------|-------| | §2.5 Database abstraction — no alternative DB in integration tests | ✅ ENFORCED | This feature implements the enforcement | | §5.1 TDD — failing test before implementation | ✅ | Guard itself is tested by running with a bad URL before adding the guard | | §5.2 Test pyramid — integration tests use real PostgreSQL | ✅ ENFORCED | docker-compose.test.yml provides the real instance | | §5.4 CI must pass before task is done | ✅ | Verified by running the full suite via compose | | §6 Tech stack — asyncpg driver, Docker Compose | ✅ | No new technologies introduced | | §7.1 One-command local start | ✅ | `docker compose -f docker-compose.test.yml run --rm api-test` | | §7.2 Environment config via env vars | ✅ | .env.test.example documents all vars | | §7.3 Linting not optional | ✅ | ruff will run as part of task validation | No violations. --- ## Project Structure ### Documentation (this feature) ```text specs/008-postgres-integration-tests/ ├── plan.md ← this file ├── research.md ← decisions made above ├── spec.md ← feature specification └── tasks.md ← generated by /speckit-tasks ``` ### Source changes ```text # New files docker-compose.test.yml ← isolated test services + api-test runner .env.test.example ← documents test environment variables Makefile ← test-unit / test-integration targets # Modified files api/tests/integration/conftest.py ← add postgresql+asyncpg:// dialect guard ``` No application source files (`api/app/`) are modified. No UI files are touched. --- ## Detailed Design ### 1. conftest.py — dialect guard Add a module-level `pytest_configure` hook at the top of `api/tests/integration/conftest.py`. It resolves the database URL (same logic as the `engine` fixture: prefer `TEST_DATABASE_URL`, fall back to `settings.database_url`) and calls `pytest.exit()` if the scheme is not `postgresql+asyncpg`: ```python def pytest_configure(config): import os 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, ) ``` The hook runs before any fixture or collection, giving an immediate, unambiguous error. **Note**: This guard goes in `api/tests/integration/conftest.py` only, not in `api/tests/conftest.py`, so that unit tests (which use no database) are unaffected. ### 2. docker-compose.test.yml ```yaml services: postgres-test: image: postgres:16-alpine environment: POSTGRES_USER: reactbin POSTGRES_PASSWORD: reactbin POSTGRES_DB: reactbin_test ports: - "5433:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U reactbin"] interval: 5s timeout: 5s retries: 5 minio-test: image: minio/minio:latest command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin ports: - "9002:9000" - "9003:9001" healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 5s timeout: 5s retries: 5 minio-init-test: image: minio/mc:latest depends_on: minio-test: condition: service_healthy environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin entrypoint: > /bin/sh -c " mc alias set local http://minio-test:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/reactbin-test " api-test: build: context: ./api environment: TEST_DATABASE_URL: postgresql+asyncpg://reactbin:reactbin@postgres-test:5432/reactbin_test DATABASE_URL: postgresql+asyncpg://reactbin:reactbin@postgres-test:5432/reactbin_test S3_ENDPOINT_URL: http://minio-test:9000 S3_BUCKET_NAME: reactbin-test S3_ACCESS_KEY_ID: minioadmin S3_SECRET_ACCESS_KEY: minioadmin S3_REGION: us-east-1 JWT_SECRET_KEY: test-secret-key-for-testing-only OWNER_USERNAME: testowner OWNER_PASSWORD: testpassword API_BASE_URL: http://localhost:8000 MAX_UPLOAD_BYTES: "52428800" depends_on: postgres-test: condition: service_healthy minio-init-test: condition: service_completed_successfully command: ["python", "-m", "pytest", "tests/", "-v"] working_dir: /app ``` ### 3. .env.test.example Documents the variables needed to run integration tests from the host (with postgres-test and minio-test already running via compose): ```bash # Integration test environment — used when running pytest directly on the host # Start test services first: docker compose -f docker-compose.test.yml up -d postgres-test minio-test minio-init-test TEST_DATABASE_URL=postgresql+asyncpg://reactbin:reactbin@localhost:5433/reactbin_test DATABASE_URL=postgresql+asyncpg://reactbin:reactbin@localhost:5433/reactbin_test S3_ENDPOINT_URL=http://localhost:9002 S3_BUCKET_NAME=reactbin-test S3_ACCESS_KEY_ID=minioadmin S3_SECRET_ACCESS_KEY=minioadmin S3_REGION=us-east-1 JWT_SECRET_KEY=test-secret-key-for-testing-only OWNER_USERNAME=testowner OWNER_PASSWORD=testpassword API_BASE_URL=http://localhost:8000 MAX_UPLOAD_BYTES=52428800 ``` ### 4. Makefile ```makefile .PHONY: test-unit test-integration test-unit: cd api && python -m pytest tests/unit/ -v test-integration: docker compose -f docker-compose.test.yml run --rm api-test ``` --- ## Phase Breakdown ### Phase 1: Guard (FR-001) — US1 - Write a failing test: run `pytest api/tests/integration/` with `TEST_DATABASE_URL=sqlite+aiosqlite:///test.db` — confirm it does NOT exit early (test that the guard is absent) - Add `pytest_configure` guard to `api/tests/integration/conftest.py` - Verify: running with SQLite URL now exits immediately with the correct message - Verify: running with a PostgreSQL URL proceeds normally ### Phase 2: Docker Compose test stack (FR-002, FR-003) — US2 - Write `docker-compose.test.yml` with `postgres-test`, `minio-test`, `minio-init-test`, `api-test` - Run `docker compose -f docker-compose.test.yml run --rm api-test` — all tests pass - Confirm dev stack (port 5432, 9000) is unaffected ### Phase 3: Documentation (FR-004, FR-005) — US3 - Write `.env.test.example` - Write `Makefile` with `test-unit` and `test-integration` - Verify `make test-unit` runs unit tests without Docker - Verify `make test-integration` invokes the compose command ### Phase 4: Polish - `ruff check api/app/ api/tests/` — zero violations - `ng lint` is unaffected (no UI changes) --- ## No data model or API contracts This feature touches only developer tooling. No new API endpoints, database schema changes, or UI components.