- 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>
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:
- Given
TEST_DATABASE_URLis set to a SQLite URL, Whenpytest api/tests/integration/is invoked, Then pytest exits before collecting any test with an error:Integration tests require postgresql+asyncpg://. - Given
DATABASE_URLis unset andTEST_DATABASE_URLis unset, When pytest is invoked, Then pytest exits with a clear message about the missing database URL. - Given
TEST_DATABASE_URLis a validpostgresql+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:
- Given Docker is running and dev services are stopped, When the test command is run, Then isolated
postgres-testandminio-testservices start, all tests run against them, and the command exits with pytest's return code. - 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.
- 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:
- 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. - Given the Makefile, When the developer runs
make test-unit, Then the pytest unit suite runs without requiring Docker. - Given the Makefile, When the developer runs
make test-integration, Then the Docker Compose test command runs.
Edge Cases
- What if
TEST_DATABASE_URLis 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-integrationfails 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.pyMUST assert the resolved database URL starts withpostgresql+asyncpg://and callpytest.exit()with a descriptive message before any test collects. - FR-002: A
docker-compose.test.ymlMUST define isolatedpostgres-test(port 5433) andminio-test(ports 9002/9003) services and anapi-testrunner service. - FR-003: The
api-testservice MUST setTEST_DATABASE_URLpointing topostgres-testand all S3 env vars pointing tominio-test. - FR-004: A
.env.test.exampleMUST document all environment variables required to run integration tests outside Docker. - FR-005: A
MakefileMUST providetest-unitandtest-integrationtargets.
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-testcompletes 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.pyenginefixture (session-scopedcreate_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.