Files
reactbin/specs/008-postgres-integration-tests/plan.md
agatha f3e0021ee8 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>
2026-05-06 19:14:12 +00:00

8.3 KiB

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)

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

# 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:

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

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):

# 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

.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.