Files
reactbin/specs/008-postgres-integration-tests/spec.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

5.8 KiB

Feature Specification: PostgreSQL Integration Test Infrastructure

Feature Branch: 008-postgres-integration-tests Created: 2026-05-06 Status: Draft


Overview

Integration tests currently permit any SQLAlchemy-compatible database URL, including SQLite. This allowed a real production bug (incorrect HAVING without GROUP BY) to ship undetected because SQLite's permissive dialect did not reject it. The project constitution (§2.5, §5.2 v1.3.0) now explicitly mandates PostgreSQL for integration tests. This feature enforces that mandate with infrastructure and guardrails.


User Scenarios & Testing

User Story 1 — Integration tests are enforced to run against PostgreSQL (Priority: P1)

A developer running pytest against a non-PostgreSQL database URL receives an immediate, descriptive failure before any test runs.

Why this priority: Directly addresses the production bug that prompted this feature. Without this, the constitution mandate has no teeth.

Independent Test: Set TEST_DATABASE_URL=sqlite+aiosqlite:///test.db and run pytest api/tests/integration/. Confirm pytest exits immediately with a message identifying the dialect problem and naming the required scheme.

Acceptance Scenarios:

  1. Given TEST_DATABASE_URL is set to a SQLite URL, When pytest api/tests/integration/ is invoked, Then pytest exits before collecting any test with an error: Integration tests require postgresql+asyncpg://.
  2. Given DATABASE_URL is unset and TEST_DATABASE_URL is unset, When pytest is invoked, Then pytest exits with a clear message about the missing database URL.
  3. Given TEST_DATABASE_URL is a valid postgresql+asyncpg:// URL, When pytest is invoked, Then tests collect and run normally.

User Story 2 — One-command integration test run against isolated services (Priority: P1)

A developer can run the entire integration test suite against dedicated, isolated PostgreSQL and MinIO instances with a single command.

Why this priority: Without this, the PostgreSQL requirement is mandated but impractical — developers have no easy way to satisfy it.

Independent Test: From the repo root with Docker available, run docker compose -f docker-compose.test.yml run --rm api-test. Confirm all integration tests pass, test containers start and stop cleanly, and dev database/bucket are untouched.

Acceptance Scenarios:

  1. Given Docker is running and dev services are stopped, When the test command is run, Then isolated postgres-test and minio-test services start, all tests run against them, and the command exits with pytest's return code.
  2. Given dev services are running on their normal ports, When the test command is run, Then test services use different ports (5433, 9002/9003) and do not interfere with the dev stack.
  3. Given any test data is written during the run, When the test run completes, Then all test schema is dropped (conftest teardown is unchanged).

User Story 3 — Test infrastructure is documented (Priority: P2)

A developer new to the project can understand how to run unit tests vs integration tests without reading the source code.

Independent Test: Read .env.test.example and Makefile. Confirm all required environment variables are documented and make test-unit / make test-integration targets are present.

Acceptance Scenarios:

  1. Given a fresh clone, When the developer reads .env.test.example, Then they see every variable needed to run integration tests outside Docker, with example values.
  2. Given the Makefile, When the developer runs make test-unit, Then the pytest unit suite runs without requiring Docker.
  3. Given the Makefile, When the developer runs make test-integration, Then the Docker Compose test command runs.

Edge Cases

  • What if TEST_DATABASE_URL is set but malformed? — The guard should still catch a non-PostgreSQL scheme; asyncpg will raise its own error for a malformed URL.
  • What if Docker is not available? — make test-integration fails at the Docker level with Docker's own error; the Makefile does not need to guard for this.
  • What if the test PostgreSQL port (5433) is already in use? — Standard Docker port conflict error; no special handling needed.

Requirements

Functional Requirements

  • FR-001: conftest.py MUST assert the resolved database URL starts with postgresql+asyncpg:// and call pytest.exit() with a descriptive message before any test collects.
  • FR-002: A docker-compose.test.yml MUST define isolated postgres-test (port 5433) and minio-test (ports 9002/9003) services and an api-test runner service.
  • FR-003: The api-test service MUST set TEST_DATABASE_URL pointing to postgres-test and all S3 env vars pointing to minio-test.
  • FR-004: A .env.test.example MUST document all environment variables required to run integration tests outside Docker.
  • FR-005: A Makefile MUST provide test-unit and test-integration targets.

Success Criteria

  • SC-001: Running pytest api/tests/integration/ with a SQLite URL exits in under 2 seconds with a clear error message — no tests run.
  • SC-002: docker compose -f docker-compose.test.yml run --rm api-test completes successfully with all integration tests passing.
  • SC-003: Dev services (postgres on 5432, minio on 9000) are unaffected when the test command runs.

Assumptions

  • Docker Compose v2 (docker compose) is available in the developer environment.
  • The existing conftest.py engine fixture (session-scoped create_all / drop_all) continues to handle schema lifecycle; no per-test transaction rollback mechanism is introduced.
  • CI/CD pipeline configuration is out of scope for this feature.