Private
Public Access
Mirrors the existing @-mention system's shape: a regex finds #roomname tokens, extract_referenced_room_ids validates them against rooms the *sender* actually belongs to (mirrors mentions' "must be a real member" rule -- referencing a private room the sender isn't in would otherwise leak its existence), and a MessageRoomReference join row is stored per match in create_message. No notification/unread layer, unlike mentions -- referencing a room has no "you were referenced" semantics. Rendering is the same markdown-link rewrite trick MessageContent.tsx already uses for mentions (#username -> [#username](mention:username)), but resolved against the *viewer's* own room list (threaded down from ChatShellPage's room state through ChatPane/MessageList) rather than the stored server-side reference -- a reference to a room the current viewer isn't in quietly renders as plain text instead of a link, same as an @mention of someone outside the room does. The href scheme renders a real react-router Link instead of mentions' inert span, since a room reference is meant to be navigable. mention_service.strip_code_spans (was _strip_code_spans) is now shared between both extraction paths rather than private to one module. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
120 lines
4.6 KiB
Python
120 lines
4.6 KiB
Python
import uuid
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.models import MessageRoomReference
|
|
from app.schemas.user import UserCreate
|
|
from app.services.auth_service import register_user
|
|
|
|
|
|
def _unique(prefix: str) -> str:
|
|
return f"{prefix}-{uuid.uuid4().hex[:8]}"
|
|
|
|
|
|
def _recv(ws) -> dict:
|
|
"""Reads the next frame, discarding member_updated presence-change
|
|
broadcasts -- same convention as test_mentions.py."""
|
|
while True:
|
|
msg = ws.receive_json()
|
|
if msg.get("type") != "member_updated":
|
|
return msg
|
|
|
|
|
|
def _register_ws(ws_client, username: str) -> dict:
|
|
async def _seed():
|
|
async with ws_client.session_factory() as session:
|
|
await register_user(
|
|
session,
|
|
UserCreate(username=username, email=f"{username}@example.com", password="password123"),
|
|
)
|
|
|
|
ws_client.portal.call(_seed)
|
|
resp = ws_client.post(
|
|
"/api/auth/login", json={"username_or_email": username, "password": "password123"}
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
return resp.json()
|
|
|
|
|
|
def _send_and_sync(ws, room_id: str, content: str) -> dict:
|
|
"""Sync barrier -- see test_mentions.py's identical helper. Proves the
|
|
message frame's full handling (including room-reference extraction) has
|
|
completed before the test checks anything."""
|
|
ws.send_json({"type": "message", "room_id": room_id, "content": content})
|
|
message = ws.receive_json()
|
|
ws.send_json({"type": "join", "room_id": room_id})
|
|
assert ws.receive_json()["type"] == "joined"
|
|
return message
|
|
|
|
|
|
def _referenced_room_ids(ws_client, message_id: str) -> set[str]:
|
|
async def _query():
|
|
async with ws_client.session_factory() as session:
|
|
result = await session.execute(
|
|
select(MessageRoomReference.room_id).where(
|
|
MessageRoomReference.message_id == uuid.UUID(message_id)
|
|
)
|
|
)
|
|
return {str(row[0]) for row in result.all()}
|
|
|
|
return ws_client.portal.call(_query)
|
|
|
|
|
|
def test_room_reference_to_own_room_is_stored(ws_client):
|
|
username = _unique("alice")
|
|
_register_ws(ws_client, username=username)
|
|
room_a = ws_client.post("/api/rooms", json={"name": _unique("general")}).json()
|
|
room_b = ws_client.post("/api/rooms", json={"name": _unique("random")}).json()
|
|
|
|
with ws_client.websocket_connect("/ws/chat") as ws:
|
|
ws.send_json({"type": "join", "room_id": room_a["id"]})
|
|
assert _recv(ws)["type"] == "joined"
|
|
message = _send_and_sync(ws, room_a["id"], f"see #{room_b['name']} for details")
|
|
|
|
assert _referenced_room_ids(ws_client, message["id"]) == {room_b["id"]}
|
|
|
|
|
|
def test_room_reference_to_room_sender_is_not_in_is_not_stored(ws_client_factory):
|
|
instance1 = ws_client_factory()
|
|
instance2 = ws_client_factory()
|
|
|
|
_register_ws(instance1, _unique("alice"))
|
|
room_a = instance1.post("/api/rooms", json={"name": _unique("general")}).json()
|
|
|
|
_register_ws(instance2, _unique("bob"))
|
|
other_room = instance2.post("/api/rooms", json={"name": _unique("bobs-room")}).json()
|
|
# Deliberately not joining other_room from instance1.
|
|
|
|
with instance1.websocket_connect("/ws/chat") as ws:
|
|
ws.send_json({"type": "join", "room_id": room_a["id"]})
|
|
assert _recv(ws)["type"] == "joined"
|
|
message = _send_and_sync(ws, room_a["id"], f"check #{other_room['name']} sometime")
|
|
|
|
assert _referenced_room_ids(instance1, message["id"]) == set()
|
|
|
|
|
|
def test_room_reference_inside_code_span_is_not_stored(ws_client):
|
|
username = _unique("alice")
|
|
_register_ws(ws_client, username=username)
|
|
room_a = ws_client.post("/api/rooms", json={"name": _unique("general")}).json()
|
|
room_b = ws_client.post("/api/rooms", json={"name": _unique("random")}).json()
|
|
|
|
with ws_client.websocket_connect("/ws/chat") as ws:
|
|
ws.send_json({"type": "join", "room_id": room_a["id"]})
|
|
assert _recv(ws)["type"] == "joined"
|
|
message = _send_and_sync(ws, room_a["id"], f"like this: `#{room_b['name']}`")
|
|
|
|
assert _referenced_room_ids(ws_client, message["id"]) == set()
|
|
|
|
|
|
def test_room_reference_without_space_does_not_render_as_heading():
|
|
# #47: confirms the char-class match itself is unaffected by CommonMark
|
|
# heading syntax concerns (a real heading needs "# text" with a space --
|
|
# this is a backend-extraction test, not a markdown-rendering one, but
|
|
# asserts the regex matches "#roomname" with no space, which is the
|
|
# whole point of the feature).
|
|
from app.services.room_reference_service import ROOM_REFERENCE_PATTERN
|
|
|
|
assert ROOM_REFERENCE_PATTERN.findall("check #general now") == ["general"]
|
|
assert ROOM_REFERENCE_PATTERN.findall("# general now") == []
|