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:
+242
-14
@@ -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, db_session, monkeypatch):
|
||||||
async def test_content_stream_200(async_client, auth_user):
|
|
||||||
"""GET /api/documents/{id}/content returns 200 with correct Content-Type and Content-Disposition: inline."""
|
"""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, db_session, monkeypatch):
|
||||||
async def test_content_stream_206_range(async_client, auth_user):
|
|
||||||
"""GET /api/documents/{id}/content with Range header returns 206 and Content-Range header."""
|
"""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, db_session, monkeypatch):
|
||||||
async def test_content_stream_admin_403(async_client, admin_user):
|
|
||||||
"""GET /api/documents/{id}/content with admin JWT returns 403."""
|
"""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, db_session, monkeypatch):
|
||||||
async def test_content_stream_no_presigned_url(async_client, auth_user):
|
"""GET /api/documents/{id}/content response does not call presigned_get_url."""
|
||||||
"""GET /api/documents/{id}/content response body does not contain any presigned URL token."""
|
import uuid as _uuid
|
||||||
pytest.xfail("not implemented yet")
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user