test(phase-4-05): add failing tests for document streaming proxy (DOC-02)

- test_content_stream_200: 200 response with correct headers
- test_content_stream_206_range: Range header returns 206 + Content-Range
- test_content_stream_admin_403: admin role blocked by get_regular_user
- test_content_stream_no_presigned_url: presigned_get_url never called
- test_content_stream_share_recipient_200: share recipient access
- test_content_stream_not_found/invalid_id: 404 paths
- test_parse_range_416: out-of-bounds Range header returns 416
This commit is contained in:
curo1305
2026-05-25 18:47:24 +02:00
parent 731857231f
commit 8e6cb6e7d0
+242 -14
View File
@@ -339,29 +339,257 @@ async def test_documents_require_auth(async_client):
# ---------------------------------------------------------------------------
# Wave 0 xfail stubs for Phase 4 DOC-02 proxy / content-stream tests
# Phase 4 DOC-02 proxy / content-stream tests
# ---------------------------------------------------------------------------
@pytest.mark.xfail(strict=False)
async def test_content_stream_200(async_client, auth_user):
async def test_content_stream_200(async_client, auth_user, db_session, monkeypatch):
"""GET /api/documents/{id}/content returns 200 with correct Content-Type and Content-Disposition: inline."""
pytest.xfail("not implemented yet")
import uuid as _uuid
from unittest.mock import AsyncMock
from db.models import Document
from storage.minio_backend import MinIOBackend
file_bytes = b"Hello, PDF content!"
monkeypatch.setattr(MinIOBackend, "get_object", AsyncMock(return_value=file_bytes), raising=False)
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=auth_user["user"].id,
filename="test.pdf",
content_type="application/pdf",
size_bytes=len(file_bytes),
storage_backend="minio",
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.get(
f"/api/documents/{doc_id}/content",
headers=auth_user["headers"],
)
assert resp.status_code == 200
assert resp.content == file_bytes
assert resp.headers["content-type"].startswith("application/pdf")
assert "inline" in resp.headers.get("content-disposition", "")
@pytest.mark.xfail(strict=False)
async def test_content_stream_206_range(async_client, auth_user):
async def test_content_stream_206_range(async_client, auth_user, db_session, monkeypatch):
"""GET /api/documents/{id}/content with Range header returns 206 and Content-Range header."""
pytest.xfail("not implemented yet")
import uuid as _uuid
from unittest.mock import AsyncMock
from db.models import Document
from storage.minio_backend import MinIOBackend
file_bytes = b"0123456789ABCDEF" # 16 bytes
monkeypatch.setattr(MinIOBackend, "get_object", AsyncMock(return_value=file_bytes), raising=False)
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=auth_user["user"].id,
filename="test.pdf",
content_type="application/pdf",
size_bytes=len(file_bytes),
storage_backend="minio",
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.get(
f"/api/documents/{doc_id}/content",
headers={**auth_user["headers"], "Range": "bytes=0-7"},
)
assert resp.status_code == 206
assert resp.content == b"01234567"
assert "content-range" in resp.headers
assert resp.headers["content-range"] == "bytes 0-7/16"
@pytest.mark.xfail(strict=False)
async def test_content_stream_admin_403(async_client, admin_user):
async def test_content_stream_admin_403(async_client, admin_user, db_session, monkeypatch):
"""GET /api/documents/{id}/content with admin JWT returns 403."""
pytest.xfail("not implemented yet")
import uuid as _uuid
from unittest.mock import AsyncMock
from db.models import Document
from storage.minio_backend import MinIOBackend
file_bytes = b"admin should not see this"
monkeypatch.setattr(MinIOBackend, "get_object", AsyncMock(return_value=file_bytes), raising=False)
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=admin_user["user"].id,
filename="test.pdf",
content_type="application/pdf",
size_bytes=len(file_bytes),
storage_backend="minio",
status="uploaded",
object_key=f"{admin_user['user'].id}/{doc_id}/{_uuid.uuid4()}.pdf",
)
db_session.add(doc)
await db_session.commit()
resp = await async_client.get(
f"/api/documents/{doc_id}/content",
headers=admin_user["headers"],
)
assert resp.status_code == 403
@pytest.mark.xfail(strict=False)
async def test_content_stream_no_presigned_url(async_client, auth_user):
"""GET /api/documents/{id}/content response body does not contain any presigned URL token."""
pytest.xfail("not implemented yet")
async def test_content_stream_no_presigned_url(async_client, auth_user, db_session, monkeypatch):
"""GET /api/documents/{id}/content response does not call presigned_get_url."""
import uuid as _uuid
from unittest.mock import AsyncMock, MagicMock
from db.models import Document
from storage.minio_backend import MinIOBackend
file_bytes = b"document content"
get_object_mock = AsyncMock(return_value=file_bytes)
presigned_mock = AsyncMock(return_value="http://minio/presigned?X-Amz-Signature=FAKE")
monkeypatch.setattr(MinIOBackend, "get_object", get_object_mock, raising=False)
monkeypatch.setattr(MinIOBackend, "presigned_get_url", presigned_mock, raising=False)
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=auth_user["user"].id,
filename="test.pdf",
content_type="application/pdf",
size_bytes=len(file_bytes),
storage_backend="minio",
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.get(
f"/api/documents/{doc_id}/content",
headers=auth_user["headers"],
)
assert resp.status_code == 200
# presigned_get_url must NEVER be called
presigned_mock.assert_not_called()
# get_object must be called (direct fetch)
get_object_mock.assert_called_once()
async def test_content_stream_share_recipient_200(async_client, auth_user, admin_user, db_session, monkeypatch):
"""Share recipient can access document content via GET /api/documents/{id}/content."""
import uuid as _uuid
from unittest.mock import AsyncMock
from db.models import Document, Share
from storage.minio_backend import MinIOBackend
file_bytes = b"shared document content"
monkeypatch.setattr(MinIOBackend, "get_object", AsyncMock(return_value=file_bytes), raising=False)
# Create a regular user as recipient (use auth_user as recipient, admin_user as owner)
# But we need two regular users; use auth_user as owner and create a second regular user
import uuid as _uuid2
from db.models import User, Quota
from services.auth import hash_password, create_access_token
recipient_id = _uuid2.uuid4()
recipient = User(
id=recipient_id,
handle=f"recipient_{recipient_id.hex[:8]}",
email=f"recipient_{recipient_id.hex[:8]}@example.com",
password_hash=hash_password("Testpassword123!"),
role="user",
is_active=True,
password_must_change=False,
)
recipient_quota = Quota(user_id=recipient_id, limit_bytes=104857600, used_bytes=0)
db_session.add(recipient)
db_session.add(recipient_quota)
await db_session.flush()
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=auth_user["user"].id,
filename="shared.pdf",
content_type="application/pdf",
size_bytes=len(file_bytes),
storage_backend="minio",
status="uploaded",
object_key=f"{auth_user['user'].id}/{doc_id}/{_uuid.uuid4()}.pdf",
)
db_session.add(doc)
await db_session.flush()
share = Share(
document_id=doc_id,
owner_id=auth_user["user"].id,
recipient_id=recipient_id,
permission="view",
)
db_session.add(share)
await db_session.commit()
recipient_token = create_access_token(str(recipient_id), "user")
recipient_headers = {"Authorization": f"Bearer {recipient_token}"}
resp = await async_client.get(
f"/api/documents/{doc_id}/content",
headers=recipient_headers,
)
assert resp.status_code == 200
assert resp.content == file_bytes
async def test_content_stream_not_found(async_client, auth_user):
"""GET /api/documents/{id}/content returns 404 for unknown document ID."""
import uuid as _uuid
resp = await async_client.get(
f"/api/documents/{_uuid.uuid4()}/content",
headers=auth_user["headers"],
)
assert resp.status_code == 404
async def test_content_stream_invalid_id(async_client, auth_user):
"""GET /api/documents/{id}/content returns 404 for invalid UUID."""
resp = await async_client.get(
"/api/documents/not-a-uuid/content",
headers=auth_user["headers"],
)
assert resp.status_code == 404
async def test_parse_range_416(async_client, auth_user, db_session, monkeypatch):
"""GET /api/documents/{id}/content with invalid Range returns 416."""
import uuid as _uuid
from unittest.mock import AsyncMock
from db.models import Document
from storage.minio_backend import MinIOBackend
file_bytes = b"short"
monkeypatch.setattr(MinIOBackend, "get_object", AsyncMock(return_value=file_bytes), raising=False)
doc_id = _uuid.uuid4()
doc = Document(
id=doc_id,
user_id=auth_user["user"].id,
filename="test.pdf",
content_type="application/pdf",
size_bytes=len(file_bytes),
storage_backend="minio",
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.get(
f"/api/documents/{doc_id}/content",
headers={**auth_user["headers"], "Range": "bytes=100-200"},
)
assert resp.status_code == 416