diff --git a/docs/api-reference.md b/docs/api-reference.md index f5bb297..c1a0095 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -978,6 +978,10 @@ Body: Response: `200` + `MessageRead` +Errors: + +- `403` - not message author or message is older than 7 days. + ### DELETE `/api/v1/messages/{message_id}?for_all=false` Auth required. diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index 6a14cc0..f2d517f 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -14,7 +14,7 @@ Legend: 5. Chat List - `DONE` (all/pinned/archive/sort/unread) 6. Chat Types - `DONE` (private/group/channel) 7. Chat Creation - `DONE` (private/group/channel) -8. Messages (base) - `DONE` (send/read/edit/delete/delete for all; group UI shows sender names over bubbles + sender avatars on incoming message clusters) +8. Messages (base) - `DONE` (send/read/edit/delete/delete for all; 7-day edit window enforced and covered by integration tests; group UI shows sender names over bubbles + sender avatars on incoming message clusters) 9. Message Types - `PARTIAL` (text/photo/video/docs/audio/voice/circle; GIF/stickers via dedicated system missing) 10. Reply/Quote/Threads - `PARTIAL` (reply + quote-like UI + thread panel with nested replies, no dedicated full thread navigation yet) 11. Forwarding - `DONE` (single + bulk + without author) diff --git a/tests/test_chat_message_flow.py b/tests/test_chat_message_flow.py index b860845..625944d 100644 --- a/tests/test_chat_message_flow.py +++ b/tests/test_chat_message_flow.py @@ -1,7 +1,10 @@ +from datetime import datetime, timedelta, timezone + from sqlalchemy import select from app.auth.models import EmailVerificationToken from app.chats.models import ChatType +from app.messages.models import Message async def _create_verified_user(client, db_session, email: str, username: str, password: str) -> dict: @@ -61,6 +64,43 @@ async def test_private_chat_message_lifecycle(client, db_session): assert delete_message_response.status_code == 204 +async def test_edit_message_older_than_7_days_is_forbidden(client, db_session): + u1 = await _create_verified_user(client, db_session, "edit_older_u1@example.com", "edit_older_u1", "strongpass123") + u2 = await _create_verified_user(client, db_session, "edit_older_u2@example.com", "edit_older_u2", "strongpass123") + + me_u2 = await client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {u2['access_token']}"}) + u2_id = me_u2.json()["id"] + + create_chat_response = await client.post( + "/api/v1/chats", + headers={"Authorization": f"Bearer {u1['access_token']}"}, + json={"type": ChatType.PRIVATE.value, "title": None, "member_ids": [u2_id]}, + ) + assert create_chat_response.status_code == 200 + chat_id = create_chat_response.json()["id"] + + send_message_response = await client.post( + "/api/v1/messages", + headers={"Authorization": f"Bearer {u1['access_token']}"}, + json={"chat_id": chat_id, "type": "text", "text": "original"}, + ) + assert send_message_response.status_code == 201 + message_id = send_message_response.json()["id"] + + message_row = await db_session.execute(select(Message).where(Message.id == message_id)) + message = message_row.scalar_one() + message.created_at = datetime.now(timezone.utc) - timedelta(days=8) + await db_session.commit() + + edit_message_response = await client.put( + f"/api/v1/messages/{message_id}", + headers={"Authorization": f"Bearer {u1['access_token']}"}, + json={"text": "edited text"}, + ) + assert edit_message_response.status_code == 403 + assert "7 days" in edit_message_response.json().get("detail", "") + + async def test_private_chat_respects_contacts_only_policy(client, db_session): u1 = await _create_verified_user(client, db_session, "pm_u1@example.com", "pm_user_one", "strongpass123") u2 = await _create_verified_user(client, db_session, "pm_u2@example.com", "pm_user_two", "strongpass123")