♻️(backend) extract notify_participant to util function
Move from lobby service to utils for reuse across services. Method is generic enough for utility status. Future: create dedicated LiveKit service to encapsulate all LiveKit-related utilities.
This commit is contained in:
committed by
aleb_the_flash
parent
85bde9633f
commit
17c486f7bf
@@ -1,6 +1,5 @@
|
|||||||
"""Lobby Service"""
|
"""Lobby Service"""
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -11,13 +10,6 @@ from uuid import UUID
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from livekit.api import ( # pylint: disable=E0611
|
|
||||||
ListRoomsRequest,
|
|
||||||
SendDataRequest,
|
|
||||||
TwirpError,
|
|
||||||
)
|
|
||||||
|
|
||||||
from core import models, utils
|
from core import models, utils
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -46,10 +38,6 @@ class LobbyParticipantNotFound(LobbyError):
|
|||||||
"""Raised when participant is not found."""
|
"""Raised when participant is not found."""
|
||||||
|
|
||||||
|
|
||||||
class LobbyNotificationError(LobbyError):
|
|
||||||
"""Raised when LiveKit notification fails."""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LobbyParticipant:
|
class LobbyParticipant:
|
||||||
"""Participant in a lobby system."""
|
"""Participant in a lobby system."""
|
||||||
@@ -211,9 +199,6 @@ class LobbyService:
|
|||||||
|
|
||||||
Create a new participant entry in waiting status and notify room
|
Create a new participant entry in waiting status and notify room
|
||||||
participants of the new entry request.
|
participants of the new entry request.
|
||||||
|
|
||||||
Raises:
|
|
||||||
LobbyNotificationError: If room notification fails
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
color = utils.generate_color(participant_id)
|
color = utils.generate_color(participant_id)
|
||||||
@@ -226,10 +211,15 @@ class LobbyService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.notify_participants(room_id=room_id)
|
utils.notify_participants(
|
||||||
except LobbyNotificationError:
|
room_name=room_id,
|
||||||
|
notification_data={
|
||||||
|
"type": settings.LOBBY_NOTIFICATION_TYPE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except utils.NotificationError:
|
||||||
# If room not created yet, there is no participants to notify
|
# If room not created yet, there is no participants to notify
|
||||||
pass
|
logger.exception("Failed to notify room participants")
|
||||||
|
|
||||||
cache_key = self._get_cache_key(room_id, participant_id)
|
cache_key = self._get_cache_key(room_id, participant_id)
|
||||||
cache.set(
|
cache.set(
|
||||||
@@ -334,44 +324,6 @@ class LobbyService:
|
|||||||
participant.status = status
|
participant.status = status
|
||||||
cache.set(cache_key, participant.to_dict(), timeout=timeout)
|
cache.set(cache_key, participant.to_dict(), timeout=timeout)
|
||||||
|
|
||||||
@async_to_sync
|
|
||||||
async def notify_participants(self, room_id: UUID):
|
|
||||||
"""Notify room participants about a new waiting participant using LiveKit.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
LobbyNotificationError: If notification fails to send
|
|
||||||
"""
|
|
||||||
|
|
||||||
notification_data = {
|
|
||||||
"type": settings.LOBBY_NOTIFICATION_TYPE,
|
|
||||||
}
|
|
||||||
|
|
||||||
lkapi = utils.create_livekit_client()
|
|
||||||
|
|
||||||
try:
|
|
||||||
room_response = await lkapi.room.list_rooms(
|
|
||||||
ListRoomsRequest(
|
|
||||||
names=[str(room_id)],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if the room exists
|
|
||||||
if not room_response.rooms:
|
|
||||||
return
|
|
||||||
|
|
||||||
await lkapi.room.send_data(
|
|
||||||
SendDataRequest(
|
|
||||||
room=str(room_id),
|
|
||||||
data=json.dumps(notification_data).encode("utf-8"),
|
|
||||||
kind="RELIABLE",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except TwirpError as e:
|
|
||||||
logger.exception("Failed to notify room participants")
|
|
||||||
raise LobbyNotificationError("Failed to notify room participants") from e
|
|
||||||
finally:
|
|
||||||
await lkapi.aclose()
|
|
||||||
|
|
||||||
def clear_room_cache(self, room_id: UUID) -> None:
|
def clear_room_cache(self, room_id: UUID) -> None:
|
||||||
"""Clear all participant entries from the cache for a specific room."""
|
"""Clear all participant entries from the cache for a specific room."""
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def test_request_entry_anonymous(settings):
|
|||||||
assert not lobby_keys
|
assert not lobby_keys
|
||||||
|
|
||||||
with (
|
with (
|
||||||
mock.patch.object(LobbyService, "notify_participants", return_value=None),
|
mock.patch.object(utils, "notify_participants", return_value=None),
|
||||||
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
|
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
|
||||||
):
|
):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
@@ -86,7 +86,7 @@ def test_request_entry_authenticated_user(settings):
|
|||||||
assert not lobby_keys
|
assert not lobby_keys
|
||||||
|
|
||||||
with (
|
with (
|
||||||
mock.patch.object(LobbyService, "notify_participants", return_value=None),
|
mock.patch.object(utils, "notify_participants", return_value=None),
|
||||||
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
|
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
|
||||||
):
|
):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
@@ -156,7 +156,7 @@ def test_request_entry_with_existing_participants(settings):
|
|||||||
|
|
||||||
# Mock external service calls to isolate the test
|
# Mock external service calls to isolate the test
|
||||||
with (
|
with (
|
||||||
mock.patch.object(LobbyService, "notify_participants", return_value=None),
|
mock.patch.object(utils, "notify_participants", return_value=None),
|
||||||
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
|
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
|
||||||
):
|
):
|
||||||
# Make request as a new anonymous user
|
# Make request as a new anonymous user
|
||||||
@@ -205,7 +205,7 @@ def test_request_entry_public_room(settings):
|
|||||||
assert not lobby_keys
|
assert not lobby_keys
|
||||||
|
|
||||||
with (
|
with (
|
||||||
mock.patch.object(LobbyService, "notify_participants", return_value=None),
|
mock.patch.object(utils, "notify_participants", return_value=None),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
LobbyService, "_get_or_create_participant_id", return_value="123"
|
LobbyService, "_get_or_create_participant_id", return_value="123"
|
||||||
),
|
),
|
||||||
@@ -255,7 +255,7 @@ def test_request_entry_authenticated_user_public_room(settings):
|
|||||||
assert not lobby_keys
|
assert not lobby_keys
|
||||||
|
|
||||||
with (
|
with (
|
||||||
mock.patch.object(LobbyService, "notify_participants", return_value=None),
|
mock.patch.object(utils, "notify_participants", return_value=None),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
LobbyService,
|
LobbyService,
|
||||||
"_get_or_create_participant_id",
|
"_get_or_create_participant_id",
|
||||||
@@ -315,7 +315,7 @@ def test_request_entry_waiting_participant_public_room(settings):
|
|||||||
client.cookies.load({"mocked-cookie": "2f7f162fe7d1421b90e702bfbfbf8def"})
|
client.cookies.load({"mocked-cookie": "2f7f162fe7d1421b90e702bfbfbf8def"})
|
||||||
|
|
||||||
with (
|
with (
|
||||||
mock.patch.object(LobbyService, "notify_participants", return_value=None),
|
mock.patch.object(utils, "notify_participants", return_value=None),
|
||||||
mock.patch.object(
|
mock.patch.object(
|
||||||
utils, "generate_livekit_config", return_value={"token": "test-token"}
|
utils, "generate_livekit_config", return_value={"token": "test-token"}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Test lobby service.
|
|||||||
# pylint: disable=W0621,W0613, W0212, R0913
|
# pylint: disable=W0621,W0613, W0212, R0913
|
||||||
# ruff: noqa: PLR0913
|
# ruff: noqa: PLR0913
|
||||||
|
|
||||||
import json
|
|
||||||
import uuid
|
import uuid
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@@ -14,18 +13,17 @@ from django.core.cache import cache
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from livekit.api import TwirpError
|
|
||||||
|
|
||||||
from core.factories import RoomFactory
|
from core.factories import RoomFactory
|
||||||
from core.models import RoomAccessLevel
|
from core.models import RoomAccessLevel
|
||||||
from core.services.lobby import (
|
from core.services.lobby import (
|
||||||
LobbyNotificationError,
|
|
||||||
LobbyParticipant,
|
LobbyParticipant,
|
||||||
LobbyParticipantNotFound,
|
LobbyParticipantNotFound,
|
||||||
LobbyParticipantParsingError,
|
LobbyParticipantParsingError,
|
||||||
LobbyParticipantStatus,
|
LobbyParticipantStatus,
|
||||||
LobbyService,
|
LobbyService,
|
||||||
)
|
)
|
||||||
|
from core.utils import NotificationError
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
@@ -414,7 +412,7 @@ def test_refresh_waiting_status(mock_cache, lobby_service, participant_id):
|
|||||||
# pylint: disable=R0917
|
# pylint: disable=R0917
|
||||||
@mock.patch("core.services.lobby.cache")
|
@mock.patch("core.services.lobby.cache")
|
||||||
@mock.patch("core.utils.generate_color")
|
@mock.patch("core.utils.generate_color")
|
||||||
@mock.patch("core.services.lobby.LobbyService.notify_participants")
|
@mock.patch("core.utils.notify_participants")
|
||||||
def test_enter_success(
|
def test_enter_success(
|
||||||
mock_notify,
|
mock_notify,
|
||||||
mock_generate_color,
|
mock_generate_color,
|
||||||
@@ -443,13 +441,15 @@ def test_enter_success(
|
|||||||
participant.to_dict(),
|
participant.to_dict(),
|
||||||
timeout=settings.LOBBY_WAITING_TIMEOUT,
|
timeout=settings.LOBBY_WAITING_TIMEOUT,
|
||||||
)
|
)
|
||||||
mock_notify.assert_called_once_with(room_id=room.id)
|
mock_notify.assert_called_once_with(
|
||||||
|
room_name=room.id, notification_data={"type": "participantWaiting"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=R0917
|
# pylint: disable=R0917
|
||||||
@mock.patch("core.services.lobby.cache")
|
@mock.patch("core.services.lobby.cache")
|
||||||
@mock.patch("core.utils.generate_color")
|
@mock.patch("core.utils.generate_color")
|
||||||
@mock.patch("core.services.lobby.LobbyService.notify_participants")
|
@mock.patch("core.utils.notify_participants")
|
||||||
def test_enter_with_notification_error(
|
def test_enter_with_notification_error(
|
||||||
mock_notify,
|
mock_notify,
|
||||||
mock_generate_color,
|
mock_generate_color,
|
||||||
@@ -460,7 +460,7 @@ def test_enter_with_notification_error(
|
|||||||
):
|
):
|
||||||
"""Test participant entry with notification error."""
|
"""Test participant entry with notification error."""
|
||||||
mock_generate_color.return_value = "#123456"
|
mock_generate_color.return_value = "#123456"
|
||||||
mock_notify.side_effect = LobbyNotificationError("Error notifying")
|
mock_notify.side_effect = NotificationError("Error notifying")
|
||||||
lobby_service._get_cache_key = mock.Mock(return_value="mocked_cache_key")
|
lobby_service._get_cache_key = mock.Mock(return_value="mocked_cache_key")
|
||||||
|
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||||
@@ -776,116 +776,6 @@ def test_update_participant_status_success(mock_cache, lobby_service, participan
|
|||||||
lobby_service._get_cache_key.assert_called_once_with(room.id, participant_id)
|
lobby_service._get_cache_key.assert_called_once_with(room.id, participant_id)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.utils.create_livekit_client")
|
|
||||||
def test_notify_participants_success_no_room(mock_create_livekit_client, lobby_service):
|
|
||||||
"""Test the notify_participants method when the LiveKit room doesn't exist yet."""
|
|
||||||
|
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
|
||||||
|
|
||||||
# Set up the mock LiveKitAPI and its behavior
|
|
||||||
mock_api_instance = mock.Mock()
|
|
||||||
mock_api_instance.room = mock.Mock()
|
|
||||||
mock_api_instance.room.send_data = mock.AsyncMock()
|
|
||||||
|
|
||||||
# Create a proper response object with an empty rooms list
|
|
||||||
class MockResponse:
|
|
||||||
"""LiveKit API response mock with empty rooms list."""
|
|
||||||
|
|
||||||
rooms = []
|
|
||||||
|
|
||||||
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
|
|
||||||
mock_api_instance.aclose = mock.AsyncMock()
|
|
||||||
mock_create_livekit_client.return_value = mock_api_instance
|
|
||||||
|
|
||||||
# Act
|
|
||||||
lobby_service.notify_participants(room.id)
|
|
||||||
|
|
||||||
# Verify that the service checked for existing rooms
|
|
||||||
mock_api_instance.room.list_rooms.assert_called_once()
|
|
||||||
|
|
||||||
# Verify the send_data method was not called since no room exists
|
|
||||||
mock_api_instance.room.send_data.assert_not_called()
|
|
||||||
|
|
||||||
# Verify the connection was properly closed
|
|
||||||
mock_api_instance.aclose.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.utils.create_livekit_client")
|
|
||||||
def test_notify_participants_success(mock_create_livekit_client, lobby_service):
|
|
||||||
"""Test successful participant notification."""
|
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
|
||||||
# Set up the mock LiveKitAPI and its behavior
|
|
||||||
mock_api_instance = mock.Mock()
|
|
||||||
mock_api_instance.room = mock.Mock()
|
|
||||||
mock_api_instance.room.send_data = mock.AsyncMock()
|
|
||||||
|
|
||||||
class MockResponse:
|
|
||||||
"""LiveKit API response mock with non-empty rooms list."""
|
|
||||||
|
|
||||||
rooms = ["room-1"]
|
|
||||||
|
|
||||||
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
|
|
||||||
|
|
||||||
mock_api_instance.aclose = mock.AsyncMock()
|
|
||||||
mock_create_livekit_client.return_value = mock_api_instance
|
|
||||||
|
|
||||||
# Call the function
|
|
||||||
lobby_service.notify_participants(room.id)
|
|
||||||
|
|
||||||
# Verify that the service checked for existing rooms
|
|
||||||
mock_api_instance.room.list_rooms.assert_called_once()
|
|
||||||
|
|
||||||
# Verify the send_data method was called
|
|
||||||
mock_api_instance.room.send_data.assert_called_once()
|
|
||||||
send_data_request = mock_api_instance.room.send_data.call_args[0][0]
|
|
||||||
assert send_data_request.room == str(room.id)
|
|
||||||
assert (
|
|
||||||
json.loads(send_data_request.data.decode("utf-8"))["type"]
|
|
||||||
== settings.LOBBY_NOTIFICATION_TYPE
|
|
||||||
)
|
|
||||||
assert send_data_request.kind == 0 # RELIABLE mode in Livekit protocol
|
|
||||||
|
|
||||||
# Verify aclose was called
|
|
||||||
mock_api_instance.aclose.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.utils.create_livekit_client")
|
|
||||||
def test_notify_participants_error(mock_create_livekit_client, lobby_service):
|
|
||||||
"""Test participant notification with API error."""
|
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
|
||||||
# Set up the mock LiveKitAPI and its behavior
|
|
||||||
mock_api_instance = mock.Mock()
|
|
||||||
mock_api_instance.room = mock.Mock()
|
|
||||||
mock_api_instance.room.send_data = mock.AsyncMock(
|
|
||||||
side_effect=TwirpError(msg="test error", code=123, status=123)
|
|
||||||
)
|
|
||||||
|
|
||||||
class MockResponse:
|
|
||||||
"""LiveKit API response mock with non-empty rooms list."""
|
|
||||||
|
|
||||||
rooms = ["room-1"]
|
|
||||||
|
|
||||||
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
|
|
||||||
|
|
||||||
mock_api_instance.aclose = mock.AsyncMock()
|
|
||||||
mock_create_livekit_client.return_value = mock_api_instance
|
|
||||||
|
|
||||||
# Call the function and expect an exception
|
|
||||||
with pytest.raises(
|
|
||||||
LobbyNotificationError, match="Failed to notify room participants"
|
|
||||||
):
|
|
||||||
lobby_service.notify_participants(room.id)
|
|
||||||
|
|
||||||
# Verify that the service checked for existing rooms
|
|
||||||
mock_api_instance.room.list_rooms.assert_called_once()
|
|
||||||
|
|
||||||
# Verify send_data was called
|
|
||||||
mock_api_instance.room.send_data.assert_called_once()
|
|
||||||
|
|
||||||
# Verify aclose was still called after the exception
|
|
||||||
mock_api_instance.aclose.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
def test_clear_room_cache(settings, lobby_service):
|
def test_clear_room_cache(settings, lobby_service):
|
||||||
"""Test clearing room cache actually removes entries from cache."""
|
"""Test clearing room cache actually removes entries from cache."""
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
Test utils functions
|
Test utils functions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from core.utils import create_livekit_client
|
import pytest
|
||||||
|
from livekit.api import TwirpError
|
||||||
|
|
||||||
|
from core.utils import NotificationError, create_livekit_client, notify_participants
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("asyncio.get_running_loop")
|
@mock.patch("asyncio.get_running_loop")
|
||||||
@@ -60,3 +64,105 @@ def test_create_livekit_client_custom_configuration(
|
|||||||
create_livekit_client(custom_configuration)
|
create_livekit_client(custom_configuration)
|
||||||
|
|
||||||
mock_livekit_api.assert_called_once_with(**custom_configuration, session=None)
|
mock_livekit_api.assert_called_once_with(**custom_configuration, session=None)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
|
def test_notify_participants_error(mock_create_livekit_client):
|
||||||
|
"""Test participant notification with API error."""
|
||||||
|
|
||||||
|
# Set up the mock LiveKitAPI and its behavior
|
||||||
|
mock_api_instance = mock.Mock()
|
||||||
|
mock_api_instance.room = mock.Mock()
|
||||||
|
mock_api_instance.room.send_data = mock.AsyncMock(
|
||||||
|
side_effect=TwirpError(msg="test error", code=123, status=123)
|
||||||
|
)
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
"""LiveKit API response mock with non-empty rooms list."""
|
||||||
|
|
||||||
|
rooms = ["room-1"]
|
||||||
|
|
||||||
|
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
|
||||||
|
|
||||||
|
mock_api_instance.aclose = mock.AsyncMock()
|
||||||
|
mock_create_livekit_client.return_value = mock_api_instance
|
||||||
|
|
||||||
|
# Call the function and expect an exception
|
||||||
|
with pytest.raises(NotificationError, match="Failed to notify room participants"):
|
||||||
|
notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})
|
||||||
|
|
||||||
|
# Verify that the service checked for existing rooms
|
||||||
|
mock_api_instance.room.list_rooms.assert_called_once()
|
||||||
|
|
||||||
|
# Verify send_data was called
|
||||||
|
mock_api_instance.room.send_data.assert_called_once()
|
||||||
|
|
||||||
|
# Verify aclose was still called after the exception
|
||||||
|
mock_api_instance.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
|
def test_notify_participants_success_no_room(mock_create_livekit_client):
|
||||||
|
"""Test the notify_participants function when the LiveKit room doesn't exist."""
|
||||||
|
|
||||||
|
# Set up the mock LiveKitAPI and its behavior
|
||||||
|
mock_api_instance = mock.Mock()
|
||||||
|
mock_api_instance.room = mock.Mock()
|
||||||
|
mock_api_instance.room.send_data = mock.AsyncMock()
|
||||||
|
|
||||||
|
# Create a proper response object with an empty rooms list
|
||||||
|
class MockResponse:
|
||||||
|
"""LiveKit API response mock with empty rooms list."""
|
||||||
|
|
||||||
|
rooms = []
|
||||||
|
|
||||||
|
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
|
||||||
|
mock_api_instance.aclose = mock.AsyncMock()
|
||||||
|
mock_create_livekit_client.return_value = mock_api_instance
|
||||||
|
|
||||||
|
notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})
|
||||||
|
|
||||||
|
# Verify that the service checked for existing rooms
|
||||||
|
mock_api_instance.room.list_rooms.assert_called_once()
|
||||||
|
|
||||||
|
# Verify the send_data method was not called since no room exists
|
||||||
|
mock_api_instance.room.send_data.assert_not_called()
|
||||||
|
|
||||||
|
# Verify the connection was properly closed
|
||||||
|
mock_api_instance.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("core.utils.create_livekit_client")
|
||||||
|
def test_notify_participants_success(mock_create_livekit_client):
|
||||||
|
"""Test successful participant notification."""
|
||||||
|
|
||||||
|
# Set up the mock LiveKitAPI and its behavior
|
||||||
|
mock_api_instance = mock.Mock()
|
||||||
|
mock_api_instance.room = mock.Mock()
|
||||||
|
mock_api_instance.room.send_data = mock.AsyncMock()
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
"""LiveKit API response mock with non-empty rooms list."""
|
||||||
|
|
||||||
|
rooms = ["room-1"]
|
||||||
|
|
||||||
|
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
|
||||||
|
|
||||||
|
mock_api_instance.aclose = mock.AsyncMock()
|
||||||
|
mock_create_livekit_client.return_value = mock_api_instance
|
||||||
|
|
||||||
|
# Call the function
|
||||||
|
notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})
|
||||||
|
|
||||||
|
# Verify that the service checked for existing rooms
|
||||||
|
mock_api_instance.room.list_rooms.assert_called_once()
|
||||||
|
|
||||||
|
# Verify the send_data method was called
|
||||||
|
mock_api_instance.room.send_data.assert_called_once()
|
||||||
|
send_data_request = mock_api_instance.room.send_data.call_args[0][0]
|
||||||
|
assert send_data_request.room == "room-number-1"
|
||||||
|
assert json.loads(send_data_request.data.decode("utf-8")) == {"foo": "foo"}
|
||||||
|
assert send_data_request.kind == 0 # RELIABLE mode in Livekit protocol
|
||||||
|
|
||||||
|
# Verify aclose was called
|
||||||
|
mock_api_instance.aclose.assert_called_once()
|
||||||
|
|||||||
@@ -15,7 +15,15 @@ from django.core.files.storage import default_storage
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import botocore
|
import botocore
|
||||||
from livekit.api import AccessToken, LiveKitAPI, VideoGrants
|
from asgiref.sync import async_to_sync
|
||||||
|
from livekit.api import ( # pylint: disable=E0611
|
||||||
|
AccessToken,
|
||||||
|
ListRoomsRequest,
|
||||||
|
LiveKitAPI,
|
||||||
|
SendDataRequest,
|
||||||
|
TwirpError,
|
||||||
|
VideoGrants,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_color(identity: str) -> str:
|
def generate_color(identity: str) -> str:
|
||||||
@@ -158,3 +166,37 @@ def create_livekit_client(custom_configuration=None):
|
|||||||
configuration = custom_configuration or settings.LIVEKIT_CONFIGURATION
|
configuration = custom_configuration or settings.LIVEKIT_CONFIGURATION
|
||||||
|
|
||||||
return LiveKitAPI(session=custom_session, **configuration)
|
return LiveKitAPI(session=custom_session, **configuration)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationError(Exception):
|
||||||
|
"""Notification delivery to room participants fails."""
|
||||||
|
|
||||||
|
|
||||||
|
@async_to_sync
|
||||||
|
async def notify_participants(room_name: str, notification_data: dict):
|
||||||
|
"""Send notification data to all participants in a LiveKit room."""
|
||||||
|
|
||||||
|
lkapi = create_livekit_client()
|
||||||
|
|
||||||
|
try:
|
||||||
|
room_response = await lkapi.room.list_rooms(
|
||||||
|
ListRoomsRequest(
|
||||||
|
names=[room_name],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the room exists
|
||||||
|
if not room_response.rooms:
|
||||||
|
return
|
||||||
|
|
||||||
|
await lkapi.room.send_data(
|
||||||
|
SendDataRequest(
|
||||||
|
room=room_name,
|
||||||
|
data=json.dumps(notification_data).encode("utf-8"),
|
||||||
|
kind="RELIABLE",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except TwirpError as e:
|
||||||
|
raise NotificationError("Failed to notify room participants") from e
|
||||||
|
finally:
|
||||||
|
await lkapi.aclose()
|
||||||
|
|||||||
Reference in New Issue
Block a user