diff --git a/.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md b/.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md index 686aa30..23b7c5c 100644 --- a/.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md +++ b/.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md @@ -1,10 +1,11 @@ --- phase: 3 slug: document-migration-multi-user-isolation -status: draft -nyquist_compliant: false -wave_0_complete: false +status: partial +nyquist_compliant: true +wave_0_complete: true created: 2026-05-23 +audited: 2026-05-31 --- # Phase 3 — Validation Strategy @@ -38,29 +39,65 @@ created: 2026-05-23 | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| Migration null-user cleanup | 01 | 1 | D-02 | SEC-04 | Null-user docs deleted before NOT NULL constraint | integration | `pytest tests/test_alembic.py::test_migration_0003 -x` | ❌ W0 | ⬜ pending | -| Quota reconciliation | 01 | 1 | D-03 | STORE-03 | used_bytes matches SUM(size_bytes) post-migration | integration | `pytest tests/test_alembic.py::test_migration_0003 -x` | ❌ W0 | ⬜ pending | -| Atomic quota enforce | 02 | 2 | STORE-03 | STORE-03 | No double-spend on concurrent uploads | unit+integration | `pytest tests/test_quota.py -x` | ❌ W0 | ⬜ pending | -| Concurrent quota race | 02 | 2 | STORE-03 (SC2) | STORE-03 | Two concurrent uploads at limit → exactly one 413 | integration | `pytest tests/test_quota.py::test_concurrent_quota_race -x` | ❌ W0 | ⬜ pending | -| Quota exceeded response | 02 | 2 | STORE-05 | STORE-05 | 413 with {used_bytes, limit_bytes, rejected_bytes} | unit | `pytest tests/test_quota.py::test_quota_exceeded_response -x` | ❌ W0 | ⬜ pending | -| Atomic quota decrement | 02 | 2 | STORE-06 | STORE-06 | Delete decrements quota atomically | unit | `pytest tests/test_quota.py::test_delete_decrements_quota -x` | ❌ W0 | ⬜ pending | -| Upload-url endpoint | 02 | 2 | D-05 | SEC-04 | Creates pending Document row + returns presigned URL | unit | `pytest tests/test_documents.py::test_upload_url_endpoint -x` | ❌ W0 | ⬜ pending | -| Confirm endpoint | 02 | 2 | D-05 | STORE-03 | stat_object size used, status=uploaded set | unit | `pytest tests/test_documents.py::test_confirm_endpoint -x` | ❌ W0 | ⬜ pending | -| Quota bar endpoint | 02 | 2 | STORE-04 | — | GET /api/me/quota returns {used_bytes, limit_bytes} | unit | `pytest tests/test_documents.py::test_get_quota -x` | ❌ W0 | ⬜ pending | -| Cross-user access | 03 | 3 | SEC-04 | SEC-04 | Cross-user document access returns 404 | unit | `pytest tests/test_documents.py::test_cross_user_access_404 -x` | ❌ W0 | ⬜ pending | -| Admin 403 on documents | 03 | 3 | SEC-04 (SC4) | SEC-04 | Admin JWT on /api/documents/* returns 403 | unit | `pytest tests/test_documents.py::test_admin_cannot_access_documents -x` | ❌ W0 | ⬜ pending | -| Topic namespace isolation | 03 | 3 | DOC-04 | — | Topic list = system topics + own topics only | unit | `pytest tests/test_topics.py::test_topic_namespace -x` | ❌ W0 | ⬜ pending | -| Per-user AI provider | 04 | 4 | DOC-03/DOC-05 | — | Classifier uses user's assigned provider not global | unit | `pytest tests/test_classifier.py::test_per_user_provider -x` | ❌ W0 | ⬜ pending | -| Celery task provider lookup | 04 | 4 | DOC-05 | — | Celery task resolves provider from document owner's DB | unit | `pytest tests/test_classifier.py::test_celery_task_uses_user_provider -x` | ❌ W0 | ⬜ pending | -| Settings endpoint removed | 04 | 4 | D-12 | — | /api/settings returns 404 | unit | `pytest tests/test_settings.py::test_settings_endpoint_removed -x` | ❌ W0 | ⬜ pending | +| Atomic quota enforce | 02 | 2 | STORE-03 | STORE-03 | No double-spend on concurrent uploads | unit+integration | `pytest tests/test_quota.py::test_quota_increment_atomic -x` | ✅ | ✅ green | +| Quota exceeded response | 02 | 2 | STORE-05 | STORE-05 | 413 with {used_bytes, limit_bytes, rejected_bytes} | unit | `pytest tests/test_quota.py::test_quota_exceeded_response -x` | ✅ | ✅ green | +| Upload-url endpoint | 02 | 2 | D-05 | SEC-04 | Creates pending Document row + returns presigned URL | unit | `pytest tests/test_documents.py::test_upload_url_endpoint -x` | ✅ | ✅ green | +| Confirm endpoint | 02 | 2 | D-05 | STORE-03 | stat_object size used, status=uploaded set | unit | `pytest tests/test_documents.py::test_confirm_endpoint -x` | ✅ | ✅ green | +| Quota bar endpoint | 02 | 2 | STORE-04 | — | GET /api/me/quota returns {used_bytes, limit_bytes} | unit | `pytest tests/test_documents.py::test_get_quota -x` | ✅ | ✅ green | +| Cross-user access | 03 | 3 | SEC-04 | SEC-04 | Cross-user document access returns 404 | unit | `pytest tests/test_documents.py::test_cross_user_access_404 -x` | ✅ | ✅ green | +| Admin 403 on documents | 03 | 3 | SEC-04 (SC4) | SEC-04 | Admin JWT on /api/documents/* returns 403 | unit | `pytest tests/test_documents.py::test_admin_cannot_access_documents -x` | ✅ | ✅ green | +| Topic namespace isolation | 03 | 3 | DOC-04 | — | Topic list = system topics + own topics only | unit | `pytest tests/test_topics.py::test_topic_namespace -x` | ✅ | ✅ green | +| Per-user AI provider | 04 | 4 | DOC-03/DOC-05 | — | Classifier uses user's assigned provider not global | unit | `pytest tests/test_classifier.py::test_per_user_provider -x` | ✅ | ✅ green | +| Celery task provider lookup | 04 | 4 | DOC-05 | — | Celery task resolves provider from document owner's DB | unit | `pytest tests/test_classifier.py::test_celery_task_uses_user_provider -x` | ✅ | ✅ green | +| Settings endpoint removed | 04 | 4 | D-12 | — | /api/settings returns 404 | unit | `pytest tests/test_settings.py::test_settings_endpoint_removed -x` | ✅ | ✅ green | +| Default provider fallback | 04 | 4 | D-15 | — | Classifier falls back to app_settings defaults when user has no provider | unit | `pytest tests/test_classifier.py::test_default_provider_fallback -x` | ✅ | ✅ green | +| Documents require auth | 02 | 2 | D-16 | SEC-04 | Unauthenticated requests to /api/documents/* return 401 | unit | `pytest tests/test_documents.py::test_documents_require_auth -x` | ✅ | ✅ green | +| Admin create system topic | 03 | 3 | D-09 | — | Admin can create is_system=true topics | unit | `pytest tests/test_topics.py::test_admin_create_system_topic -x` | ✅ | ✅ green | +| User cannot create system topic | 03 | 3 | D-09 | — | Regular user POST with is_system=true returns 403 | unit | `pytest tests/test_topics.py::test_regular_user_cannot_create_system_topic -x` | ✅ | ✅ green | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* --- -## Wave 0 Requirements +## Manual-Only Tasks -- [ ] `backend/tests/test_quota.py` — stubs for STORE-03, STORE-05, STORE-06, concurrent race -- [ ] `backend/tests/test_alembic.py` — stub for migration 0003 test -- [ ] `backend/tests/test_classifier.py` — stubs for DOC-03, DOC-05 per-user provider -- [ ] `backend/tests/conftest.py` — `auth_user` fixture (authenticated user with quota row), `admin_user` fixture, MinIO mock fixtures for `presigned_put_object` and `stat_object` +> These tasks require infrastructure not available in the unit test environment. +> They must be verified manually before phase sign-off. + +| Task ID | Plan | Requirement | Why Manual | Verification Step | +|---------|------|-------------|------------|-------------------| +| Migration null-user cleanup | 01 | D-02 | Alembic upgrade requires a real PostgreSQL instance + migration scripts | `INTEGRATION=1 pytest tests/test_alembic.py::test_migration_0003 -x` against real DB | +| Quota reconciliation | 01 | D-03 | Same migration test; verifies used_bytes matches SUM(size_bytes) post-migration | `INTEGRATION=1 pytest tests/test_alembic.py::test_migration_0003 -x` against real DB | +| Concurrent quota race | 02 | STORE-03 SC2 | PostgreSQL row-level locking required; SQLite cannot emulate concurrent atomic UPDATE | `INTEGRATION=1 pytest tests/test_quota.py::test_concurrent_quota_race -x` against PostgreSQL | +| Atomic quota decrement | 02 | STORE-06 | SQLite UUID format mismatch in GREATEST() WHERE clause; real PostgreSQL required | `INTEGRATION=1 pytest tests/test_quota.py::test_delete_decrements_quota -x` against PostgreSQL | + +--- + +## Wave 0 Status + +- [x] `backend/tests/test_quota.py` — STORE-03, STORE-05, STORE-06, concurrent race +- [x] `backend/tests/test_alembic.py` — migration 0003 test (manual-only) +- [x] `backend/tests/test_classifier.py` — DOC-03, DOC-05, D-15 per-user provider +- [x] `backend/tests/test_documents.py` — D-05, D-16, STORE-04, SEC-04 +- [x] `backend/tests/test_topics.py` — DOC-04, D-09 +- [x] `backend/tests/test_settings.py` — D-12 +- [x] `backend/tests/conftest.py` — auth_user, admin_user, MinIO mock fixtures + +--- + +## Validation Audit 2026-05-31 + +| Metric | Count | +|--------|-------| +| Tasks in map | 19 | +| Automated (green) | 15 | +| Manual-only | 4 | +| Gaps found | 10 | +| Resolved (xfail removed) | 2 | +| Added to task map | 4 | +| Escalated to manual-only | 4 | + +**Changes made:** +- Removed stale `xfail` markers from `test_quota_increment_atomic` and `test_quota_exceeded_response` (both XPASS → now clean PASSED) +- Added 4 unlisted-but-passing tests to task map: `test_default_provider_fallback` (D-15), `test_documents_require_auth` (D-16), `test_admin_create_system_topic` (D-09), `test_regular_user_cannot_create_system_topic` (D-09) +- Moved 4 infrastructure-blocked tasks to Manual-Only: migration null-user cleanup, quota reconciliation, concurrent quota race, atomic quota decrement