- Remove _ProviderConfigStub from services/ai_config.py (replaced by real ProviderConfig)
- Add module-level import: from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS
- load_provider_config() now returns ProviderConfig (no lazy import inside function body)
- classifier.py: replace inline _settings dict with load_provider_config(session) call (D-06)
- Per-user override path builds ProviderConfig from PROVIDER_DEFAULTS (no api_key — T-07-06)
- Fallback to app_settings defaults when load_provider_config returns None (D-15)
- Truncation delegated to provider._truncate() — no more text slices in classifier (D-12/D-13)
- Promote test_api_key_encrypt_decrypt to passing (round-trip + domain salt isolation)
- Update test_classifier.py: test_per_user_provider + test_default_provider_fallback use ProviderConfig assertions
- test_get_provider_typed: registry get_provider() returns correct class + ValueError on bogus id
- test_client_singleton: AsyncOpenAI instantiated once per provider instance (D-07)
- test_generic_openai_json_mode: response_format present/absent based on supports_json_mode (D-01)
- test_context_chars_truncation: _truncate() shortens long text (D-12)
- test_smart_truncation: 60% head + 40% tail truncation strategy (D-13)
- test_gemini_fallback_to_parse_classification: supports_json_mode=False skips
response_format AND routes through parse_classification() from ai.utils (D-02 contract)
- test_anthropic_structured_output remains xfail (Plan 07-03)
- D-11: replace get_client_ip body with trusted-proxy CIDR check (127/8, 172.16/12,
192.168/16, ::1/128); untrusted peers always return their own IP, preventing XFF
spoofing by external callers
- D-12: create services/rate_limiting.py with _account_key() keyed by user.id
(falls back to peer IP when no authenticated user in request.state); exports
account_limiter = Limiter(key_func=_account_key)
- Wire: auth.py switches from get_remote_address to get_client_ip; main.py imports
account_limiter; all 9 documents.py endpoints and 7 cloud.py endpoints decorated
@account_limiter.limit("100/minute") with request.state.current_user = current_user
as the first handler statement (A1 ordering invariant)
- Promote all 8 xfail stubs to real assertions; add autouse fixture in conftest.py
to reset MemoryStorage between tests preventing cross-test 429 contamination
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- D-11: 4 stubs for trusted-proxy CIDR logic in get_client_ip
(untrusted peer, trusted+XFF, trusted+no-XFF, None client)
- D-12: 2 stubs for account_limiter key_func (user_id, fallback to IP)
- A1: test_account_limiter_key_ordering_assumption — explicit gate that
request.state.current_user is readable by key_func before counting
- D-12 integration: 429 after 100 req/min with same auth_user
All strict=False xfail stubs; implementation target: plan 06-04
- Add backend/ai/utils.py — parse_classification, parse_suggestions, strip_code_fences
shared by all AI providers; removes duplicated private functions from
anthropic_provider.py and openai_provider.py
- Add backend/deps/utils.py — get_client_ip, parse_uuid request-parsing helpers;
removes local _ip() variants from admin.py, auth.py, shares.py, folders.py
- Add backend/storage/exceptions.py — canonical CloudConnectionError definition;
all routers and backends import from here instead of redefining
- Move validate_password_strength to backend/services/auth.py; removes duplicated
_validate_password_strength from admin.py and auth.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: add user_email to _build_filtered_query_with_handles (UserSubject join) and _audit_to_dict_with_handles; propagate through JSON viewer and CSV export including empty-result path
- Frontend: AuditLogTab adds Email column between User and Action Type; removes @ prefix from handle cell
- Test: update expected CSV header to include user_email
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plain MagicMock() failed the isinstance(backend, MinIOBackend) guard in
download_daily_export(), returning 404. spec=MinIOBackend sets __class__
so isinstance passes and the mock path executes correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
str(uuid_obj) produces dashed 36-char format; SQLite stores UUID as 32-char
hex without dashes, so WHERE user_id = :uid never matched. Using .hex fixes
confirm_upload (api/documents.py) and delete_document (services/storage.py).
Removes stale xfail from test_delete_decrements_quota — now passes on SQLite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 6 new tests covering document sort (name/size), FTS search cross-user
isolation, credentials_enc exclusion from all responses, and MinIO object
cleanup on user deletion.
Fix FTS try/except misplacement in api/documents.py — was wrapping the ORM
statement builder (never raises) instead of the execute call, causing HTTP 500
on SQLite test env. Now falls back to unfiltered results when @@ unsupported.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_quota_increment_atomic and test_quota_exceeded_response were marked
xfail for PostgreSQL but pass on SQLite — markers removed, tests now PASSED.
Concurrent race and delete decrement keep xfail; they require real PG locking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add permission field (default "view") with field_validator to ShareCreate
- Add SharePermissionPatch model with same validator
- Wire body.permission into grant_share() Share constructor
- Add PATCH /{share_id} endpoint with IDOR protection (T-06.2-02-01)
- Promote 3 xfail stubs to real tests (create_with_permission, patch_permission, patch_idor)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_share_default_permission_view: asserts permission='view' in POST
response and owner's GET /api/shares list (SHARE-03)
- test_share_indicator_in_owner_list: asserts is_shared flips True in
owner's GET /api/documents after sharing (SHARE-05)
All 14 phase tests now pass (9 shares + 5 audit).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WR-01: extend nested metadata_ forbidden-key check to all 4 keys
WR-02: assert no forbidden fields in CSV export body (D-15)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @pytest_asyncio.fixture second_auth_user with handle prefix 'user2_'
- Creates User + Quota row following the same pattern as auth_user
- Returns {user, token, headers} dict shape for use in sharing tests
- Add test_concurrent_put_objects to test_storage.py (STORE-07: verifies no
per-instance lock blocks concurrent MinIO workers via asyncio.gather)
- Remove @pytest.mark.xfail from test_confirm_endpoint; test now passes on
SQLite after uuid format fix in api/documents.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CR-01: add `except HTTPException: raise` before broad except in
stream_document_content — prevents 503 (reconnect prompt) from being
swallowed and replaced with misleading 502
CR-02: move pre-flight credential checks BEFORE Redis setex in
oauth_initiate — no orphan state tokens written for unconfigured providers;
also adds onedrive_tenant_id to OneDrive pre-flight condition (WR-02)
CR-03: add CLOUD_CREDS_KEY to celery-worker environment in docker-compose.yml
— worker cannot decrypt cloud credentials without this key; every cloud
document task was silently failing at runtime
WR-03: assert Redis store empty after 400 pre-flight responses in both
new tests — confirms no token leak on misconfigured-provider requests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- oauth_initiate: pre-flight check returns 400 with env-var hint when
GOOGLE_CLIENT_ID/SECRET or ONEDRIVE_CLIENT_ID/SECRET are not configured,
preventing opaque MSAL/OAuth library 500 errors on misconfigured servers
- stream_document_content: broad except-clause catches non-CloudConnectionError
exceptions and returns 502 with user-friendly message (was raw 500)
- docker-compose.yml: add volumes: - ./backend:/app to celery-worker so code
changes are picked up by docker compose restart without a rebuild
- CloudStorageView: upload hint paragraph directs users to navigate into a
cloud folder; no DropZone added (no folder context at overview level)
- 3 new backend tests pass; 2 existing tests patched with credential monkeypatch;
full suite: 293 passed, 0 new failures, 1 pre-existing (test_extract_docx)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cloud-aware routing added in 05-09 checks doc.storage_backend; MagicMock
attribute is truthy and != 'minio', so the test was entering the cloud branch
without any mock for get_storage_backend_for_document. Regression: test passed
before 05-09 when _run() had no cloud routing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_delete_user_correct_password: 204 on correct admin password
- test_delete_user_wrong_password: 403 on wrong password, user survives
- test_delete_user_no_body: 422 when no body provided (Pydantic validation)
- Remove response_class=RedirectResponse from @router.get decorator
- Replace both RedirectResponse(status_code=302) returns with JSONResponse({url})
- Frontend can now inject Bearer header before navigating to OAuth URL (T-05-10-01)
- Update test_connect_google_drive to expect 200 JSON (regression fix)
- Add DocumentPatch Pydantic model with filename and folder_id optional fields
- Add PATCH /api/documents/{doc_id} endpoint: ownership guard, model_fields_set
to distinguish absent vs null folder_id, returns updated metadata dict
- Update _run() in document_tasks.py to use get_storage_backend_for_document
for non-MinIO backends instead of hardcoded MinIO path
- CloudConnectionError caught in cloud path: returns extract_failed status
- Update test to use pure unit mocks (no PostgreSQL) for _run() cloud routing
- All 3 plan tests pass; 23 test_cloud.py tests pass
- validate_cloud_url(): blocks RFC-1918 (10.x, 172.16.x, 192.168.x), loopback (127.x),
link-local (169.254.x), IPv6 loopback (::1), ULA (fc00::/7), and 'localhost' string;
resolves DNS via socket.getaddrinfo BEFORE IP check (anti-DNS-rebinding per D-17)
- _derive_fernet_key(): creates fresh HKDF-SHA256 instance per call (AlreadyFinalized
pitfall avoided per RESEARCH.md Pitfall 3); uses user_id as salt for per-user isolation
- encrypt_credentials(): Fernet-encrypts JSON-serialised credentials dict; returns str
- decrypt_credentials(): decrypts Fernet token back to original dict
- [Rule 1 - Bug] Fixed test_allows_public_https to use 8.8.8.8 IP (cloud.example.com
does not resolve in offline CI environments)
Adds the unified file manager view (Windows Explorer-style), collapsible
folder tree sidebar item, full vitest test suite (55 tests, 4 files), and
commits all Phase 4 backend/frontend fixes that were staged but uncommitted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>