chore: merge executor worktree (06.1-01 shares tests)

This commit is contained in:
curo1305
2026-05-30 23:16:38 +02:00
3 changed files with 349 additions and 29 deletions
+39
View File
@@ -226,6 +226,45 @@ async def auth_user(db_session: AsyncSession):
}
@pytest_asyncio.fixture
async def second_auth_user(db_session: AsyncSession):
"""Create a second regular user with a Quota row and return auth context.
Returns the same dict shape as auth_user but with a distinct handle prefix
("user2_") so sharing tests can have a sharer and a recipient in the same
test without handle collisions.
"""
import uuid as _uuid
from db.models import User, Quota
from services.auth import hash_password, create_access_token
user_id = _uuid.uuid4()
user = User(
id=user_id,
handle=f"user2_{user_id.hex[:8]}",
email=f"user2_{user_id.hex[:8]}@example.com",
password_hash=hash_password("Testpassword123!"),
role="user",
is_active=True,
password_must_change=False,
)
quota = Quota(
user_id=user_id,
limit_bytes=104857600, # 100 MB
used_bytes=0,
)
db_session.add(user)
db_session.add(quota)
await db_session.commit()
token = create_access_token(str(user_id), "user")
return {
"user": user,
"token": token,
"headers": {"Authorization": f"Bearer {token}"},
}
@pytest_asyncio.fixture
async def admin_user(db_session: AsyncSession):
"""Create an admin user with a Quota row and return auth context.
+216 -29
View File
@@ -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.
The stubs ensure pytest collects them and keeps CI green before implementation
code exists.
Promotes all 7 xfail stubs to real tests covering SHARE-01 through SHARE-05.
"""
from __future__ import annotations
import os
import uuid as _uuid
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):
async def test_share_success(async_client, auth_user, second_auth_user, db_session):
"""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):
async def test_share_handle_not_found(async_client, auth_user, db_session):
"""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):
"""GET /api/shares/received lists docs shared with current user."""
pytest.xfail("not implemented yet")
async def test_shared_with_me(async_client, auth_user, second_auth_user, db_session):
"""GET /api/shares/received lists docs shared with current user (T-04-04-03)."""
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
# 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):
"""Share does not increment recipient's quota used_bytes."""
pytest.xfail("not implemented yet")
async def test_share_no_quota_impact(async_client, auth_user, second_auth_user, db_session):
"""Share does not increment recipient's quota used_bytes (T-04-04-04)."""
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
# 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):
async def test_revoke_share(async_client, auth_user, second_auth_user, db_session):
"""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):
"""DELETE /api/shares/{id} by non-owner returns 404."""
pytest.xfail("not implemented yet")
async def test_share_revoke_wrong_owner_404(async_client, auth_user, second_auth_user, db_session):
"""DELETE /api/shares/{id} by non-owner returns 404 (IDOR protection — T-04-04-02)."""
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"]
# 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):
async def test_share_duplicate(async_client, auth_user, second_auth_user, db_session):
"""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}"
)