When API_DOCS_ENABLED=false, FastAPI registers no routes for /docs, /redoc, or /openapi.json, returning 404 for all three. Default is true for backwards compatibility. Invalid values fall back to true (FR-007). Fix: Remove tests/ and alembic/ from api/.dockerignore so the test Dockerfile (which uses COPY . .) includes the test suite; Dockerfile.prod is unaffected as it only copies app/ explicitly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.8 KiB
Tasks: API Documentation Visibility Gate
Input: Design documents from specs/012-api-docs-gate/
Prerequisites: plan.md ✅, spec.md ✅, research.md ✅, contracts/docs-endpoints.md ✅, quickstart.md ✅
Tests: TDD is non-negotiable (§5.1). Failing tests are written before implementation code in each phase.
Organization: No setup or foundational phases — this feature modifies three existing files and adds one new test file. Phase 3 (US1) covers the disable path; Phase 4 (US2) verifies the enable/default path using the same implementation; Phase 5 polishes.
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 3: User Story 1 — Documentation Hidden in Production (Priority: P1) 🎯 MVP
Goal: When API_DOCS_ENABLED=false, all three documentation endpoints (/docs, /redoc, /openapi.json) return 404. All other endpoints are unaffected.
Independent Test: make test-unit passes the new settings tests; make test-integration passes the new test_docs_disabled integration test.
Tests for User Story 1 (TDD — write first, confirm failure before T003)
-
T001 [US1] Add three failing unit tests to
api/tests/unit/test_config.pyusing the existing_apply_env/_BASE_ENVpattern:test_api_docs_enabled_default— callSettings()with_BASE_ENVonly (noAPI_DOCS_ENABLED); asserts.api_docs_enabled is Truetest_api_docs_enabled_false— callSettings()with_BASE_ENV+{"API_DOCS_ENABLED": "false"}; asserts.api_docs_enabled is Falsetest_api_docs_invalid_value_defaults_to_enabled— callSettings()with_BASE_ENV+{"API_DOCS_ENABLED": "not-a-bool"}; asserts.api_docs_enabled is True(graceful fallback, FR-007) All three tests fail before T003 becauseapi_docs_enableddoes not yet exist onSettings.
-
T002 [US1] Create
api/tests/integration/test_docs_gate.pywith two failing integration tests; the file MUST set up a minimal app client usingfrom starlette.testclient import TestClientand theimportlib.reload+get_settings.cache_clear()pattern shown in plan.md:test_docs_hidden_when_flag_disabled(monkeypatch)— setAPI_DOCS_ENABLED=falsevia monkeypatch + all required env vars (DATABASE_URL,JWT_SECRET_KEY,OWNER_USERNAME,OWNER_PASSWORD,S3_ENDPOINT_URL,S3_BUCKET_NAME,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY); callget_settings.cache_clear();importlib.reload(app.main); createTestClient(app.main.app); assert/docs→ 404,/redoc→ 404,/openapi.json→ 404,/api/v1/health→ 200; after test, callget_settings.cache_clear()again as cleanuptest_docs_visible_when_flag_enabled(monkeypatch)— same setup but withAPI_DOCS_ENABLED=true(or omit it); assert/docs→ 200,/redoc→ 200,/openapi.json→ 200 Both tests fail before T003/T004 becauseapi_docs_enableddoes not exist onSettings.
Implementation for User Story 1
-
T003 [US1] Add
api_docs_enabled: bool = Truefield and acoerce_docs_enabledfield validator to theSettingsclass inapi/app/config.py: the validator MUST usemode='before', be a@classmethod, and wrap Pydantic bool coercion in a try/except that returnsTrueon any exception (implements FR-007); importfield_validatorfrompydanticat the top of the file; the field goes after the existinglogin_trusted_proxy_ipsfield. -
T004 [US1] Update
api/app/main.py: before theapp = FastAPI(...)call, add_settings = get_settings(); adddocs_url="/docs" if _settings.api_docs_enabled else None,redoc_url="/redoc" if _settings.api_docs_enabled else None, andopenapi_url="/openapi.json" if _settings.api_docs_enabled else Noneas keyword arguments to theFastAPI()constructor; the existing module-level defaults forapp.state(after theapp = FastAPI(...)line) are unchanged. -
T005 [US1] Verify TDD green for US1: run
cd api && python -m pytest tests/unit/ -v -k "docs"and confirm all three new unit tests pass; then runcd api && python -m pytest tests/unit/ -vto confirm no regressions in the full 102-test unit suite.
Checkpoint: US1 is complete. With API_DOCS_ENABLED=false the three docs endpoints return 404; all other endpoints are unaffected.
Phase 4: User Story 2 — Documentation Available in Development (Priority: P2)
Goal: Without the flag set (or with it set to true), docs endpoints behave identically to before this feature. Default is backwards compatible.
Independent Test: make test-integration — the test_docs_visible_when_flag_enabled test written in T002 passes, confirming the enabled/default path.
- T006 [US2] Verify TDD green for US2: run
make test-integrationfrom/workspaceand confirm all integration tests pass, includingtest_docs_gate.py::test_docs_visible_when_flag_enabledand the full existing suite (102 tests + 2 new = 104 total).
Checkpoint: Both user stories verified. Flag disabled → 404; flag enabled or absent → unchanged behaviour.
Phase 5: Polish & Cross-Cutting Concerns
-
T007 Add documentation for
API_DOCS_ENABLEDto/workspace/.env.example: insert a new section after theLOGIN_TRUSTED_PROXY_IPSblock with a comment andAPI_DOCS_ENABLED=true; the comment MUST note that this should be set tofalsein production to avoid publicly exposing the API schema -
T008 Run
ruff check api/app/config.py api/app/main.py api/tests/integration/test_docs_gate.pyfrom/workspace/apiand fix any lint violations; then runruff check api/to confirm the full API directory is clean
Dependencies & Execution Order
- T001 and T002 can run in parallel (different files, both TDD-red before implementation)
- T003 must complete before T004 (main.py reads from config.py)
- T005 after T003 and T004
- T006 after T005
- T007 and T008 can run in parallel (different files, after all tests pass)
Execution Order Summary
Step 1: T001 ∥ T002 (write failing tests — TDD red)
Step 2: T003 (implement config.py — turns T001 green)
Step 3: T004 (implement main.py — turns T002 green)
Step 4: T005 (verify unit tests green)
Step 5: T006 (verify integration tests green — regression gate)
Step 6: T007 ∥ T008 (polish — .env.example + ruff)
Implementation Strategy
MVP (US1 + US2 — one implementation covers both)
- Write failing tests (T001, T002)
- Add
api_docs_enabledtoconfig.py(T003) - Update
FastAPI()constructor inmain.py(T004) - Verify all tests green (T005, T006)
- Polish (T007, T008)
US1 and US2 share the same implementation — the flag controls both paths. There is no separate implementation for US2; the default value of true is the entire implementation of US2.