- 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>
7.2 KiB
Tasks: PostgreSQL Integration Test Infrastructure
Input: Design documents from specs/008-postgres-integration-tests/
Prerequisites: plan.md ✅, spec.md ✅, research.md ✅, quickstart.md ✅
Tests: TDD is non-negotiable (§5.1). For infrastructure tasks the "failing test" is a verification step that confirms the thing being built is absent before building it, then confirms it works after. Every user story has an explicit TDD red step before its implementation task.
Organization: No foundational blocking phase — all three user stories touch independent files and can proceed in order.
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel with other [P] tasks in the same phase
- [Story]: Which user story this task belongs to
- Exact file paths included in every task description
Phase 1: Setup
No new project structure required. The existing layout accommodates all changes.
Phase 2: User Story 1 — Dialect guard in conftest (Priority: P1) 🎯 MVP
Goal: pytest api/tests/integration/ exits immediately with a clear message if the database URL is not postgresql+asyncpg://.
Independent Test: Run TEST_DATABASE_URL=sqlite+aiosqlite:///test.db python -m pytest api/tests/integration/ -q — command exits in < 2 s with the error message Integration tests require postgresql+asyncpg:// and no tests are collected.
- T001 [US1] Confirm guard is absent (TDD red): from
api/, runTEST_DATABASE_URL=sqlite+aiosqlite:///test.db python -m pytest tests/integration/ -q --co 2>&1 | head -20— observe that tests ARE collected and note the count (guard not yet in place) - T002 [US1] Add
pytest_configurehook toapi/tests/integration/conftest.py— resolve URL viaos.getenv("TEST_DATABASE_URL") or os.getenv("DATABASE_URL", ""), callpytest.exit("Integration tests require postgresql+asyncpg://...", returncode=1)if URL does not start withpostgresql+asyncpg://; place hook before any imports that depend on the database URL - T003 [US1] Verify guard works (TDD green): run
TEST_DATABASE_URL=sqlite+aiosqlite:///test.db python -m pytest api/tests/integration/ -q— confirm immediate exit with the correct error message and zero tests collected; also confirm a validpostgresql+asyncpg://URL does not trigger the guard
Checkpoint: Dialect-mismatched test runs are blocked before any test collects.
Phase 3: User Story 2 — Docker Compose test stack (Priority: P1)
Goal: docker compose -f docker-compose.test.yml run --rm api-test runs the full integration suite against isolated PostgreSQL and MinIO services on different ports than the dev stack.
Independent Test: Run docker compose -f docker-compose.test.yml run --rm api-test from the repo root — all tests pass; verify docker compose ps shows dev services (if running) are unaffected on their original ports.
- T004 [US2] Confirm compose file is absent (TDD red): run
test -f docker-compose.test.yml && echo EXISTS || echo ABSENT— confirm output isABSENT - T005 [US2] Create
docker-compose.test.ymlat the repo root with four services:postgres-test(imagepostgres:16-alpine, host port 5433, dbreactbin_test),minio-test(imageminio/minio:latest, host ports 9002/9003),minio-init-test(creates bucketreactbin-test, depends onminio-testhealthy), andapi-test(builds from./api, runspython -m pytest tests/ -v, depends onpostgres-testhealthy andminio-init-testcompleted, environment setsTEST_DATABASE_URL=postgresql+asyncpg://reactbin:reactbin@postgres-test:5432/reactbin_test,DATABASE_URLto same value, and all S3 vars pointing tominio-test:9000with bucketreactbin-test) — follow exact design inspecs/008-postgres-integration-tests/plan.md - T006 [US2] Verify compose stack (TDD green): run
docker compose -f docker-compose.test.yml run --rm api-test— confirm all integration tests pass; confirm no errors about missing env vars or connection failures
Checkpoint: Full integration suite runs against real PostgreSQL via one command.
Phase 4: User Story 3 — Test documentation (Priority: P2)
Goal: .env.test.example and Makefile document how to run both test tiers.
Independent Test: Read .env.test.example — all variables needed for integration tests are present with example values. Run make test-unit — pytest unit suite runs without Docker and passes.
- T007 [P] [US3] Create
.env.test.exampleat the repo root documenting all variables required to run integration tests outside Docker:TEST_DATABASE_URL,DATABASE_URL,S3_ENDPOINT_URL,S3_BUCKET_NAME,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY,S3_REGION,JWT_SECRET_KEY,OWNER_USERNAME,OWNER_PASSWORD,API_BASE_URL,MAX_UPLOAD_BYTES— with example values pointing tolocalhost:5433andlocalhost:9002(test service ports); include a comment explaining how to start test services first — follow exact design inspecs/008-postgres-integration-tests/plan.md - T008 [P] [US3] Create
Makefileat the repo root with.PHONY: test-unit test-integration,test-unittarget runningcd api && python -m pytest tests/unit/ -v, andtest-integrationtarget runningdocker compose -f docker-compose.test.yml run --rm api-test - T009 [US3] Verify
make test-unit— unit tests pass without Docker (validates the Makefile target and confirms unit tests have no Docker dependency) - T010 Verify
make test-integration— Docker integration suite passes end-to-end (cross-story verification: exercises the US2 compose stack via the US3 Makefile target)
Checkpoint: All three user stories independently functional.
Phase 5: Polish & Cross-Cutting Concerns
- T011 Run
ruff check api/app/ api/tests/— zero violations (conftest change must pass ruff; fix any issues)
Dependencies & Execution Order
Phase Dependencies
- Phase 2 (US1): No external dependencies — can start immediately
- Phase 3 (US2): Depends on Phase 2 (guard must be in place so the compose stack run exercises it)
- Phase 4 (US3): T007 and T008 are independent file writes (can run in parallel with each other after Phase 3); T009 requires T008; T010 requires T008 and T006
- Phase 5 (Polish): Depends on all prior phases
Within Phase 4
- T007 ∥ T008 (different files, no dependency)
- T009 after T008 (Makefile must exist)
- T010 after T008 and T006 (requires both Makefile and compose stack)
Execution Order Summary
Step 1: T001, T002, T003 (sequential — TDD for guard)
Step 2: T004, T005, T006 (sequential — TDD for compose stack)
Step 3 (parallel): T007, T008
Step 4: T009 (after T008), T010 (after T008 + T006)
Step 5: T011
Implementation Strategy
MVP (US1 — the guard)
- Complete T001–T003
- Validate: SQLite URL is blocked; PostgreSQL URL proceeds
- US2 and US3 add the infrastructure to make the mandate practical
Incremental Delivery
- After Phase 2: Dialect bugs are caught immediately — core safety net is in place
- After Phase 3: Full integration suite runs against PostgreSQL via one Docker command
- After Phase 4: Both test tiers are documented and accessible via
make - After Phase 5: Lint clean, ready for merge