diff --git a/apps/course/tests/test_webhook_live_session.py b/apps/course/tests/test_webhook_live_session.py new file mode 100644 index 0000000..2401a7e --- /dev/null +++ b/apps/course/tests/test_webhook_live_session.py @@ -0,0 +1,139 @@ +import hashlib +import hmac +import json +import tempfile + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import override_settings +from django.urls import reverse +from django.utils import timezone +from dj_language.models import Language +from rest_framework import status +from rest_framework.test import APITestCase + +from apps.account.models import ProfessorUser, StudentUser +from apps.course.models import ( + Course, + CourseCategory, + CourseLiveSession, + LiveSessionUser, +) + + +@override_settings( + PLUGNMEET_API_SECRET='test-secret', + MEDIA_ROOT=tempfile.gettempdir(), +) +class PlugNMeetWebhookLiveSessionTests(APITestCase): + def setUp(self): + Language.objects.update_or_create( + id=69, + defaults={ + 'name': 'English', + 'code': 'en', + 'status': True, + 'countries': ['US'], + }, + ) + self.professor = ProfessorUser.objects.create( + email='webhook-prof@example.com', + fullname='Webhook Professor', + experience_years=8, + ) + self.student = StudentUser.objects.create( + email='webhook-student@example.com', + fullname='Webhook Student', + ) + self.category = CourseCategory.objects.create( + name='Webhook Category', + slug='webhook-category', + ) + thumbnail = SimpleUploadedFile( + 'thumb.jpg', + b'filecontent', + content_type='image/jpeg', + ) + self.course = Course.objects.create( + title='Webhook Course', + slug='webhook-course', + category=self.category, + professor=self.professor, + thumbnail=thumbnail, + video_type=Course.VedioTypeChoices.YOUTUBE_LINK, + video_link='https://example.com/video', + is_online=True, + online_link='https://example.com/live', + level=Course.LevelChoices.BEGINNER, + duration=10, + lessons_count=1, + description='Description', + short_description='Short', + status=Course.StatusChoices.ONGOING, + is_free=True, + ) + self.session = CourseLiveSession.objects.create( + course=self.course, + room_id='room-16-1781160705', + subject='Webhook Course Live Session', + started_at=timezone.now(), + ) + LiveSessionUser.objects.create( + session=self.session, + user=self.student, + role='participant', + entered_at=timezone.now(), + is_online=True, + ) + + def _post_webhook(self, payload: dict): + body = json.dumps(payload, ensure_ascii=False).encode('utf-8') + signature = hmac.new( + b'test-secret', + body, + hashlib.sha256, + ).hexdigest() + url = reverse('plugnmeet-webhook') + return self.client.post( + url, + data=body, + content_type='application/webhook+json', + HTTP_HASH_TOKEN=signature, + ) + + def test_room_finished_with_room_name_closes_live_session(self): + response = self._post_webhook( + { + 'event': 'room_finished', + 'room': { + 'name': self.session.room_id, + }, + } + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.session.refresh_from_db() + self.assertIsNotNone(self.session.ended_at) + self.assertFalse( + LiveSessionUser.objects.get(session=self.session, user=self.student).is_online + ) + + def test_session_ended_event_closes_live_session(self): + second_session = CourseLiveSession.objects.create( + course=self.course, + room_id='room-16-1781160999', + subject='Second Live Session', + started_at=timezone.now(), + ) + + response = self._post_webhook( + { + 'event': 'session_ended', + 'room': { + 'name': second_session.room_id, + }, + } + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + second_session.refresh_from_db() + self.assertIsNotNone(second_session.ended_at) diff --git a/apps/course/views/webhook.py b/apps/course/views/webhook.py index 604815d..b242c94 100644 --- a/apps/course/views/webhook.py +++ b/apps/course/views/webhook.py @@ -91,6 +91,7 @@ class PlugNMeetWebhookAPIView(APIView): # 4. Route Event handler_map = { 'room_finished': self._handle_room_finished, + 'session_ended': self._handle_room_finished, 'participant_joined': self._handle_participant_joined, 'participant_left': self._handle_participant_left, 'recording_proceeded': self._handle_recording_proceeded, @@ -178,9 +179,21 @@ class PlugNMeetWebhookAPIView(APIView): logger.error(f"❌ [PlugNMeet Webhook] Signature mismatch! \nReceived: {hash_token[:10]}...\nExpected HMAC: {expected_signature[:10]}...") return False + @staticmethod + def _extract_room_id(room_data: Dict[str, Any]) -> str | None: + if not isinstance(room_data, dict): + return None + + return ( + room_data.get('room_id') + or room_data.get('roomId') + or room_data.get('identity') + or room_data.get('name') + ) + def _handle_room_finished(self, payload: Dict[str, Any]) -> Dict[str, Any]: room_data = payload.get('room', {}) - room_id = room_data.get('room_id') or room_data.get('roomId') or room_data.get('identity') + room_id = self._extract_room_id(room_data) if not room_id: logger.warning(f"⚠️ [PlugNMeet Webhook] Missing room_id in room_finished event. Payload: {payload}") @@ -211,7 +224,7 @@ class PlugNMeetWebhookAPIView(APIView): def _handle_participant_joined(self, payload: Dict[str, Any]) -> Dict[str, Any]: room_data = payload.get('room', {}) participant_data = payload.get('participant', {}) - room_id = room_data.get('room_id') or room_data.get('roomId') or room_data.get('identity') + room_id = self._extract_room_id(room_data) user_id = participant_data.get('identity') or participant_data.get('user_id') or participant_data.get('userId') if not room_id or not user_id: @@ -247,7 +260,7 @@ class PlugNMeetWebhookAPIView(APIView): def _handle_participant_left(self, payload: Dict[str, Any]) -> Dict[str, Any]: room_data = payload.get('room', {}) participant_data = payload.get('participant', {}) - room_id = room_data.get('room_id') or room_data.get('roomId') or room_data.get('identity') + room_id = self._extract_room_id(room_data) user_id = participant_data.get('identity') or participant_data.get('user_id') or participant_data.get('userId') if not room_id or not user_id: @@ -300,7 +313,7 @@ class PlugNMeetWebhookAPIView(APIView): room_data = payload.get('room', {}) recording_info = payload.get('recording_info', {}) - room_id = room_data.get('room_id') or room_data.get('roomId') or room_data.get('identity') + room_id = self._extract_room_id(room_data) recording_id = ( recording_info.get('record_id') or recording_info.get('recordId') or