Commit Graph
239 Commits
Author SHA1 Message Date
curo1305andClaude Sonnet 4.6 23c27efd65 fix(06): CR-03 replace raw X-Forwarded-For reads with get_client_ip()
Replace all instances of request.headers.get("X-Forwarded-For") in
cloud.py (connect_webdav, delete_connection) and documents.py (upload,
confirm, delete) with get_client_ip(request) from deps.utils. Add
get_client_ip import to both files. Prevents XFF spoofing in audit logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 23:15:48 +02:00
curo1305 63cd707d52 feat(07-04): POST /classify re-queues Celery, removes synchronous classify (D-11)
- Replace synchronous await classifier.classify_document() with extract_and_classify.delay()
- Set doc.status='processing' and commit before dispatching Celery task
- Return {'document_id': str, 'status': 'processing'} instead of assigned topics
- Promote test_reclassify_requeues_celery from xfail stub to passing test
- Add test_reclassify_cross_user_returns_404 for IDOR security invariant (T-07-10)
2026-06-04 23:10:49 +02:00
curo1305 e9ee5d4ba5 feat(07-04): Celery retry harness + _ClassificationError sentinel (D-09/D-10)
- Add _ClassificationError sentinel raised by _run() for classification failures
- Add _mark_classification_failed() async helper for final status writeback
- Change decorator to bind=True, max_retries=3
- Outer sync task catches _ClassificationError and calls self.retry(countdown=[30,90,270])
- MaxRetriesExceededError caught in nested try/except to avoid re-raise from exc block
- Promote test_retry_backoff and test_exhaustion_sets_failed_status from xfail to passing
- Tests use push_request(retries=N) + patch.object(task, "retry") to verify countdown values
- test_exhaustion_sets_failed_status uses assert_called_once_with (asyncio.run calling convention)
2026-06-04 23:08:16 +02:00
curo1305 e0cd183a63 chore: merge executor worktree (wave3-recovery/07-03) 2026-06-04 19:17:50 +02:00
curo1305 95c386f764 feat(07-03): classifier wired to load_provider_config + ai_config stub removed — D-04/D-06
- 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
2026-06-04 19:14:48 +02:00
curo1305andClaude Sonnet 4.6 382f9bec6b fix(06-06): patch OpenSSL CVE-2026-31789 — add apt-get upgrade to runtime Dockerfile stage
Apply all available OS security updates in the runtime image stage to pull in
openssl 3.5.5-1~deb13u2 (fixes CVE-2026-31789 heap buffer overflow). Three
CVEs with no upstream fix (Mesa will_not_fix, perl-base affected) documented
and suppressed in .trivyignore with rationale. Trivy rescan exits 0 (D-10 gate
passes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:13:05 +02:00
curo1305 efc177a155 feat(07-03): AnthropicProvider singleton + output_config + truncation — D-03/D-07/D-12/D-13
- Delete MAX_AI_CHARS constant and _client() method from anthropic_provider.py
- Add self._client = AsyncAnthropic(...) singleton in __init__ (D-07)
- Add _truncate() with 60/40 split using self._context_chars (D-13)
- Add _CLASSIFICATION_SCHEMA and _SUGGESTIONS_SCHEMA module-level constants
- classify() and suggest_topics() pass output_config with json_schema format (D-03)
- stop_reason != "end_turn" degrades to parse_classification("") (T-07-08)
- Widen __init__ signature to (api_key, model, context_chars, base_url) (uniform ctor)
- Update ai/__init__.py to pass context_chars + base_url to AnthropicProvider
- Promote test_anthropic_structured_output + add test_anthropic_stop_reason_fallback
2026-06-04 19:10:05 +02:00
curo1305 209b156af1 test(07-02): promote 6 Wave-2 xfail tests to passing — D-01/D-02/D-07/D-12/D-13
- 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)
2026-06-04 19:01:38 +02:00
curo1305 13eef371bc feat(07-02): registry-based get_provider(config: ProviderConfig) — D-06
- Rewrites ai/__init__.py with _REGISTRY dict (10 entries) replacing the if/elif chain
- get_provider() accepts ProviderConfig (not raw dict), raises ValueError on unknown
- Resolves effective values: api_key fallback to "not-needed", model/base_url/context_chars
  from PROVIDER_DEFAULTS when config fields are empty/zero
- SUPPORTS_JSON_MODE lookup sets supports_json_mode on GenericOpenAIProvider instances
- AnthropicProvider instantiated without base_url (Plan 03 will widen the ctor)
- provider_config.py: context_chars default changed to 0 (sentinel for PROVIDER_DEFAULTS
  resolution); 8000 default was inconsistent with O(1) factory pattern (Rule 1 fix)
2026-06-04 19:01:26 +02:00
curo1305 02bcbb9143 feat(07-02): singleton OpenAIProvider + GenericOpenAIProvider + MAX_AI_CHARS removal
- openai_provider.py: singleton self._client=AsyncOpenAI(...) in __init__ (D-07),
  _truncate() 60/40 smart truncation (D-13), removed MAX_AI_CHARS constant,
  changed __init__ signature to include context_chars, removed _client() method
- generic_openai_provider.py: new class, subclasses OpenAIProvider, conditional
  response_format={"type":"json_object"} on supports_json_mode flag (D-01/D-02),
  imports parse_classification + parse_suggestions from ai.utils (D-02 contract)
- ollama_provider.py: added context_chars kwarg with default 8000, passes through
- lmstudio_provider.py: added context_chars kwarg with default 8000, passes through
- classifier.py: removed MAX_AI_CHARS constant and text[:MAX_AI_CHARS] slices;
  truncation now handled inside each provider via _truncate()
- requirements.txt: bumped anthropic floor to >=0.95.0 (D-03 output_config support)
2026-06-04 18:58:19 +02:00
curo1305 beb5b5e49d feat(07-02): ProviderConfig Pydantic model + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE
- Creates backend/ai/provider_config.py as a pure data module (no provider imports)
- ProviderConfig(BaseModel) with extra=forbid: provider_id, api_key, base_url, model, context_chars
- PROVIDER_DEFAULTS dict with all 10 providers and verbatim base_url/model/context_chars from RESEARCH.md
- SUPPORTS_JSON_MODE dict with gemini=False per JSON Compatibility Matrix (D-02)
2026-06-04 18:56:31 +02:00
curo1305 0fd6930b41 feat(07-01): services/ai_config.py HKDF helpers, ProviderConfig loader, env seed
- Create backend/services/ai_config.py with:
  - _derive_ai_settings_key(master_key, provider_id): fresh HKDF per call, salt=provider_id.encode("utf-8"), info=b"ai-provider-settings" (domain-separated from b"cloud-credentials")
  - encrypt_api_key / decrypt_api_key: Fernet round-trip without JSON wrapping
  - load_provider_config(session): reads is_active=True row from system_settings, decrypts api_key_enc; returns stub ProviderConfig (real class lands in Plan 02)
  - seed_system_settings_from_env(session): idempotent insert of default provider on first boot
  - _ProviderConfigStub: minimal Pydantic model stub until ai/provider_config.py exists (Plan 02)
- Update backend/main.py: import seed_system_settings_from_env, call in lifespan with try/except so missing table (pre-migration fresh boot) doesn't crash startup
- Round-trip smoke test: encrypt_api_key → decrypt_api_key == original plaintext
2026-06-04 18:50:01 +02:00
curo1305 4eb317749f feat(07-01): Alembic migration 0005 + SystemSettings ORM model
- Add backend/migrations/versions/0005_system_settings.py (revision 0005, down_revision 0004)
- Creates system_settings table with 9 columns: id (UUID PK gen_random_uuid()), provider_id (String NOT NULL UNIQUE), api_key_enc (Text NULL), base_url (Text NULL), model_name (Text NOT NULL default ''), context_chars (Integer NOT NULL default 8000), is_active (Boolean NOT NULL default false), created_at/updated_at (TIMESTAMPTZ default now())
- UniqueConstraint uq_system_settings_provider_id on provider_id
- Add SystemSettings ORM model to backend/db/models.py following CloudConnection style
- downgrade() drops system_settings table cleanly
2026-06-04 18:46:11 +02:00
curo1305 4febe2f704 test(07-01): Wave 0 xfail stubs for D-01..D-16 (13 new stubs)
- Create backend/tests/test_ai_providers.py with 7 stubs: test_generic_openai_json_mode, test_anthropic_structured_output, test_get_provider_typed, test_client_singleton, test_context_chars_truncation, test_smart_truncation, test_gemini_fallback_to_parse_classification
- Create backend/tests/test_ai_config.py with 2 stubs: test_load_provider_config, test_api_key_encrypt_decrypt
- Create backend/tests/test_admin_ai_config.py with 2 stubs: test_get_never_returns_key, test_put_writes_active_provider
- Create backend/tests/test_document_tasks.py with 2 stubs: test_retry_backoff, test_exhaustion_sets_failed_status
- Append test_reclassify_requeues_celery xfail stub to test_documents.py
- D-14 regression guard: grep confirms 3 host-gateway entries (>=2 required)
2026-06-04 18:44:53 +02:00
curo1305andClaude Sonnet 4.6 f98cab30e9 chore: merge executor worktree (06-05) — trusted-proxy rate limiting + per-account limiter
Resolved merge conflicts:
- backend/main.py: keep both setup_logging (06-02) and account_limiter (06-05) imports
- backend/tests/test_rate_limiting.py: take 06-05 version (all 8 xfails promoted)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 18:31:46 +02:00
curo1305andClaude Sonnet 4.6 a826738e18 feat(06-05): trusted-proxy get_client_ip, per-account rate limiter, promote 8 xfail tests (D-11/D-12/D-13)
- 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>
2026-06-04 18:27:49 +02:00
curo1305andClaude Sonnet 4.6 378822682c feat(06-04): multi-stage Dockerfile with appuser + docker-compose runtime hardening (D-07/D-08/D-09)
- backend/Dockerfile: two-stage build (builder/runtime); runtime stage creates
  appuser uid=1000, copies installed packages from discardable builder stage,
  runs as non-root USER appuser; no --reload baked in CMD
- docker-compose.yml: backend + celery-worker get read_only:true,
  tmpfs:/tmp:mode=1777, cap_drop:ALL, security_opt:no-new-privileges:true;
  celery-beat intentionally left unhardened (Pitfall 7 — writes celerybeat-schedule)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 18:20:38 +02:00
curo1305 339cb9b991 chore: merge executor worktree (06-02) — structured logging + Loki stack 2026-06-03 18:52:13 +02:00
curo1305 5a93257ac0 feat(06-03): implement Locust load test — self-bootstrap strategy, 4 tasks, SLA listener (D-04/D-05/D-06) 2026-06-03 18:48:47 +02:00
curo1305 abe8f8ee90 feat(06-02): wire CorrelationIDMiddleware + config fields + promote 5 test stubs
- backend/config.py: add log_level: str = "INFO" and log_json: bool = False
  fields under Observability (Phase 6 — D-01) comment; pydantic-settings reads
  LOG_LEVEL / LOG_JSON env vars automatically
- backend/main.py: add imports (uuid, time, structlog, ASGIApp/Receive/Scope/Send,
  setup_logging); add CorrelationIDMiddleware raw-ASGI class (NOT BaseHTTPMiddleware
  — avoids streaming buffering); call setup_logging() as first lifespan statement;
  register CorrelationIDMiddleware LAST so it runs FIRST (Starlette reverse order)
- backend/tests/test_logging.py: remove all 5 xfail decorators; replace single-line
  bodies with real assertions for JSON renderer, contextvar binding, X-Correlation-ID
  header, no-bleed between requests, uvicorn.access propagate=False
2026-06-03 18:47:49 +02:00
curo1305 fb35f0e988 feat(06-03): add backend/requirements-dev.txt with locust pin (D-04) 2026-06-03 18:47:43 +02:00
curo1305 9fa74a91f5 feat(06-02): add structlog dependency + create services/logging.py
- Append structlog>=25.5.0 to requirements.txt under Phase 6 D-01 comment
- Create backend/services/logging.py with setup_logging(json_logs, log_level)
- shared_processors: merge_contextvars first, then add_log_level, add_logger_name,
  PositionalArgumentsFormatter, ExtraAdder, TimeStamper(iso), StackInfoRenderer
- JSON branch appends format_exc_info to shared_processors
- JSONRenderer when json_logs=True, ConsoleRenderer when False
- ProcessorFormatter bridges uvicorn/stdlib loggers through same chain
- Idempotent: clears root handlers before installing new one
- uvicorn.access propagate=False — middleware owns request logging
2026-06-03 18:44:37 +02:00
curo1305 594eb46efe feat(06-01): add backend/load_tests/ package skeleton with Locust stub (D-04/D-05/D-06)
- backend/load_tests/__init__.py — empty package marker; stops pytest discovery
- backend/load_tests/locustfile.py — DocuVaultUser skeleton (on_start,
  _auth_headers, list_documents, upload_document, refresh_token methods)
  and check_sla SLA listener; all bodies raise NotImplementedError
- No application imports; TEST_EMAIL/TEST_PASSWORD from env vars only
- Full implementation in plan 06-03
2026-06-03 18:37:23 +02:00
curo1305 56d9da7be1 test(06-01): add 8 xfail stubs for rate limiting (D-11/D-12 + A1)
- 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
2026-06-03 18:36:35 +02:00
curo1305 e1f8874b9d test(06-01): add 5 xfail stubs for structured logging (D-01/D-02)
- test_setup_logging_emits_json_when_LOG_JSON_true
- test_correlation_id_middleware_binds_contextvar
- test_correlation_id_response_header_present
- test_contextvars_cleared_between_requests (Pitfall 2 guard)
- test_uvicorn_access_log_suppressed
All strict=False xfail stubs; implementation target: plan 06-02
2026-06-03 18:35:55 +02:00
curo1305andClaude Sonnet 4.6 a548266461 refactor(backend): extract shared helper modules per architecture rules
- 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>
2026-06-02 16:10:35 +02:00
curo1305andClaude Sonnet 4.6 2686fde2d7 feat(06.2): log attempted email on failed login and surface it in audit log
- auth.py: store attempted_email in metadata_ and link user_id when the account exists (wrong password case); previously logged no PII at all
- AuditLogTab: Email column falls back to metadata_.attempted_email in amber with "(attempted)" label when no confirmed user_email is available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 21:02:37 +02:00
curo1305andClaude Sonnet 4.6 7027347597 fix(06.2): audit log — add email column, remove @ prefix from handles
- 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>
2026-06-01 20:59:09 +02:00
curo1305andClaude Sonnet 4.6 d771f0805d fix(06.2): shared badge and recipient handle missing in FileManagerView
- shares.py grant_share: include recipient_handle in response so ShareModal shows the name immediately without reload
- FileManagerView: add Shared pill badge next to document name (badge only existed in DocumentCard, not the main file manager view)
- FileManagerView ShareModal: wire @unshared to clear is_shared flag when last recipient is removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 19:47:00 +02:00
curo1305andClaude Sonnet 4.6 089da94d8b fix(security): apply two findings from sharing security review
- get_document: strip extracted_text for share recipients (T-04-04-03 consistency)
- ShareModal: emit 'unshared' when last recipient is revoked; DocumentCard clears is_shared badge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 19:41:15 +02:00
curo1305andClaude Sonnet 4.6 a0f6c2f663 fix(06.2): resolve four sharing UX issues found in re-test UAT
- AccountView: remove hardcoded @ prefix so handle matches what share dialog expects
- documents store: set is_shared=true optimistically after successful share so badge shows without refetch
- GET /api/documents/{id}: allow recipients of an active share to view the document (was returning 404 for non-owners)
- ShareModal: move Share button to its own full-width row so it no longer overflows the input area

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 19:32:51 +02:00
curo1305andClaude Sonnet 4.6 cc2825b3b7 fix(phase-04): use MagicMock(spec=MinIOBackend) in test_daily_export_download
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>
2026-06-01 18:14:54 +02:00
curo1305andClaude Sonnet 4.6 b245fcc527 fix(phase-03): use UUID.hex in raw SQL to fix SQLite UUID format mismatch
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>
2026-06-01 15:43:01 +02:00
curo1305 683670afa1 fix(06.2): WR-02 constrain format parameter to Literal[csv] to reject unsupported formats 2026-06-01 14:31:32 +02:00
curo1305 2072c3ddcd fix(06.2): WR-08 delete_document defers commit so audit log writes in same transaction 2026-06-01 14:30:31 +02:00
curo1305 50b6e7fd06 fix(06.2): WR-07 document X-Forwarded-For trust boundary in all IP extraction code 2026-06-01 14:29:35 +02:00
curo1305 1f2cec9ac3 fix(06.2): CR-07 add audit log entry for PATCH /shares/{share_id} permission change 2026-06-01 14:27:08 +02:00
curo1305 1a34209bb0 fix(06.2): CR-06 RFC 5987-encode Content-Disposition filename to prevent header injection 2026-06-01 14:26:46 +02:00
curo1305 653cb3a98b fix(06.2): CR-05 remove UUID dash-stripping in quota SQL — PostgreSQL expects dashed UUID format 2026-06-01 14:26:24 +02:00
curo1305 792d4639d1 fix(06.2): CR-03 serialize metadata_ with json.dumps in CSV export instead of Python repr 2026-06-01 14:25:29 +02:00
curo1305 50859bb430 fix(06.2): CR-02 add MinIOBackend guard in download_daily_export before accessing _client 2026-06-01 14:25:06 +02:00
curo1305 a3ad36cc82 fix(06.2): CR-01 event-type filter uses prefix LIKE match instead of exact equality 2026-06-01 14:24:50 +02:00
curo1305 46f7505e36 chore: merge executor worktree (worktree-agent-af66944050628b0e4) 2026-05-31 15:23:36 +02:00
curo1305andClaude Sonnet 4.6 62daf0d750 test(phase-04): fill Nyquist validation gaps — FOLD-04, FOLD-05, SEC-08, SEC-09
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>
2026-05-31 15:21:02 +02:00
curo1305 839bfe0ffe feat(06.2-04): backend — handle enrichment, user_handle filter, two daily-export endpoints
- Add _audit_to_dict_with_handles() with user_handle + actor_handle fields
- Add _build_filtered_query_with_handles() with aliased User double-JOIN
- Change list_audit_log user_id param to user_handle string with handle→UUID resolution
- Change export_audit_log user_id param to user_handle (Pitfall 7 — both endpoints enriched)
- Add GET /audit-log/daily-exports — lists MinIO audit-logs bucket, asyncio.to_thread
- Add GET /audit-log/daily-exports/{date} — streams CSV, date regex validation (T-06.2-04-01)
- Move daily-export endpoints before viewer to ensure specific path registration order
- Update test_audit_log_export_csv to match enriched CSV header (user_handle, actor_handle)
- All 10 test_audit.py tests pass
2026-05-31 15:17:53 +02:00
curo1305 d7cfc5ccee test(06.2-04): add failing tests for handle enrichment, user_handle filter, daily exports
- test_audit_log_includes_user_handle: asserts user_handle/actor_handle in items
- test_audit_log_filter_by_handle: asserts filtering by handle works correctly
- test_audit_log_filter_unknown_handle: asserts 200+empty for unknown handle
- test_daily_exports_list: mocks MinIO list_objects, asserts sorted items
- test_daily_export_download: mocks MinIO get_object, asserts CSV response + 404 on bad date
2026-05-31 15:15:46 +02:00
curo1305andClaude Sonnet 4.6 95c7ed786a feat(06.2-03): backend — cloud-aware delete routing + skip_quota + remove_only param
- storage.delete_document gains skip_quota=False param; quota decrement gated on it
- DELETE /api/documents/{id} gains remove_only=bool query param
- Cloud docs (storage_backend != minio): attempt cloud backend delete_object first
  - On failure: return HTTP 200 {success: false, cloud_delete_failed: true} (not 4xx)
  - On success or remove_only: delete DB row with skip_quota=True
- Cloud creds/exception message never included in response body (T-06.2-03-02)
- Promote 3 xfail stubs to real tests (propagates, failure, remove_only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 15:09:44 +02:00
curo1305andClaude Sonnet 4.6 1ee27da332 test(phase-03): remove stale xfail markers from quota tests
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>
2026-05-31 15:07:18 +02:00
curo1305andClaude Sonnet 4.6 ea231853e9 feat(06.2-02): backend — ShareCreate.permission field + PATCH /{share_id} endpoint
- 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>
2026-05-31 15:04:53 +02:00
curo1305andClaude Sonnet 4.6 d98e3ab7a1 test(phase-02): add Nyquist validation tests — fill SEC-05, AUTH-08, SEC-03 and frontend gaps
8 test files, 60 new tests (14 backend + 46 frontend). All green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:04:21 +02:00