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>
This commit is contained in:
curo1305
2026-05-31 15:09:44 +02:00
parent e812922a26
commit 95c7ed786a
3 changed files with 139 additions and 23 deletions
+95 -5
View File
@@ -636,16 +636,106 @@ async def test_stream_document_content_cloud_backend_error(async_client, auth_us
# ---------------------------------------------------------------------------
async def test_delete_cloud_document_propagates(async_client, auth_user, db_session):
async def test_delete_cloud_document_propagates(async_client, auth_user, db_session, monkeypatch):
"""DELETE /api/documents/{id} for a cloud doc calls cloud backend delete_object (D-01)"""
pytest.xfail("Phase 6.2 — not implemented yet")
import uuid as _uuid
from unittest.mock import AsyncMock
from db.models import Document
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=auth_user["user"].id,
filename="gdrive_doc.pdf",
content_type="application/pdf",
size_bytes=512,
storage_backend="google_drive",
status="uploaded",
object_key=f"{auth_user['user'].id}/{doc_id}/{_uuid.uuid4()}.pdf",
)
db_session.add(doc)
await db_session.commit()
mock_backend = AsyncMock()
mock_backend.delete_object = AsyncMock(return_value=None)
async def fake_get_backend(*args, **kwargs):
return mock_backend
monkeypatch.setattr("api.documents.get_storage_backend_for_document", fake_get_backend)
resp = await async_client.delete(f"/api/documents/{doc_id}", headers=auth_user["headers"])
assert resp.status_code == 200, resp.text
assert resp.json()["success"] is True
mock_backend.delete_object.assert_called_once()
# DB row removed
deleted = await db_session.get(Document, doc_id)
assert deleted is None
async def test_delete_cloud_document_failure(async_client, auth_user, db_session):
async def test_delete_cloud_document_failure(async_client, auth_user, db_session, monkeypatch):
"""DELETE /api/documents/{id} returns cloud_delete_failed=True when provider raises (D-03)"""
pytest.xfail("Phase 6.2 — not implemented yet")
import uuid as _uuid
from unittest.mock import AsyncMock
from db.models import Document
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=auth_user["user"].id,
filename="gdrive_fail.pdf",
content_type="application/pdf",
size_bytes=512,
storage_backend="google_drive",
status="uploaded",
object_key=f"{auth_user['user'].id}/{doc_id}/{_uuid.uuid4()}.pdf",
)
db_session.add(doc)
await db_session.commit()
async def raise_provider_error(*args, **kwargs):
raise RuntimeError("provider error")
monkeypatch.setattr("api.documents.get_storage_backend_for_document", raise_provider_error)
resp = await async_client.delete(f"/api/documents/{doc_id}", headers=auth_user["headers"])
assert resp.status_code == 200, resp.text
body = resp.json()
assert body.get("cloud_delete_failed") is True, f"Expected cloud_delete_failed=True, got {body}"
assert body.get("success") is False
# DB row must NOT be deleted (soft-failure path)
still_there = await db_session.get(Document, doc_id)
assert still_there is not None, "DB row should not be deleted when cloud delete fails"
async def test_delete_cloud_remove_only(async_client, auth_user, db_session):
"""DELETE /api/documents/{id}?remove_only=true skips cloud delete, removes DB row only (D-02)"""
pytest.xfail("Phase 6.2 — not implemented yet")
import uuid as _uuid
from db.models import Document, Quota
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=auth_user["user"].id,
filename="gdrive_remove_only.pdf",
content_type="application/pdf",
size_bytes=1024,
storage_backend="google_drive",
status="uploaded",
object_key=f"{auth_user['user'].id}/{doc_id}/{_uuid.uuid4()}.pdf",
)
db_session.add(doc)
await db_session.commit()
resp = await async_client.delete(
f"/api/documents/{doc_id}?remove_only=true",
headers=auth_user["headers"],
)
assert resp.status_code == 200, resp.text
assert resp.json()["success"] is True
# DB row removed
deleted = await db_session.get(Document, doc_id)
assert deleted is None