mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-09 17:05:56 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 069b3d5b95 | |||
| e0ab7f191f | |||
| 455b315dbb | |||
| 3089b03062 | |||
| 60febb3b57 | |||
| bf76ab1ddf | |||
| 7565ede0a7 | |||
| 1a15e9f44e | |||
| 7838d8acfe | |||
| 3bb388b937 | |||
| e1cc8105db | |||
| 7844dfcc12 | |||
| ef71003721 | |||
| f74d23c57e | |||
| acedb21045 | |||
| 67e7d382e3 | |||
| 164ac8d948 |
@@ -8,6 +8,20 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 📈(frontend) include LiveKit SIDs in the connection analytics event
|
||||||
|
- 🔇(backend) silence expected 401 warnings on /me
|
||||||
|
- 🔇(backend) silence noisy request summary info logs
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🐛(backend) acknowledge unknown LiveKit webhook events instead of 422
|
||||||
|
- 🔒️(backend) enforce display name setting on rename API
|
||||||
|
- 🔒️(backend) reject inactive users in resource server backend
|
||||||
|
|
||||||
|
## [1.31.0] - 2026-09-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(frontend) add 1080p sending resolution option #1660
|
- ✨(frontend) add 1080p sending resolution option #1660
|
||||||
@@ -15,6 +29,7 @@ and this project adheres to
|
|||||||
- ✨(backend) update a room's attributes from the external API
|
- ✨(backend) update a room's attributes from the external API
|
||||||
- 🔊(backend) log request duration in Gunicorn workers
|
- 🔊(backend) log request duration in Gunicorn workers
|
||||||
- 📈(frontend) track missing lobby participant on accept/reject
|
- 📈(frontend) track missing lobby participant on accept/reject
|
||||||
|
- ✨(backend) sort waiting participants by their arrival time
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.29.0"
|
version = "1.31.0"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livekit-agents==1.6.7",
|
"livekit-agents==1.6.7",
|
||||||
|
|||||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.29.0"
|
version = "1.31.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
|||||||
@@ -909,6 +909,15 @@ class RoomViewSet(
|
|||||||
"""Rename the current participant in the room."""
|
"""Rename the current participant in the room."""
|
||||||
room = self.get_object()
|
room = self.get_object()
|
||||||
|
|
||||||
|
if (
|
||||||
|
not settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME
|
||||||
|
and request.user.is_authenticated
|
||||||
|
):
|
||||||
|
return drf_response.Response(
|
||||||
|
{"error": "Authenticated participants cannot edit their display name"},
|
||||||
|
status=drf_status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
serializer = serializers.RenameParticipantSerializer(data=request.data)
|
serializer = serializers.RenameParticipantSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,10 @@ class ResourceServerBackend(LaSuiteBackend):
|
|||||||
if user is None and settings.OIDC_CREATE_USER:
|
if user is None and settings.OIDC_CREATE_USER:
|
||||||
user = self.create_user(sub)
|
user = self.create_user(sub)
|
||||||
|
|
||||||
|
if user is not None and not user.is_active:
|
||||||
|
logger.warning("Inactive user attempted authentication: %s", user.pk)
|
||||||
|
raise SuspiciousOperation("User account is disabled.")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def create_user(self, sub):
|
def create_user(self, sub):
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""Logging filters for the core application."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class SilenceExpected401(logging.Filter):
|
||||||
|
"""Drop the expected 401 from anonymous hits on the /me endpoint.
|
||||||
|
|
||||||
|
The frontend probes `/users/me/` to check authentication; a 401 for
|
||||||
|
anonymous users is normal, not a warning worth logging.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def filter(self, record):
|
||||||
|
"""Return False for a 401 on a silenced path, True otherwise."""
|
||||||
|
if getattr(record, "status_code", None) != 401:
|
||||||
|
return True
|
||||||
|
|
||||||
|
request = getattr(record, "request", None)
|
||||||
|
path = getattr(request, "path", None)
|
||||||
|
if not path:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return path not in settings.LOGGING_SILENCED_401_PATHS
|
||||||
@@ -52,12 +52,6 @@ class InvalidPayloadError(LiveKitWebhookError):
|
|||||||
status_code = 400
|
status_code = 400
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedEventTypeError(LiveKitWebhookError):
|
|
||||||
"""Unsupported event type."""
|
|
||||||
|
|
||||||
status_code = 422
|
|
||||||
|
|
||||||
|
|
||||||
class ActionFailedError(LiveKitWebhookError):
|
class ActionFailedError(LiveKitWebhookError):
|
||||||
"""Webhook action fails to process or complete."""
|
"""Webhook action fails to process or complete."""
|
||||||
|
|
||||||
@@ -74,6 +68,7 @@ class LiveKitWebhookEventType(Enum):
|
|||||||
# Participant events
|
# Participant events
|
||||||
PARTICIPANT_JOINED = "participant_joined"
|
PARTICIPANT_JOINED = "participant_joined"
|
||||||
PARTICIPANT_LEFT = "participant_left"
|
PARTICIPANT_LEFT = "participant_left"
|
||||||
|
PARTICIPANT_CONNECTION_ABORTED = "participant_connection_aborted"
|
||||||
|
|
||||||
# Track events
|
# Track events
|
||||||
TRACK_PUBLISHED = "track_published"
|
TRACK_PUBLISHED = "track_published"
|
||||||
@@ -153,10 +148,13 @@ class LiveKitEventsService:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
webhook_type = LiveKitWebhookEventType(data.event)
|
webhook_type = LiveKitWebhookEventType(data.event)
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
raise UnsupportedEventTypeError(
|
logger.warning(
|
||||||
f"Unknown webhook type: {data.event}"
|
"Ignoring unknown LiveKit webhook event type '%s' for room '%s'",
|
||||||
) from e
|
data.event,
|
||||||
|
room_name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Handle according to received webhook type
|
# Handle according to received webhook type
|
||||||
handler = self._webhook_handlers.get(webhook_type.value)
|
handler = self._webhook_handlers.get(webhook_type.value)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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 django.utils import timezone
|
||||||
|
|
||||||
from core import models, utils
|
from core import models, utils
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class LobbyParticipant:
|
|||||||
username: str
|
username: str
|
||||||
color: str
|
color: str
|
||||||
id: str
|
id: str
|
||||||
|
entered_at: str
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, str]:
|
def to_dict(self) -> Dict[str, str]:
|
||||||
"""Serialize the participant object to a dict representation."""
|
"""Serialize the participant object to a dict representation."""
|
||||||
@@ -54,6 +56,7 @@ class LobbyParticipant:
|
|||||||
"username": self.username,
|
"username": self.username,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"color": self.color,
|
"color": self.color,
|
||||||
|
"entered_at": self.entered_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -68,6 +71,7 @@ class LobbyParticipant:
|
|||||||
username=data["username"],
|
username=data["username"],
|
||||||
id=data["id"],
|
id=data["id"],
|
||||||
color=data["color"],
|
color=data["color"],
|
||||||
|
entered_at=data["entered_at"],
|
||||||
)
|
)
|
||||||
except (KeyError, ValueError) as e:
|
except (KeyError, ValueError) as e:
|
||||||
logger.exception("Error creating Participant from dict:")
|
logger.exception("Error creating Participant from dict:")
|
||||||
@@ -203,6 +207,7 @@ class LobbyService:
|
|||||||
username=username,
|
username=username,
|
||||||
id=participant_id,
|
id=participant_id,
|
||||||
color=utils.generate_color(participant_id),
|
color=utils.generate_color(participant_id),
|
||||||
|
entered_at=timezone.now().isoformat(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
participant.status = LobbyParticipantStatus.ACCEPTED
|
participant.status = LobbyParticipantStatus.ACCEPTED
|
||||||
@@ -264,6 +269,7 @@ class LobbyService:
|
|||||||
username=username,
|
username=username,
|
||||||
id=participant_id,
|
id=participant_id,
|
||||||
color=color,
|
color=color,
|
||||||
|
entered_at=timezone.now().isoformat(),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -338,6 +344,8 @@ class LobbyService:
|
|||||||
|
|
||||||
self._index_remove(room_id, *dead_ids)
|
self._index_remove(room_id, *dead_ids)
|
||||||
|
|
||||||
|
waiting_participants.sort(key=lambda p: p["entered_at"], reverse=True)
|
||||||
|
|
||||||
return tuple(waiting_participants)
|
return tuple(waiting_participants)
|
||||||
|
|
||||||
def handle_participant_entry(
|
def handle_participant_entry(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from unittest import mock
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from freezegun import freeze_time
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from ... import utils
|
from ... import utils
|
||||||
@@ -24,6 +25,7 @@ pytestmark = pytest.mark.django_db
|
|||||||
# Tests for request_entry endpoint
|
# Tests for request_entry endpoint
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-01-01 10:00:00")
|
||||||
def test_request_entry_anonymous(settings):
|
def test_request_entry_anonymous(settings):
|
||||||
"""Anonymous users should be allowed to request entry to a room."""
|
"""Anonymous users should be allowed to request entry to a room."""
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||||
@@ -59,6 +61,7 @@ def test_request_entry_anonymous(settings):
|
|||||||
"username": "test_user",
|
"username": "test_user",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "mocked-color",
|
"color": "mocked-color",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
"livekit": None,
|
"livekit": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +74,7 @@ def test_request_entry_anonymous(settings):
|
|||||||
assert participant_data.get("username") == "test_user"
|
assert participant_data.get("username") == "test_user"
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-01-01 10:00:00")
|
||||||
def test_request_entry_authenticated_user(settings):
|
def test_request_entry_authenticated_user(settings):
|
||||||
"""Authenticated users should be allowed to request entry."""
|
"""Authenticated users should be allowed to request entry."""
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
|
||||||
@@ -108,6 +112,7 @@ def test_request_entry_authenticated_user(settings):
|
|||||||
"username": "test_user",
|
"username": "test_user",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "mocked-color",
|
"color": "mocked-color",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
"livekit": None,
|
"livekit": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +125,7 @@ def test_request_entry_authenticated_user(settings):
|
|||||||
assert participant_data.get("username") == "test_user"
|
assert participant_data.get("username") == "test_user"
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-01-01 10:00:00")
|
||||||
def test_request_entry_with_existing_participants(settings):
|
def test_request_entry_with_existing_participants(settings):
|
||||||
"""Anonymous users should be allowed to request entry to a room with existing participants."""
|
"""Anonymous users should be allowed to request entry to a room with existing participants."""
|
||||||
# Create a restricted access room
|
# Create a restricted access room
|
||||||
@@ -138,6 +144,7 @@ def test_request_entry_with_existing_participants(settings):
|
|||||||
"username": "user1",
|
"username": "user1",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
cache.set(
|
cache.set(
|
||||||
@@ -147,6 +154,7 @@ def test_request_entry_with_existing_participants(settings):
|
|||||||
"username": "user2",
|
"username": "user2",
|
||||||
"status": "accepted",
|
"status": "accepted",
|
||||||
"color": "#654321",
|
"color": "#654321",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -178,6 +186,7 @@ def test_request_entry_with_existing_participants(settings):
|
|||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": participant_id,
|
"id": participant_id,
|
||||||
"username": "test_user",
|
"username": "test_user",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "mocked-color",
|
"color": "mocked-color",
|
||||||
"livekit": None,
|
"livekit": None,
|
||||||
@@ -192,6 +201,7 @@ def test_request_entry_with_existing_participants(settings):
|
|||||||
assert participant_data.get("username") == "test_user"
|
assert participant_data.get("username") == "test_user"
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-01-01 10:00:00")
|
||||||
def test_request_entry_public_room(settings):
|
def test_request_entry_public_room(settings):
|
||||||
"""Entry requests to public rooms should return ACCEPTED status with LiveKit config."""
|
"""Entry requests to public rooms should return ACCEPTED status with LiveKit config."""
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||||
@@ -230,6 +240,7 @@ def test_request_entry_public_room(settings):
|
|||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": "123",
|
"id": "123",
|
||||||
"username": "test_user",
|
"username": "test_user",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
"status": "accepted",
|
"status": "accepted",
|
||||||
"color": "mocked-color",
|
"color": "mocked-color",
|
||||||
"livekit": {"token": "test-token"},
|
"livekit": {"token": "test-token"},
|
||||||
@@ -240,6 +251,7 @@ def test_request_entry_public_room(settings):
|
|||||||
assert not lobby_keys
|
assert not lobby_keys
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-01-01 10:00:00")
|
||||||
def test_request_entry_authenticated_user_public_room(settings):
|
def test_request_entry_authenticated_user_public_room(settings):
|
||||||
"""While authenticated, entry request to public rooms should get accepted."""
|
"""While authenticated, entry request to public rooms should get accepted."""
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||||
@@ -282,6 +294,7 @@ def test_request_entry_authenticated_user_public_room(settings):
|
|||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": "2f7f162f-e7d1-421b-90e7-02bfbfbf8def",
|
"id": "2f7f162f-e7d1-421b-90e7-02bfbfbf8def",
|
||||||
"username": "test_user",
|
"username": "test_user",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
"status": "accepted",
|
"status": "accepted",
|
||||||
"color": "mocked-color",
|
"color": "mocked-color",
|
||||||
"livekit": {"token": "test-token"},
|
"livekit": {"token": "test-token"},
|
||||||
@@ -292,6 +305,7 @@ def test_request_entry_authenticated_user_public_room(settings):
|
|||||||
assert not lobby_keys
|
assert not lobby_keys
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-01-01 10:00:00")
|
||||||
def test_request_entry_waiting_participant_public_room(settings):
|
def test_request_entry_waiting_participant_public_room(settings):
|
||||||
"""While waiting, entry request to public rooms should get accepted."""
|
"""While waiting, entry request to public rooms should get accepted."""
|
||||||
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
room = RoomFactory(access_level=RoomAccessLevel.PUBLIC)
|
||||||
@@ -308,6 +322,7 @@ def test_request_entry_waiting_participant_public_room(settings):
|
|||||||
"username": "user1",
|
"username": "user1",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -338,6 +353,7 @@ def test_request_entry_waiting_participant_public_room(settings):
|
|||||||
"username": "user1",
|
"username": "user1",
|
||||||
"status": "accepted",
|
"status": "accepted",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
"livekit": {"token": "test-token"},
|
"livekit": {"token": "test-token"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +459,7 @@ def test_allow_participant_to_enter_success(settings, allow_entry, updated_statu
|
|||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"username": "foo",
|
"username": "foo",
|
||||||
"color": "123",
|
"color": "123",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -578,6 +595,7 @@ def test_list_waiting_participants_success(settings):
|
|||||||
"username": "user1",
|
"username": "user1",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
cache.set(
|
cache.set(
|
||||||
@@ -587,6 +605,7 @@ def test_list_waiting_participants_success(settings):
|
|||||||
"username": "user2",
|
"username": "user2",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "#654321",
|
"color": "#654321",
|
||||||
|
"entered_at": "2025-01-01T10:05:00+00:00",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
lobby_service = LobbyService()
|
lobby_service = LobbyService()
|
||||||
@@ -597,21 +616,24 @@ def test_list_waiting_participants_success(settings):
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
participants = response.json().get("participants")
|
assert response.json() == {
|
||||||
assert sorted(participants, key=lambda p: p["id"]) == [
|
"participants": [
|
||||||
{
|
{
|
||||||
"id": "2f7f162f-e7d1-421b-90e7-02bfbfbf8def",
|
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
|
||||||
"username": "user1",
|
"username": "user2",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "#123456",
|
"color": "#654321",
|
||||||
},
|
"entered_at": "2025-01-01T10:05:00+00:00",
|
||||||
{
|
},
|
||||||
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
|
{
|
||||||
"username": "user2",
|
"id": "2f7f162f-e7d1-421b-90e7-02bfbfbf8def",
|
||||||
"status": "waiting",
|
"username": "user1",
|
||||||
"color": "#654321",
|
"status": "waiting",
|
||||||
},
|
"color": "#123456",
|
||||||
]
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_list_waiting_participants_empty(settings):
|
def test_list_waiting_participants_empty(settings):
|
||||||
|
|||||||
@@ -372,6 +372,67 @@ def test_rename_participant_unexpected_twirp_error(mock_livekit_client, room, to
|
|||||||
mock_livekit_client.aclose.assert_called_once()
|
mock_livekit_client.aclose.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", ["John Doe", "Admin", "Room Owner"])
|
||||||
|
def test_rename_participant_forbidden_when_display_name_edit_disabled(
|
||||||
|
mock_livekit_client, settings, room, token, name
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test rename is rejected for authenticated users when the self-hoster
|
||||||
|
disables AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME.
|
||||||
|
"""
|
||||||
|
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||||
|
response = client.post(
|
||||||
|
url, {"name": name}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
assert response.data == {
|
||||||
|
"error": "Authenticated participants cannot edit their display name"
|
||||||
|
}
|
||||||
|
mock_livekit_client.room.update_participant.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rename_participant_allowed_when_display_name_edit_enabled(
|
||||||
|
mock_livekit_client, settings, room, token
|
||||||
|
):
|
||||||
|
"""Test rename still works for authenticated users when the setting is enabled."""
|
||||||
|
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = True
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||||
|
response = client.post(
|
||||||
|
url, {"name": "John Doe"}, format="json", HTTP_AUTHORIZATION=f"Bearer {token}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
mock_livekit_client.room.update_participant.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rename_participant_anonymous_allowed_when_display_name_edit_disabled(
|
||||||
|
mock_livekit_client, settings, room, anonymous_token
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the setting only restricts authenticated users: anonymous participants
|
||||||
|
have no account name to fall back on and can still rename themselves.
|
||||||
|
"""
|
||||||
|
settings.AUTHENTICATED_PARTICIPANTS_CAN_EDIT_DISPLAY_NAME = False
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
url = reverse("rooms-rename", kwargs={"pk": room.id})
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
{"name": "Guest User"},
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"Bearer {anonymous_token}",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
mock_livekit_client.room.update_participant.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_rename_participant_success_anonymous(
|
def test_rename_participant_success_anonymous(
|
||||||
mock_livekit_client, room, anonymous_token
|
mock_livekit_client, room, anonymous_token
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def test_invalid_payload(client, auth_token, mock_livekit_config):
|
|||||||
|
|
||||||
|
|
||||||
def test_unknown_event_type(client, mock_livekit_config):
|
def test_unknown_event_type(client, mock_livekit_config):
|
||||||
"""Should return 422 for unknown event type."""
|
"""Should acknowledge (200) an unknown event type rather than reject it."""
|
||||||
event_data = json.dumps({"event": "unknown_event_type"})
|
event_data = json.dumps({"event": "unknown_event_type"})
|
||||||
|
|
||||||
# Generate auth token for this specific payload
|
# Generate auth token for this specific payload
|
||||||
@@ -112,10 +112,8 @@ def test_unknown_event_type(client, mock_livekit_config):
|
|||||||
HTTP_AUTHORIZATION=auth_token,
|
HTTP_AUTHORIZATION=auth_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 422
|
assert response.status_code == 200
|
||||||
assert response.json() == {
|
assert response.json() == {"status": "success"}
|
||||||
"status": "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(LiveKitEventsService, "_handle_room_finished")
|
@mock.patch.object(LiveKitEventsService, "_handle_room_finished")
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from core.services.livekit_events import (
|
|||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
InvalidPayloadError,
|
InvalidPayloadError,
|
||||||
LiveKitEventsService,
|
LiveKitEventsService,
|
||||||
UnsupportedEventTypeError,
|
|
||||||
api,
|
api,
|
||||||
)
|
)
|
||||||
from core.services.lobby import LobbyService
|
from core.services.lobby import LobbyService
|
||||||
@@ -665,22 +664,27 @@ def test_receive_missing_auth(service):
|
|||||||
|
|
||||||
|
|
||||||
@mock.patch.object(api.WebhookReceiver, "receive")
|
@mock.patch.object(api.WebhookReceiver, "receive")
|
||||||
def test_receive_unsupported_event(mock_receive, service):
|
def test_receive_unknown_event_is_acknowledged(mock_receive, service, caplog):
|
||||||
"""Should raise LiveKitWebhookError for unsupported events."""
|
"""Unknown event types are logged and ignored, not rejected.
|
||||||
|
|
||||||
|
LiveKit adds event types over time and does not retry 4xx responses, so
|
||||||
|
raising here would silently drop the event.
|
||||||
|
"""
|
||||||
mock_request = mock.MagicMock()
|
mock_request = mock.MagicMock()
|
||||||
mock_request.headers = {"Authorization": "test_token"}
|
mock_request.headers = {"Authorization": "test_token"}
|
||||||
mock_request.body = b"{}"
|
mock_request.body = b"{}"
|
||||||
|
|
||||||
# Mock returned data with unsupported event type
|
|
||||||
mock_data = mock.MagicMock()
|
mock_data = mock.MagicMock()
|
||||||
mock_data.room.name = str(uuid.uuid4())
|
mock_data.room.name = str(uuid.uuid4())
|
||||||
mock_data.event = "unsupported_event"
|
mock_data.event = "some_future_event"
|
||||||
mock_receive.return_value = mock_data
|
mock_receive.return_value = mock_data
|
||||||
|
|
||||||
with pytest.raises(
|
with caplog.at_level("WARNING", logger="core.services.livekit_events"):
|
||||||
UnsupportedEventTypeError, match="Unknown webhook type: unsupported_event"
|
service.receive(mock_request) # must not raise
|
||||||
):
|
|
||||||
service.receive(mock_request)
|
assert "Ignoring unknown LiveKit webhook event type 'some_future_event'" in (
|
||||||
|
caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(api.WebhookReceiver, "receive")
|
@mock.patch.object(api.WebhookReceiver, "receive")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from django.core.cache import cache
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from core.factories import RoomFactory, UserFactory, UserResourceAccessFactory
|
from core.factories import RoomFactory, UserFactory, UserResourceAccessFactory
|
||||||
from core.models import RoleChoices, RoomAccessLevel
|
from core.models import RoleChoices, RoomAccessLevel
|
||||||
@@ -55,6 +56,7 @@ def participant_dict():
|
|||||||
"username": "test-username",
|
"username": "test-username",
|
||||||
"id": "test-participant-id",
|
"id": "test-participant-id",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ def participant_data():
|
|||||||
username="test-username",
|
username="test-username",
|
||||||
id="test-participant-id",
|
id="test-participant-id",
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -77,6 +80,7 @@ def test_lobby_participant_to_dict(participant_data):
|
|||||||
assert result["username"] == "test-username"
|
assert result["username"] == "test-username"
|
||||||
assert result["id"] == "test-participant-id"
|
assert result["id"] == "test-participant-id"
|
||||||
assert result["color"] == "#123456"
|
assert result["color"] == "#123456"
|
||||||
|
assert result["entered_at"] == "2025-01-01T10:00:00+00:00"
|
||||||
|
|
||||||
|
|
||||||
def test_lobby_participant_from_dict_success(participant_dict):
|
def test_lobby_participant_from_dict_success(participant_dict):
|
||||||
@@ -87,6 +91,20 @@ def test_lobby_participant_from_dict_success(participant_dict):
|
|||||||
assert participant.username == "test-username"
|
assert participant.username == "test-username"
|
||||||
assert participant.id == "test-participant-id"
|
assert participant.id == "test-participant-id"
|
||||||
assert participant.color == "#123456"
|
assert participant.color == "#123456"
|
||||||
|
assert participant.entered_at == "2025-01-01T10:00:00+00:00"
|
||||||
|
|
||||||
|
|
||||||
|
def test_lobby_participant_from_dict_missing_entered_at():
|
||||||
|
"""`entered_at` is mandatory; data without it is rejected."""
|
||||||
|
data = {
|
||||||
|
"status": "waiting",
|
||||||
|
"username": "test-username",
|
||||||
|
"id": "test-participant-id",
|
||||||
|
"color": "#123456",
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(LobbyParticipantParsingError, match="Invalid participant data"):
|
||||||
|
LobbyParticipant.from_dict(data)
|
||||||
|
|
||||||
|
|
||||||
def test_lobby_participant_from_dict_default_status():
|
def test_lobby_participant_from_dict_default_status():
|
||||||
@@ -95,6 +113,7 @@ def test_lobby_participant_from_dict_default_status():
|
|||||||
"username": "test-username",
|
"username": "test-username",
|
||||||
"id": "test-participant-id",
|
"id": "test-participant-id",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
participant = LobbyParticipant.from_dict(data_without_status)
|
participant = LobbyParticipant.from_dict(data_without_status)
|
||||||
@@ -120,6 +139,7 @@ def test_lobby_participant_from_dict_invalid_status():
|
|||||||
"username": "test-username",
|
"username": "test-username",
|
||||||
"id": "test-participant-id",
|
"id": "test-participant-id",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
with pytest.raises(LobbyParticipantParsingError, match="Invalid participant data"):
|
with pytest.raises(LobbyParticipantParsingError, match="Invalid participant data"):
|
||||||
@@ -264,6 +284,7 @@ def test_request_entry_public_room(
|
|||||||
username=username,
|
username=username,
|
||||||
id=participant_id,
|
id=participant_id,
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
)
|
)
|
||||||
|
|
||||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||||
@@ -302,6 +323,7 @@ def test_request_entry_trusted_room(
|
|||||||
username=username,
|
username=username,
|
||||||
id=participant_id,
|
id=participant_id,
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
)
|
)
|
||||||
|
|
||||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||||
@@ -344,6 +366,7 @@ def test_request_entry_new_participant(
|
|||||||
username=username,
|
username=username,
|
||||||
id=participant_id,
|
id=participant_id,
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
)
|
)
|
||||||
mock_enter.return_value = participant_data
|
mock_enter.return_value = participant_data
|
||||||
|
|
||||||
@@ -371,6 +394,7 @@ def test_request_entry_waiting_participant(
|
|||||||
username=username,
|
username=username,
|
||||||
id=participant_id,
|
id=participant_id,
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
)
|
)
|
||||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||||
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
||||||
@@ -399,6 +423,7 @@ def test_request_entry_accepted_participant(
|
|||||||
username=username,
|
username=username,
|
||||||
id=participant_id,
|
id=participant_id,
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
)
|
)
|
||||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||||
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
||||||
@@ -439,6 +464,7 @@ def test_request_entry_participant_with_role(
|
|||||||
username=username,
|
username=username,
|
||||||
id=participant_id,
|
id=participant_id,
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
)
|
)
|
||||||
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
lobby_service._get_or_create_participant_id = mock.Mock(return_value=participant_id)
|
||||||
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
lobby_service._get_participant = mock.Mock(return_value=mocked_participant)
|
||||||
@@ -479,6 +505,7 @@ def test_refresh_waiting_status(mock_cache, lobby_service, participant_id):
|
|||||||
@mock.patch("core.utils.generate_color")
|
@mock.patch("core.utils.generate_color")
|
||||||
@mock.patch("core.utils.notify_participants")
|
@mock.patch("core.utils.notify_participants")
|
||||||
@mock.patch("core.services.lobby.LobbyService._index_add")
|
@mock.patch("core.services.lobby.LobbyService._index_add")
|
||||||
|
@freeze_time("2025-01-01 10:00:00")
|
||||||
def test_enter_success(
|
def test_enter_success(
|
||||||
mock_index_add,
|
mock_index_add,
|
||||||
mock_notify,
|
mock_notify,
|
||||||
@@ -500,6 +527,7 @@ def test_enter_success(
|
|||||||
assert participant.username == username
|
assert participant.username == username
|
||||||
assert participant.id == participant_id
|
assert participant.id == participant_id
|
||||||
assert participant.color == "#123456"
|
assert participant.color == "#123456"
|
||||||
|
assert participant.entered_at == "2025-01-01T10:00:00+00:00"
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -629,6 +657,7 @@ def test_list_waiting_participants_multiple(mock_cache, lobby_service):
|
|||||||
"username": "user1",
|
"username": "user1",
|
||||||
"id": "participant1",
|
"id": "participant1",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
participant2 = {
|
participant2 = {
|
||||||
@@ -636,6 +665,7 @@ def test_list_waiting_participants_multiple(mock_cache, lobby_service):
|
|||||||
"username": "user2",
|
"username": "user2",
|
||||||
"id": "participant2",
|
"id": "participant2",
|
||||||
"color": "#654321",
|
"color": "#654321",
|
||||||
|
"entered_at": "2025-01-01T10:05:00+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
lobby_service._index_members = mock.Mock(
|
lobby_service._index_members = mock.Mock(
|
||||||
@@ -651,9 +681,10 @@ def test_list_waiting_participants_multiple(mock_cache, lobby_service):
|
|||||||
|
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
|
|
||||||
# Verify both participants are in the result
|
# Most recent entry comes first
|
||||||
assert any(p["id"] == "participant1" and p["username"] == "user1" for p in result)
|
assert [p["id"] for p in result] == ["participant2", "participant1"]
|
||||||
assert any(p["id"] == "participant2" and p["username"] == "user2" for p in result)
|
assert result[0]["username"] == "user2"
|
||||||
|
assert result[1]["username"] == "user1"
|
||||||
|
|
||||||
# Verify all participants have waiting status
|
# Verify all participants have waiting status
|
||||||
assert all(p["status"] == "waiting" for p in result)
|
assert all(p["status"] == "waiting" for p in result)
|
||||||
@@ -689,6 +720,7 @@ def test_list_waiting_participants_partially_corrupted(mock_cache, lobby_service
|
|||||||
"username": "user2",
|
"username": "user2",
|
||||||
"id": "participant2",
|
"id": "participant2",
|
||||||
"color": "#654321",
|
"color": "#654321",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
corrupted_participant = {"invalid": "data"}
|
corrupted_participant = {"invalid": "data"}
|
||||||
@@ -729,12 +761,14 @@ def test_list_waiting_participants_non_waiting(mock_cache, lobby_service):
|
|||||||
"username": "user1",
|
"username": "user1",
|
||||||
"id": "participant1",
|
"id": "participant1",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
participant2 = {
|
participant2 = {
|
||||||
"status": "accepted",
|
"status": "accepted",
|
||||||
"username": "user2",
|
"username": "user2",
|
||||||
"id": "participant2",
|
"id": "participant2",
|
||||||
"color": "#654321",
|
"color": "#654321",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
lobby_service._index_members = mock.Mock(
|
lobby_service._index_members = mock.Mock(
|
||||||
@@ -832,6 +866,7 @@ def test_update_participant_status_success(mock_cache, lobby_service, participan
|
|||||||
"username": "test-username",
|
"username": "test-username",
|
||||||
"id": participant_id,
|
"id": participant_id,
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_cache.get.return_value = participant_dict
|
mock_cache.get.return_value = participant_dict
|
||||||
@@ -850,6 +885,7 @@ def test_update_participant_status_success(mock_cache, lobby_service, participan
|
|||||||
"username": "test-username",
|
"username": "test-username",
|
||||||
"id": participant_id,
|
"id": participant_id,
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
mock_cache.set.assert_called_once_with(
|
mock_cache.set.assert_called_once_with(
|
||||||
"mocked_cache_key", expected_data, timeout=60
|
"mocked_cache_key", expected_data, timeout=60
|
||||||
@@ -875,6 +911,7 @@ def test_clear_room_cache(settings, lobby_service):
|
|||||||
username="participant1",
|
username="participant1",
|
||||||
id="participant1",
|
id="participant1",
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
),
|
),
|
||||||
timeout=settings.LOBBY_WAITING_TIMEOUT,
|
timeout=settings.LOBBY_WAITING_TIMEOUT,
|
||||||
)
|
)
|
||||||
@@ -885,6 +922,7 @@ def test_clear_room_cache(settings, lobby_service):
|
|||||||
username="participant2",
|
username="participant2",
|
||||||
id="participant2",
|
id="participant2",
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
),
|
),
|
||||||
timeout=settings.LOBBY_ACCEPTED_TIMEOUT,
|
timeout=settings.LOBBY_ACCEPTED_TIMEOUT,
|
||||||
)
|
)
|
||||||
@@ -895,6 +933,7 @@ def test_clear_room_cache(settings, lobby_service):
|
|||||||
username="participant3",
|
username="participant3",
|
||||||
id="participant3",
|
id="participant3",
|
||||||
color="#123456",
|
color="#123456",
|
||||||
|
entered_at="2025-01-01T10:00:00+00:00",
|
||||||
),
|
),
|
||||||
timeout=settings.LOBBY_DENIED_TIMEOUT,
|
timeout=settings.LOBBY_DENIED_TIMEOUT,
|
||||||
)
|
)
|
||||||
@@ -930,6 +969,7 @@ def test_clear_participant_cache(lobby_service):
|
|||||||
"username": "test-username",
|
"username": "test-username",
|
||||||
"id": participant_id,
|
"id": participant_id,
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
}
|
}
|
||||||
cache.set(cache_key, participant_data, timeout=settings.LOBBY_WAITING_TIMEOUT)
|
cache.set(cache_key, participant_data, timeout=settings.LOBBY_WAITING_TIMEOUT)
|
||||||
lobby_service._index_add(room_id, participant_id)
|
lobby_service._index_add(room_id, participant_id)
|
||||||
@@ -1000,6 +1040,7 @@ def test_list_waiting_participants_prunes_stale_index_ids(settings, lobby_servic
|
|||||||
"username": "user1",
|
"username": "user1",
|
||||||
"status": "waiting",
|
"status": "waiting",
|
||||||
"color": "#123456",
|
"color": "#123456",
|
||||||
|
"entered_at": "2025-01-01T10:00:00+00:00",
|
||||||
},
|
},
|
||||||
timeout=100,
|
timeout=100,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""Tests for the external API ResourceServerBackend."""
|
||||||
|
|
||||||
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import responses
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core.external_api.authentication import ResourceServerBackend
|
||||||
|
from core.factories import UserFactory
|
||||||
|
from core.models import User
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def _payload(sub):
|
||||||
|
return {"sub": sub, "active": True, "scope": "lasuite_meet", "client_id": "app"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_resource_server_backend_get_or_create_user_active():
|
||||||
|
"""An existing active user matching the sub should be returned."""
|
||||||
|
|
||||||
|
user = UserFactory()
|
||||||
|
|
||||||
|
result = ResourceServerBackend().get_or_create_user(
|
||||||
|
access_token="token", id_token=None, payload=_payload(user.sub)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == user
|
||||||
|
|
||||||
|
|
||||||
|
def test_resource_server_backend_get_or_create_user_inactive():
|
||||||
|
"""An inactive user should be rejected even with a valid token."""
|
||||||
|
|
||||||
|
user = UserFactory(is_active=False)
|
||||||
|
|
||||||
|
with pytest.raises(SuspiciousOperation, match="User account is disabled."):
|
||||||
|
ResourceServerBackend().get_or_create_user(
|
||||||
|
access_token="token", id_token=None, payload=_payload(user.sub)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resource_server_backend_get_or_create_user_creates(settings):
|
||||||
|
"""An unknown sub should create an active user when OIDC_CREATE_USER is set."""
|
||||||
|
|
||||||
|
settings.OIDC_CREATE_USER = True
|
||||||
|
|
||||||
|
result = ResourceServerBackend().get_or_create_user(
|
||||||
|
access_token="token", id_token=None, payload=_payload("new-sub")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.sub == "new-sub"
|
||||||
|
assert result.is_active is True
|
||||||
|
assert User.objects.filter(sub="new-sub").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_resource_server_backend_get_or_create_user_no_creation(settings):
|
||||||
|
"""An unknown sub should return None when OIDC_CREATE_USER is unset."""
|
||||||
|
|
||||||
|
settings.OIDC_CREATE_USER = False
|
||||||
|
|
||||||
|
result = ResourceServerBackend().get_or_create_user(
|
||||||
|
access_token="token", id_token=None, payload=_payload("new-sub")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
assert not User.objects.filter(sub="new-sub").exists()
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_rooms_list_resource_server_inactive_user(settings):
|
||||||
|
"""End to end: a valid introspected token for an inactive user should get 401."""
|
||||||
|
|
||||||
|
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||||
|
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||||
|
|
||||||
|
user = UserFactory(is_active=False)
|
||||||
|
|
||||||
|
responses.add(
|
||||||
|
responses.POST,
|
||||||
|
"https://oidc.example.com/introspect",
|
||||||
|
json={
|
||||||
|
"iss": "https://oidc.example.com",
|
||||||
|
"active": True,
|
||||||
|
"sub": user.sub,
|
||||||
|
"scope": "openid lasuite_meet rooms:list",
|
||||||
|
"client_id": "app",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.credentials(HTTP_AUTHORIZATION="Bearer rs-token")
|
||||||
|
response = client.get("/external-api/v1.0/rooms/")
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert "login failed" in str(response.data).lower()
|
||||||
@@ -1110,6 +1110,12 @@ class Base(Configuration):
|
|||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LOGGING_SILENCED_401_PATHS = values.ListValue(
|
||||||
|
default=["/api/v1.0/users/me/"],
|
||||||
|
environ_name="LOGGING_SILENCED_401_PATHS",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
# We want to make it easy to log to console but by default we log production
|
# We want to make it easy to log to console but by default we log production
|
||||||
# to Sentry and don't want to log to console.
|
# to Sentry and don't want to log to console.
|
||||||
@@ -1122,10 +1128,16 @@ class Base(Configuration):
|
|||||||
"style": "{",
|
"style": "{",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"silence_expected_401": {
|
||||||
|
"()": "core.logging_filters.SilenceExpected401",
|
||||||
|
},
|
||||||
|
},
|
||||||
"handlers": {
|
"handlers": {
|
||||||
"console": {
|
"console": {
|
||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "simple",
|
"formatter": "simple",
|
||||||
|
"filters": ["silence_expected_401"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# Override root logger to send it to console
|
# Override root logger to send it to console
|
||||||
@@ -1136,6 +1148,13 @@ class Base(Configuration):
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
|
"request.summary": {
|
||||||
|
"level": values.Value(
|
||||||
|
"WARNING",
|
||||||
|
environ_name="LOGGING_LEVEL_REQUEST_SUMMARY",
|
||||||
|
environ_prefix="",
|
||||||
|
)
|
||||||
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"handlers": ["console"],
|
"handlers": ["console"],
|
||||||
"level": values.Value(
|
"level": values.Value(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.30.0"
|
version = "1.31.0"
|
||||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1187,7 +1187,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.30.0"
|
version = "1.31.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
||||||
"@fontsource-variable/lexend": "5.3.0",
|
"@fontsource-variable/lexend": "5.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type WaitingParticipant = {
|
|||||||
status: string
|
status: string
|
||||||
username: string
|
username: string
|
||||||
color: string
|
color: string
|
||||||
|
entered_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WaitingParticipantsResponse = {
|
export type WaitingParticipantsResponse = {
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ import {
|
|||||||
} from '../../participants/api/listWaitingParticipants'
|
} from '../../participants/api/listWaitingParticipants'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
import { reportError } from '@/features/analytics/telemetry'
|
||||||
|
|
||||||
|
const toTimestamp = (participant: WaitingParticipant): number =>
|
||||||
|
Date.parse(participant.entered_at)
|
||||||
|
|
||||||
|
export const sortWaitingParticipants = (
|
||||||
|
participants: WaitingParticipant[]
|
||||||
|
): WaitingParticipant[] =>
|
||||||
|
[...participants].sort((a, b) => toTimestamp(a) - toTimestamp(b))
|
||||||
|
|
||||||
export const useWaitingParticipants = () => {
|
export const useWaitingParticipants = () => {
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const roomId = roomData?.id || '' // FIXME - bad practice
|
const roomId = roomData?.id || '' // FIXME - bad practice
|
||||||
@@ -22,7 +30,10 @@ export const useWaitingParticipants = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const waitingParticipants = useMemo(
|
const waitingParticipants = useMemo(
|
||||||
() => (canManageLobby ? waitingData?.participants || [] : []),
|
() =>
|
||||||
|
canManageLobby
|
||||||
|
? sortWaitingParticipants(waitingData?.participants || [])
|
||||||
|
: [],
|
||||||
[waitingData, canManageLobby]
|
[waitingData, canManageLobby]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const LobbyProvider = () => {
|
|||||||
// 3. Rights regained.
|
// 3. Rights regained.
|
||||||
const prevCanManageLobby = usePrevious(canManageLobby)
|
const prevCanManageLobby = usePrevious(canManageLobby)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!prevCanManageLobby && canManageLobby && isConnected) {
|
if (prevCanManageLobby != canManageLobby && isConnected) {
|
||||||
fetchIfManager()
|
fetchIfManager()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const Card = styled('div', {
|
|||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
boxShadow: '',
|
boxShadow: '',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: '380px',
|
maxWidth: '410px',
|
||||||
minHeight: '196px',
|
minHeight: '196px',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -229,7 +229,7 @@ const ConfirmationMessage = ({ onNext }: { onNext: () => void }) => {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '380px',
|
maxWidth: '410px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
@@ -71,20 +71,30 @@ export const ConnectionObserver = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAnalyticsEnabled) return
|
if (!isAnalyticsEnabled) return
|
||||||
|
|
||||||
const handleConnection = () => {
|
const handleConnection = async () => {
|
||||||
// Preserve original connection timestamp across reconnections to measure
|
// Preserve original connection timestamp across reconnections to measure
|
||||||
// total session duration from first connect to final disconnect.
|
// total session duration from first connect to final disconnect.
|
||||||
if (connectionStartTimeRef.current != null) return
|
if (connectionStartTimeRef.current != null) return
|
||||||
connectionStartTimeRef.current = Date.now()
|
connectionStartTimeRef.current = Date.now()
|
||||||
void captureMediaEvent('connection-event', {})
|
const participantSid = room.localParticipant.sid
|
||||||
|
const roomSid = await room.getSid().catch(() => undefined)
|
||||||
|
void captureMediaEvent('connection-event', {
|
||||||
|
livekit_room_sid: roomSid,
|
||||||
|
livekit_participant_sid: participantSid,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReconnect = () => {
|
const handleReconnect = () => {
|
||||||
captureEvent('reconnect-event')
|
captureEvent('reconnect-event')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReconnected = () => {
|
const handleReconnected = async () => {
|
||||||
captureEvent('reconnected-event')
|
const participantSid = room.localParticipant.sid
|
||||||
|
const roomSid = await room.getSid().catch(() => undefined)
|
||||||
|
captureEvent('reconnected-event', {
|
||||||
|
livekit_room_sid: roomSid,
|
||||||
|
livekit_participant_sid: participantSid,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSignalingConnect = () => {
|
const handleSignalingConnect = () => {
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const Heading = styled('h1', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const buttonClass = css({
|
const buttonClass = css({
|
||||||
width: { base: '100%', xsm: 'auto' },
|
width: '100%',
|
||||||
|
flex: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
enum DisconnectReasonKey {
|
enum DisconnectReasonKey {
|
||||||
@@ -64,7 +65,7 @@ const FeedbackRoute = () => {
|
|||||||
<Heading>{t(`feedback.heading.${reasonKey || 'normal'}`)}</Heading>
|
<Heading>{t(`feedback.heading.${reasonKey || 'normal'}`)}</Heading>
|
||||||
<Stack
|
<Stack
|
||||||
direction={{ base: 'column', xsm: 'row' }}
|
direction={{ base: 'column', xsm: 'row' }}
|
||||||
width={{ base: '100%', xsm: 'auto' }}
|
width="100%"
|
||||||
maxWidth="410px"
|
maxWidth="410px"
|
||||||
>
|
>
|
||||||
{showBackButton && (
|
{showBackButton && (
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@html-to/text-cli": "0.6.1",
|
"@html-to/text-cli": "0.6.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"description": "An util to generate html and text django's templates from mjml templates",
|
"description": "An util to generate html and text django's templates from mjml templates",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./library",
|
"./library",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.30.0",
|
"version": "1.31.0",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "summary"
|
name = "summary"
|
||||||
version = "1.30.0"
|
version = "1.31.0"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.105.0",
|
"fastapi[standard]>=0.105.0",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1507,7 +1507,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "summary"
|
name = "summary"
|
||||||
version = "1.30.0"
|
version = "1.31.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "celery" },
|
{ name = "celery" },
|
||||||
|
|||||||
Reference in New Issue
Block a user