diff --git a/backend/app/routers/rooms.py b/backend/app/routers/rooms.py index bd64762..96b3f53 100644 --- a/backend/app/routers/rooms.py +++ b/backend/app/routers/rooms.py @@ -113,15 +113,23 @@ async def create_room_endpoint( @router.post("/dm", response_model=RoomRead, status_code=201) async def start_dm_endpoint( data: StartDmRequest, + request: Request, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): try: - return await find_or_create_dm(db, current_user.id, data.other_user_id) + room = await find_or_create_dm(db, current_user.id, data.other_user_id) except CannotDmSelfError: raise HTTPException(status_code=400, detail="Cannot start a DM with yourself") except TargetUserNotFoundError: raise HTTPException(status_code=404, detail="No user with that ID") + # Same signal add_member sends -- without it, the other participant's + # already-open client has no way to know this DM exists until they + # reload: GET /rooms/mine is only fetched once at app mount. Sent + # unconditionally (not just on genuine creation) since re-finding an + # existing DM and refreshing their room list again is harmless. + await broadcast_room_added(request.app.state.broadcaster, data.other_user_id, room) + return room @router.get("", response_model=list[RoomListItem]) diff --git a/backend/tests/test_broadcast.py b/backend/tests/test_broadcast.py index cb9a3f6..be73a1a 100644 --- a/backend/tests/test_broadcast.py +++ b/backend/tests/test_broadcast.py @@ -146,6 +146,28 @@ def test_add_member_notifies_target_user_via_websocket(ws_client_factory, monkey assert received == {"type": "room_added", "room_id": room["id"]} +def test_start_dm_notifies_other_participant_via_websocket(ws_client_factory): + # A production report: bob had no idea a DM existed until he reloaded -- + # find_or_create_dm was creating the room/membership correctly but never + # sending this signal, unlike every other "you're now in a room" path + # (add_member, above). Same shape as that test: bob is only ever + # "connected," never "joined," proving the signal alone is what tells + # his client the room exists at all. + instance1 = ws_client_factory() + instance2 = ws_client_factory() + + alice = _register_ws(instance1, _unique("alice")) + bob = _register_ws(instance2, _unique("bob")) + + with instance2.websocket_connect("/ws/chat") as bob_ws: + resp = instance1.post("/api/rooms/dm", json={"other_user_id": bob["id"]}) + assert resp.status_code == 201, resp.text + room = resp.json() + + received = bob_ws.receive_json() + assert received == {"type": "room_added", "room_id": room["id"]} + + def test_profile_update_notifies_room_members_via_websocket(ws_client_factory, monkeypatch): # Only reaches clients that have the room's own channel joined -- # exactly the case where a stale avatar/display name would actually be