test(05-09): add failing tests for PATCH /documents/{id} and cloud-aware re-analyze
- test_patch_document_filename: expects 200 with updated filename (PATCH endpoint missing → 405) - test_patch_document_wrong_owner: expects 404 for non-owner (PATCH endpoint missing → 405) - test_reanalyze_cloud_document_routes_to_cloud_backend: expects cloud backend called for nextcloud docs
This commit is contained in:
@@ -563,3 +563,125 @@ async def test_cross_user_idor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
# ── Plan 09 tests: PATCH /documents/{id} and cloud-aware re-analyze ──────────
|
||||||
|
|
||||||
|
async def test_patch_document_filename(async_client, db_session):
|
||||||
|
"""PATCH /api/documents/{id} with {filename} returns 200 with updated filename.
|
||||||
|
|
||||||
|
Covers T-05-09-01: ownership enforced via get_regular_user.
|
||||||
|
"""
|
||||||
|
from db.models import Document
|
||||||
|
|
||||||
|
auth = await _create_user_and_token(db_session, role="user")
|
||||||
|
|
||||||
|
# Create a document owned by this user
|
||||||
|
doc_id = _uuid.uuid4()
|
||||||
|
doc = Document(
|
||||||
|
id=doc_id,
|
||||||
|
user_id=auth["user"].id,
|
||||||
|
filename="original.pdf",
|
||||||
|
content_type="application/pdf",
|
||||||
|
size_bytes=1024,
|
||||||
|
storage_backend="minio",
|
||||||
|
status="uploaded",
|
||||||
|
object_key=f"{auth['user'].id}/{doc_id}/some-uuid.pdf",
|
||||||
|
)
|
||||||
|
db_session.add(doc)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
resp = await async_client.patch(
|
||||||
|
f"/api/documents/{doc_id}",
|
||||||
|
json={"filename": "renamed.pdf"},
|
||||||
|
headers=auth["headers"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["filename"] == "renamed.pdf" or data.get("original_name") == "renamed.pdf"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_patch_document_wrong_owner(async_client, db_session):
|
||||||
|
"""PATCH /api/documents/{id} by a non-owner returns 404 (IDOR protection).
|
||||||
|
|
||||||
|
Covers T-05-09-01: cross-user access returns 404, not 403, to avoid leaking
|
||||||
|
which document IDs exist for other users (D-16, T-03-11).
|
||||||
|
"""
|
||||||
|
from db.models import Document
|
||||||
|
|
||||||
|
auth1 = await _create_user_and_token(db_session, role="user")
|
||||||
|
auth2 = await _create_user_and_token(db_session, role="user")
|
||||||
|
|
||||||
|
# Create a document owned by user1
|
||||||
|
doc_id = _uuid.uuid4()
|
||||||
|
doc = Document(
|
||||||
|
id=doc_id,
|
||||||
|
user_id=auth1["user"].id,
|
||||||
|
filename="private.pdf",
|
||||||
|
content_type="application/pdf",
|
||||||
|
size_bytes=512,
|
||||||
|
storage_backend="minio",
|
||||||
|
status="uploaded",
|
||||||
|
object_key=f"{auth1['user'].id}/{doc_id}/some-uuid.pdf",
|
||||||
|
)
|
||||||
|
db_session.add(doc)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
# User2 tries to rename user1's document
|
||||||
|
resp = await async_client.patch(
|
||||||
|
f"/api/documents/{doc_id}",
|
||||||
|
json={"filename": "hacked.pdf"},
|
||||||
|
headers=auth2["headers"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reanalyze_cloud_document_routes_to_cloud_backend(db_session):
|
||||||
|
"""Re-analyze task calls get_storage_backend_for_document for cloud documents.
|
||||||
|
|
||||||
|
Verifies that doc.storage_backend != 'minio' causes _run() to use the cloud
|
||||||
|
backend path instead of the MinIO path (Plan 09, requirement CLOUD-07).
|
||||||
|
"""
|
||||||
|
from db.models import Document
|
||||||
|
from tasks.document_tasks import _run
|
||||||
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
|
|
||||||
|
auth = await _create_user_and_token(db_session, role="user")
|
||||||
|
|
||||||
|
# Create a nextcloud document
|
||||||
|
doc_id = _uuid.uuid4()
|
||||||
|
doc = Document(
|
||||||
|
id=doc_id,
|
||||||
|
user_id=auth["user"].id,
|
||||||
|
filename="cloud.pdf",
|
||||||
|
content_type="application/pdf",
|
||||||
|
size_bytes=2048,
|
||||||
|
storage_backend="nextcloud",
|
||||||
|
status="uploaded",
|
||||||
|
object_key="nc_file_id_xyz",
|
||||||
|
)
|
||||||
|
db_session.add(doc)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
# Mock cloud backend: returns file bytes, enabling extraction to proceed
|
||||||
|
mock_cloud_backend = AsyncMock()
|
||||||
|
mock_cloud_backend.get_object = AsyncMock(return_value=b"%PDF-1.4 fake content")
|
||||||
|
|
||||||
|
# Mock MinIO backend to verify it is NOT called
|
||||||
|
mock_minio_backend = AsyncMock()
|
||||||
|
mock_minio_backend.get_object = AsyncMock(return_value=b"should not be called")
|
||||||
|
|
||||||
|
with patch("tasks.document_tasks.get_storage_backend_for_document", return_value=mock_cloud_backend) as mock_gsb_doc, \
|
||||||
|
patch("tasks.document_tasks.get_storage_backend", return_value=mock_minio_backend) as mock_gsb:
|
||||||
|
result = await _run(str(doc_id))
|
||||||
|
|
||||||
|
# Cloud backend's get_object must have been called
|
||||||
|
mock_cloud_backend.get_object.assert_called_once_with("nc_file_id_xyz")
|
||||||
|
|
||||||
|
# MinIO backend's get_object must NOT have been called
|
||||||
|
mock_minio_backend.get_object.assert_not_called()
|
||||||
|
|
||||||
|
# Result must not be an error from MinIO path
|
||||||
|
assert result.get("status") != "extract_failed" or "MinIO" not in result.get("error", "")
|
||||||
|
|||||||
Reference in New Issue
Block a user