You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

316 lines
14 KiB

import datetime
from django.utils import timezone
from django.test import TestCase
from unittest.mock import patch
from django.core.files.uploadedfile import SimpleUploadedFile
from dj_language.models import Language
from apps.account.models import StudentUser, User, Notification
from apps.course.models.course import Course, CourseCategory
from apps.course.models.lesson import CourseChapter, CourseLesson, Lesson, LessonCompletion
from apps.course.models.participant import Participant
from apps.course.models.live_session import CourseLiveSession
from apps.certificate.models import Certificate
from apps.account.tasks import check_live_class_reminders_task, check_new_course_registration_task
class NotificationFlowTests(TestCase):
def setUp(self):
# Create languages
self.lang_ru, _ = Language.objects.update_or_create(
code='ru',
defaults={'name': 'Russian', 'status': True, 'countries': []}
)
self.lang_en, _ = Language.objects.update_or_create(
code='en',
defaults={'name': 'English', 'status': True, 'countries': []}
)
# Create category
self.category = CourseCategory.objects.create(
name=[{"title": "Category", "language_code": "en"}],
slug=[{"title": "category", "language_code": "en"}]
)
# Create student user
self.student = StudentUser.objects.create(
email='student_test@example.com',
username='student_test',
fullname='Test Student',
language=self.lang_ru, # Russian template check
fcm='token_student'
)
# Create English student user for template check
self.student_en = StudentUser.objects.create(
email='student_en@example.com',
username='student_en',
fullname='Test Student EN',
language=self.lang_en,
fcm='token_student_en'
)
def _create_course(self, slug, title_en, title_ru, status='ongoing'):
thumbnail = SimpleUploadedFile('thumb.jpg', b'filecontent', content_type='image/jpeg')
return Course.objects.create(
title=[
{"title": title_en, "language_code": "en"},
{"title": title_ru, "language_code": "ru"}
],
slug=[{"title": slug, "language_code": "en"}],
category=self.category,
thumbnail=thumbnail,
video_type=Course.VedioTypeChoices.YOUTUBE_LINK,
video_link='https://example.com/video',
is_online=False,
level=Course.LevelChoices.BEGINNER,
duration=10,
lessons_count=0,
description='Description',
short_description='Short description',
status=[
{"title": status, "language_code": "en"},
{"title": status, "language_code": "ru"}
],
is_free=True,
)
@patch('apps.account.notification_service.send_notification')
def test_course_access_granted_notification(self, mock_send):
# Event 1: Course Access Granted
course = self._create_course('course-access', 'Course 1', 'Курс 1')
# Create participant (active=True by default)
Participant.objects.create(student=self.student, course=course)
# Verify notification created in DB (in Russian)
notif_ru = Notification.objects.filter(user=self.student).first()
self.assertIsNotNone(notif_ru)
self.assertEqual(notif_ru.title, "Доступ к курсу активирован")
self.assertIn("Курс 1", notif_ru.message)
self.assertEqual(notif_ru.notification_type, "course_access_granted")
self.assertEqual(notif_ru.action, "navigate")
self.assertEqual(notif_ru.navigate_to, "/course_info?slug=course-access")
# Create participant for English student
Participant.objects.create(student=self.student_en, course=course)
notif_en = Notification.objects.filter(user=self.student_en).first()
self.assertIsNotNone(notif_en)
self.assertEqual(notif_en.title, "Course Access Granted")
self.assertIn("Course 1", notif_en.message)
self.assertEqual(notif_en.notification_type, "course_access_granted")
self.assertEqual(notif_en.action, "navigate")
self.assertEqual(notif_en.navigate_to, "/course_info?slug=course-access")
# Verify send_notification was called
self.assertTrue(mock_send.called)
# Check that target routing keys were passed to send_notification call
called_args, called_kwargs = mock_send.call_args
sent_data = called_args[3] if len(called_args) > 3 else called_kwargs.get('data')
self.assertEqual(sent_data.get('type'), "course_access_granted")
self.assertEqual(sent_data.get('action'), "navigate")
self.assertEqual(sent_data.get('navigate_to'), "/course_info?slug=course-access")
@patch('apps.account.notification_service.send_notification')
def test_live_class_reminder_notification(self, mock_send):
# Event 2: LIVE Class Reminder
course = self._create_course('course-live', 'Course 2', 'Курс 2')
Participant.objects.create(student=self.student, course=course)
# Create a live session starting in 2 hours
session = CourseLiveSession.objects.create(
course=course,
subject='Live Lesson 1',
started_at=timezone.now() + datetime.timedelta(hours=2),
is_cancelled=False,
reminder_sent=False
)
# Run periodic task
check_live_class_reminders_task()
# Verify reminder sent
session.refresh_from_db()
self.assertTrue(session.reminder_sent)
notif = Notification.objects.filter(user=self.student, title="Напоминание о живом уроке").first()
self.assertIsNotNone(notif)
self.assertIn("Live Lesson 1", notif.message)
# Test rescheduling resets reminder_sent
session.started_at = timezone.now() + datetime.timedelta(hours=5)
session.save()
session.refresh_from_db()
self.assertFalse(session.reminder_sent)
@patch('apps.account.notification_service.send_notification')
def test_course_completed_notification(self, mock_send):
# Event 3: Course Completed
course = self._create_course('course-complete', 'Course 3', 'Курс 3')
Participant.objects.create(student=self.student, course=course)
chapter = CourseChapter.objects.create(
course=course,
title='Chapter 1',
priority=1,
is_active=True
)
# Add 2 lessons
l1 = Lesson.objects.create(title='Lesson 1', content_type='video_file', duration=5)
cl1 = CourseLesson.objects.create(course=course, chapter=chapter, lesson=l1, priority=1, is_active=True)
l2 = Lesson.objects.create(title='Lesson 2', content_type='video_file', duration=5)
cl2 = CourseLesson.objects.create(course=course, chapter=chapter, lesson=l2, priority=2, is_active=True)
# Complete first lesson
LessonCompletion.objects.create(student=self.student, course_lesson=cl1)
self.assertFalse(Notification.objects.filter(user=self.student, title="Курс завершен").exists())
# Complete second (last) lesson
LessonCompletion.objects.create(student=self.student, course_lesson=cl2)
# Verify notification triggered
self.assertTrue(Notification.objects.filter(user=self.student, title="Курс завершен").exists())
@patch('apps.account.notification_service.send_notification')
def test_certificate_issued_notification(self, mock_send):
# Event 4: Certificate Issued
course = self._create_course('course-cert', 'Course 4', 'Курс 4')
cert = Certificate.objects.create(
student=self.student,
course=course,
status='pending'
)
# Verify no notification yet
self.assertFalse(Notification.objects.filter(user=self.student, title="Сертификат выдан").exists())
# Approve certificate
cert.status = 'approved'
cert.save()
# Verify notification triggered
self.assertTrue(Notification.objects.filter(user=self.student, title="Сертификат выдан").exists())
@patch('apps.account.notification_service.send_notification')
def test_new_course_registration_notification(self, mock_send):
# Event 5: New Course Registration
student_recipient = User.objects.create(
email='student_recipient@example.com',
username='student_recipient',
fullname='Recipient Student',
user_type='student',
is_active=True,
language=self.lang_ru,
fcm='token_recipient'
)
course = self._create_course('course-new', 'New Course A', 'Новый курс А', status='registering')
check_new_course_registration_task()
course.refresh_from_db()
self.assertTrue(course.notified_new_registration)
notif = Notification.objects.filter(user=student_recipient, title="Добавлен новый курс").first()
self.assertIsNotNone(notif)
self.assertIn("Новый курс А", notif.message)
@patch('apps.account.notification_service.send_notification')
def test_notification_template_customization_and_deactivation(self, mock_send):
from apps.account.models import NotificationTemplate
# 1. Create a custom template in the database
template = NotificationTemplate.objects.create(
notification_type="custom_test_type",
name="Test Template",
title_ru="Привет {student_name} к курсу {course_title}",
title_en="Hello {student_name} to course {course_title}",
body_ru="Русское описание {student_name}",
body_en="English description {student_name}",
is_active=True
)
course = self._create_course('test-tpl-course', 'Tpl Course', 'Курс Шаблон')
# 2. Trigger notification and verify custom text formatting
from apps.account.notification_service import create_and_send_notification
create_and_send_notification(
user=self.student,
title_en="Default Title EN",
body_en="Default Body EN",
title_ru="Default Title RU",
body_ru="Default Body RU",
service='imam-javad',
data={'type': 'custom_test_type', 'course_id': course.id}
)
notif = Notification.objects.filter(user=self.student).order_by('-id').first()
self.assertIsNotNone(notif)
# Verify it loaded from template and formatted using course name & student name
self.assertEqual(notif.title, f"Привет {self.student.fullname} к курсу Курс Шаблон")
self.assertEqual(notif.message, f"Русское описание {self.student.fullname}")
# 3. Disable template and verify notification is discarded
template.is_active = False
template.save()
count_before = Notification.objects.filter(user=self.student).count()
result = create_and_send_notification(
user=self.student,
title_en="Default Title EN",
body_en="Default Body EN",
title_ru="Default Title RU",
body_ru="Default Body RU",
service='imam-javad',
data={'type': 'custom_test_type', 'course_id': course.id}
)
self.assertIsNone(result)
count_after = Notification.objects.filter(user=self.student).count()
self.assertEqual(count_before, count_after)
def test_scheduled_notification_syncs_periodic_task(self):
from apps.account.models import NotificationTemplate, ScheduledNotification
from django_celery_beat.models import PeriodicTask
template = NotificationTemplate.objects.create(
notification_type="sched_test",
name="Scheduled Test",
title_ru="Тест планирования",
title_en="Scheduled Test",
body_ru="Тест",
body_en="Test"
)
# Create ScheduledNotification
sched_notif = ScheduledNotification.objects.create(
name="My Weekly Campaign",
template=template,
target_group=ScheduledNotification.TargetGroupChoices.ALL_STUDENTS,
schedule_type=ScheduledNotification.ScheduleTypeChoices.WEEKLY,
days_of_week="6,0",
time_of_day="14:30:00",
is_active=True
)
# Verify PeriodicTask and CrontabSchedule are created
self.assertIsNotNone(sched_notif.periodic_task)
pt = sched_notif.periodic_task
self.assertEqual(pt.name, "Scheduled Notification: My Weekly Campaign")
self.assertEqual(pt.crontab.minute, "30")
self.assertEqual(pt.crontab.hour, "14")
self.assertEqual(pt.crontab.day_of_week, "6,0")
self.assertTrue(pt.enabled)
# Deactivate and verify sync
sched_notif.is_active = False
sched_notif.save()
pt.refresh_from_db()
self.assertFalse(pt.enabled)
# Delete and verify dynamic cleanup
task_id = pt.id
sched_notif.delete()
self.assertFalse(PeriodicTask.objects.filter(id=task_id).exists())