feat(6.1-01): replace 7 xfail stubs with real share tests (SHARE-01..05)
- Remove all 7 @pytest.mark.xfail decorators and pytest.xfail() calls - Remove unused 'import os' - Add module-level pytestmark = pytest.mark.asyncio - Add _make_doc() helper: creates uploaded Document row via ORM - test_share_success: POST grants share, recipient sees doc in /received - test_share_handle_not_found: unknown handle returns 404 - test_shared_with_me: metadata fields present, extracted_text absent (T-04-04-03) - test_share_no_quota_impact: recipient used_bytes stays 0 (T-04-04-04) - test_revoke_share: DELETE 204, doc gone from recipient /received - test_share_revoke_wrong_owner_404: IDOR protection, 404 not 403 (T-04-04-02) - test_share_duplicate: second share of same doc+recipient returns 409 - All 7 tests verified passing in Docker (pytest 9.0.3)
This commit is contained in:
+216
-29
@@ -1,15 +1,41 @@
|
|||||||
"""
|
"""
|
||||||
Share API tests — Wave 0 xfail stubs for Phase 4.
|
Share API tests — Phase 6.1, Plan 06.1-01.
|
||||||
|
|
||||||
All tests in this file are xfail stubs. They will be implemented in Plan 04-05.
|
Promotes all 7 xfail stubs to real tests covering SHARE-01 through SHARE-05.
|
||||||
The stubs ensure pytest collects them and keeps CI green before implementation
|
|
||||||
code exists.
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import uuid as _uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helper: create an uploaded Document row directly via ORM
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def _make_doc(db_session, owner_user) -> str:
|
||||||
|
"""Create an uploaded Document row owned by owner_user and return its str UUID."""
|
||||||
|
from db.models import Document
|
||||||
|
|
||||||
|
doc_id = _uuid.uuid4()
|
||||||
|
doc = Document(
|
||||||
|
id=doc_id,
|
||||||
|
user_id=owner_user["user"].id,
|
||||||
|
filename="test.txt",
|
||||||
|
content_type="text/plain",
|
||||||
|
size_bytes=1000,
|
||||||
|
storage_backend="minio",
|
||||||
|
status="uploaded",
|
||||||
|
object_key=f"{owner_user['user'].id}/{doc_id}/{_uuid.uuid4()}.txt",
|
||||||
|
)
|
||||||
|
db_session.add(doc)
|
||||||
|
await db_session.commit()
|
||||||
|
return str(doc_id)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -17,16 +43,48 @@ import pytest
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(strict=False)
|
async def test_share_success(async_client, auth_user, second_auth_user, db_session):
|
||||||
async def test_share_success(async_client, auth_user):
|
|
||||||
"""POST /api/shares grants share; recipient can see doc via GET /api/shares/received."""
|
"""POST /api/shares grants share; recipient can see doc via GET /api/shares/received."""
|
||||||
pytest.xfail("not implemented yet")
|
doc_id = await _make_doc(db_session, auth_user)
|
||||||
|
|
||||||
|
resp = await async_client.post(
|
||||||
|
"/api/shares",
|
||||||
|
json={
|
||||||
|
"document_id": doc_id,
|
||||||
|
"recipient_handle": second_auth_user["user"].handle,
|
||||||
|
},
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert "id" in body
|
||||||
|
assert body["document_id"] == doc_id
|
||||||
|
assert body["recipient_id"] == str(second_auth_user["user"].id)
|
||||||
|
|
||||||
|
# Recipient can see the document in their received list
|
||||||
|
received = await async_client.get(
|
||||||
|
"/api/shares/received",
|
||||||
|
headers=second_auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert received.status_code == 200
|
||||||
|
items = received.json()["items"]
|
||||||
|
doc_ids = [item["id"] for item in items]
|
||||||
|
assert doc_id in doc_ids
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(strict=False)
|
async def test_share_handle_not_found(async_client, auth_user, db_session):
|
||||||
async def test_share_handle_not_found(async_client, auth_user):
|
|
||||||
"""POST /api/shares with unknown handle returns 404."""
|
"""POST /api/shares with unknown handle returns 404."""
|
||||||
pytest.xfail("not implemented yet")
|
doc_id = await _make_doc(db_session, auth_user)
|
||||||
|
|
||||||
|
resp = await async_client.post(
|
||||||
|
"/api/shares",
|
||||||
|
json={
|
||||||
|
"document_id": doc_id,
|
||||||
|
"recipient_handle": "nonexistent_handle_xyz",
|
||||||
|
},
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -34,10 +92,47 @@ async def test_share_handle_not_found(async_client, auth_user):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(strict=False)
|
async def test_shared_with_me(async_client, auth_user, second_auth_user, db_session):
|
||||||
async def test_shared_with_me(async_client, auth_user):
|
"""GET /api/shares/received lists docs shared with current user (T-04-04-03)."""
|
||||||
"""GET /api/shares/received lists docs shared with current user."""
|
doc_id = await _make_doc(db_session, auth_user)
|
||||||
pytest.xfail("not implemented yet")
|
|
||||||
|
# Grant share
|
||||||
|
share_resp = await async_client.post(
|
||||||
|
"/api/shares",
|
||||||
|
json={
|
||||||
|
"document_id": doc_id,
|
||||||
|
"recipient_handle": second_auth_user["user"].handle,
|
||||||
|
},
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert share_resp.status_code == 201
|
||||||
|
|
||||||
|
# Recipient fetches shared-with-me
|
||||||
|
received = await async_client.get(
|
||||||
|
"/api/shares/received",
|
||||||
|
headers=second_auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert received.status_code == 200
|
||||||
|
items = received.json()["items"]
|
||||||
|
assert len(items) >= 1
|
||||||
|
|
||||||
|
# Find the specific item
|
||||||
|
matching = [item for item in items if item["id"] == doc_id]
|
||||||
|
assert len(matching) == 1, f"doc {doc_id} not in received items: {items}"
|
||||||
|
item = matching[0]
|
||||||
|
|
||||||
|
# Required metadata fields present
|
||||||
|
for field in ("id", "filename", "content_type", "size_bytes", "created_at", "owner_handle"):
|
||||||
|
assert field in item, f"Expected field '{field}' in item"
|
||||||
|
|
||||||
|
# T-04-04-03: extracted_text must NEVER be in any item
|
||||||
|
for received_item in items:
|
||||||
|
assert "extracted_text" not in received_item, (
|
||||||
|
"extracted_text must not be returned in shared-with-me responses (T-04-04-03)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# owner_handle should match the sharer
|
||||||
|
assert item["owner_handle"] == auth_user["user"].handle
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -45,10 +140,31 @@ async def test_shared_with_me(async_client, auth_user):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(strict=False)
|
async def test_share_no_quota_impact(async_client, auth_user, second_auth_user, db_session):
|
||||||
async def test_share_no_quota_impact(async_client, auth_user):
|
"""Share does not increment recipient's quota used_bytes (T-04-04-04)."""
|
||||||
"""Share does not increment recipient's quota used_bytes."""
|
doc_id = await _make_doc(db_session, auth_user)
|
||||||
pytest.xfail("not implemented yet")
|
|
||||||
|
# Grant share
|
||||||
|
share_resp = await async_client.post(
|
||||||
|
"/api/shares",
|
||||||
|
json={
|
||||||
|
"document_id": doc_id,
|
||||||
|
"recipient_handle": second_auth_user["user"].handle,
|
||||||
|
},
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert share_resp.status_code == 201
|
||||||
|
|
||||||
|
# Check recipient quota — must still be 0 used_bytes
|
||||||
|
quota_resp = await async_client.get(
|
||||||
|
"/api/auth/me/quota",
|
||||||
|
headers=second_auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert quota_resp.status_code == 200
|
||||||
|
quota = quota_resp.json()
|
||||||
|
assert quota["used_bytes"] == 0, (
|
||||||
|
f"Recipient quota used_bytes should be 0 after share, got {quota['used_bytes']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -56,16 +172,65 @@ async def test_share_no_quota_impact(async_client, auth_user):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(strict=False)
|
async def test_revoke_share(async_client, auth_user, second_auth_user, db_session):
|
||||||
async def test_revoke_share(async_client, auth_user):
|
|
||||||
"""DELETE /api/shares/{id} removes share; GET /api/shares/received no longer lists the doc."""
|
"""DELETE /api/shares/{id} removes share; GET /api/shares/received no longer lists the doc."""
|
||||||
pytest.xfail("not implemented yet")
|
doc_id = await _make_doc(db_session, auth_user)
|
||||||
|
|
||||||
|
# Grant share
|
||||||
|
share_resp = await async_client.post(
|
||||||
|
"/api/shares",
|
||||||
|
json={
|
||||||
|
"document_id": doc_id,
|
||||||
|
"recipient_handle": second_auth_user["user"].handle,
|
||||||
|
},
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert share_resp.status_code == 201
|
||||||
|
share_id = share_resp.json()["id"]
|
||||||
|
|
||||||
|
# Revoke share (owner revokes)
|
||||||
|
revoke_resp = await async_client.delete(
|
||||||
|
f"/api/shares/{share_id}",
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert revoke_resp.status_code == 204
|
||||||
|
|
||||||
|
# Recipient no longer sees the document in received
|
||||||
|
received = await async_client.get(
|
||||||
|
"/api/shares/received",
|
||||||
|
headers=second_auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert received.status_code == 200
|
||||||
|
items = received.json()["items"]
|
||||||
|
doc_ids = [item["id"] for item in items]
|
||||||
|
assert doc_id not in doc_ids, f"doc {doc_id} should have been revoked but still appears"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(strict=False)
|
async def test_share_revoke_wrong_owner_404(async_client, auth_user, second_auth_user, db_session):
|
||||||
async def test_share_revoke_wrong_owner_404(async_client, auth_user):
|
"""DELETE /api/shares/{id} by non-owner returns 404 (IDOR protection — T-04-04-02)."""
|
||||||
"""DELETE /api/shares/{id} by non-owner returns 404."""
|
doc_id = await _make_doc(db_session, auth_user)
|
||||||
pytest.xfail("not implemented yet")
|
|
||||||
|
# Grant share
|
||||||
|
share_resp = await async_client.post(
|
||||||
|
"/api/shares",
|
||||||
|
json={
|
||||||
|
"document_id": doc_id,
|
||||||
|
"recipient_handle": second_auth_user["user"].handle,
|
||||||
|
},
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert share_resp.status_code == 201
|
||||||
|
share_id = share_resp.json()["id"]
|
||||||
|
|
||||||
|
# Recipient attempts to revoke — must be rejected with 404 (not 403)
|
||||||
|
revoke_resp = await async_client.delete(
|
||||||
|
f"/api/shares/{share_id}",
|
||||||
|
headers=second_auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert revoke_resp.status_code == 404, (
|
||||||
|
f"Recipient should get 404 when attempting to revoke a share they don't own, "
|
||||||
|
f"got {revoke_resp.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -73,7 +238,29 @@ async def test_share_revoke_wrong_owner_404(async_client, auth_user):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(strict=False)
|
async def test_share_duplicate(async_client, auth_user, second_auth_user, db_session):
|
||||||
async def test_share_duplicate(async_client, auth_user):
|
|
||||||
"""POST /api/shares same doc+recipient twice returns 409."""
|
"""POST /api/shares same doc+recipient twice returns 409."""
|
||||||
pytest.xfail("not implemented yet")
|
doc_id = await _make_doc(db_session, auth_user)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"document_id": doc_id,
|
||||||
|
"recipient_handle": second_auth_user["user"].handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
# First share — should succeed
|
||||||
|
first_resp = await async_client.post(
|
||||||
|
"/api/shares",
|
||||||
|
json=payload,
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert first_resp.status_code == 201
|
||||||
|
|
||||||
|
# Second share with same doc+recipient — should return 409
|
||||||
|
second_resp = await async_client.post(
|
||||||
|
"/api/shares",
|
||||||
|
json=payload,
|
||||||
|
headers=auth_user["headers"],
|
||||||
|
)
|
||||||
|
assert second_resp.status_code == 409, (
|
||||||
|
f"Duplicate share should return 409, got {second_resp.status_code}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user