Files
Amirhossein Khalili 88b793ed9f
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled
initial commit
2026-05-19 20:53:08 +03:30

1198 lines
47 KiB
Python

import hashlib
import uuid
from datetime import timedelta
from types import SimpleNamespace
from unittest import mock
from celery.exceptions import SoftTimeLimitExceeded
from django.http import QueryDict
from django.test import SimpleTestCase, TestCase, override_settings
from django.utils import timezone
from django.contrib.admin import AdminSite
from apps.events.admin import EventAdmin, EventEmailLogAdmin, RegistrationAdmin
from apps.events.admin_forms import AnnouncementForm
from apps.events.models import Event, EventEmailLog, Registration
from apps.events.resources import RegistrationResource
from apps.events.tasks import (
_build_email_context,
_event_recipients,
_event_url,
_send_html_email,
queue_event_announcement,
queue_invites_to_non_registered_users,
queue_skyroom_credentials,
send_event_announcement_to_user,
send_event_reminder_task,
send_event_reminder_to_user,
send_invite_to_user,
send_registration_cancellation_email,
send_registration_confirmation_email,
send_skyroom_credentials_individual_task,
send_skyroom_credentials_to_user,
)
from apps.users.models import User
class EventEmailLogUtilsTests(SimpleTestCase):
def test_hash_context_returns_none_for_missing_context(self):
# Arrange / Act
result = EventEmailLog._hash_context(None)
# Assert
self.assertIsNone(result)
def test_hash_context_normalizes_non_string_inputs(self):
# Arrange
value = 1234
expected = hashlib.sha256(str(value).encode("utf-8")).hexdigest()
# Act
result = EventEmailLog._hash_context(value)
# Assert
self.assertEqual(result, expected)
class EventTasksUtilityTests(SimpleTestCase):
def test_build_email_context_joined_values(self):
# Arrange
parts = ("announce", "", None, "body", "more")
# Act
result = _build_email_context(*parts)
# Assert
self.assertEqual(result, "announce|body|more")
def test_build_email_context_returns_none_for_only_empty_parts(self):
# Arrange
parts = ("", None, "")
# Act
result = _build_email_context(*parts)
# Assert
self.assertIsNone(result)
@override_settings(FRONTEND_ROOT="https://app.local/")
def test_event_url_prefers_slug(self):
# Arrange
event = SimpleNamespace(slug="my-event", id=1)
# Act
result = _event_url(event)
# Assert
self.assertEqual(result, "https://app.local/events/my-event")
@override_settings(FRONTEND_ROOT="https://app.local/")
def test_event_url_falls_back_to_id_when_slug_missing(self):
# Arrange
event = SimpleNamespace(slug=None, id=42)
# Act
result = _event_url(event)
# Assert
self.assertEqual(result, "https://app.local/events/42")
@override_settings(DEFAULT_FROM_EMAIL="noreply@example.com")
@mock.patch("apps.events.tasks.EmailMultiAlternatives")
def test_send_html_email_attaches_html_body(self, mock_email_class):
# Arrange
html_body = "<p>Hello <strong>World</strong></p>"
expected_text = "Hello World"
email_instance = mock_email_class.return_value
# Act
_send_html_email("Subject", html_body, "target@example.com")
# Assert
mock_email_class.assert_called_once_with(
subject="Subject",
body=expected_text,
from_email="noreply@example.com",
to=["target@example.com"],
)
email_instance.attach_alternative.assert_called_once_with(html_body, "text/html")
email_instance.send.assert_called_once()
class RegistrationResourceTests(SimpleTestCase):
def setUp(self):
self.resource = RegistrationResource()
def test_dehydrate_ticket_id_truncates_to_eight_characters(self):
# Arrange
ticket_id = uuid.uuid4()
record = SimpleNamespace(ticket_id=ticket_id)
expected = str(ticket_id)[:8]
# Act
result = self.resource.dehydrate_ticket_id(record)
# Assert
self.assertEqual(result, expected)
def test_dehydrate_ticket_id_handles_missing_values(self):
# Arrange
record = SimpleNamespace(ticket_id=None)
# Act
result = self.resource.dehydrate_ticket_id(record)
# Assert
self.assertEqual(result, "")
class AnnouncementFormTests(SimpleTestCase):
def test_statuses_field_initializes_with_confirmed_and_attended(self):
# Arrange
form = AnnouncementForm()
# Act
initial = form.fields["statuses"].initial
# Assert
expected = [
Registration.StatusChoices.CONFIRMED,
Registration.StatusChoices.ATTENDED,
]
self.assertEqual(initial, expected)
class EventEmailLogFactoryMixin:
def create_user(self):
unique = uuid.uuid4().hex
return User.objects.create_user(
email=f"user_{unique}@example.com",
username=f"user_{unique[:10]}",
password="pass1234",
)
def create_event(self, **kwargs):
now = timezone.now()
defaults = {
"title": f"Event {uuid.uuid4().hex[:6]}",
"description": "Fixture event",
"start_time": now,
"end_time": now + timedelta(hours=1),
"slug": f"event-{uuid.uuid4().hex[:6]}",
"price": 0,
}
defaults.update(kwargs)
return Event.objects.create(**defaults)
class EventEmailLogModelTests(EventEmailLogFactoryMixin, TestCase):
def test_claim_creates_pending_log(self):
# Arrange
event = self.create_event()
user = self.create_user()
context = "send-invite"
# Act
log, skipped = EventEmailLog.claim(
event_id=event.id,
user_id=user.id,
kind=EventEmailLog.KIND_INVITE_NON_REGISTERED,
context=context,
)
# Assert
self.assertFalse(skipped)
self.assertEqual(log.status, EventEmailLog.STATUS_PENDING)
self.assertEqual(log.context_hash, EventEmailLog._hash_context(context))
def test_claim_returns_existing_pending_log(self):
# Arrange
event = self.create_event()
user = self.create_user()
context = "announcement"
context_hash = EventEmailLog._hash_context(context)
existing = EventEmailLog.objects.create(
event=event,
user=user,
kind=EventEmailLog.KIND_EVENT_ANNOUNCEMENT,
context_hash=context_hash,
status=EventEmailLog.STATUS_PENDING,
)
# Act
log, skipped = EventEmailLog.claim(
event_id=event.id,
user_id=user.id,
kind=EventEmailLog.KIND_EVENT_ANNOUNCEMENT,
context=context,
)
# Assert
self.assertTrue(skipped)
self.assertEqual(log.pk, existing.pk)
self.assertEqual(log.status, EventEmailLog.STATUS_PENDING)
def test_claim_resets_failed_record(self):
# Arrange
event = self.create_event()
user = self.create_user()
context = "retry"
context_hash = EventEmailLog._hash_context(context)
log = EventEmailLog.objects.create(
event=event,
user=user,
kind=EventEmailLog.KIND_EVENT_ANNOUNCEMENT,
context_hash=context_hash,
status=EventEmailLog.STATUS_FAILED,
error="boom",
sent_at=timezone.now(),
)
# Act
claimed, skipped = EventEmailLog.claim(
event_id=event.id,
user_id=user.id,
kind=EventEmailLog.KIND_EVENT_ANNOUNCEMENT,
context=context,
)
# Assert
self.assertFalse(skipped)
self.assertEqual(claimed.pk, log.pk)
self.assertEqual(claimed.status, EventEmailLog.STATUS_PENDING)
self.assertEqual(claimed.error, "")
self.assertIsNone(claimed.sent_at)
def test_mark_sent_sets_sent_timestamp_and_status(self):
# Arrange
event = self.create_event()
user = self.create_user()
log = EventEmailLog.objects.create(
event=event,
user=user,
kind=EventEmailLog.KIND_EVENT_ANNOUNCEMENT,
)
# Act
log.mark_sent()
# Assert
self.assertEqual(log.status, EventEmailLog.STATUS_SENT)
self.assertIsNotNone(log.sent_at)
def test_mark_failed_clears_sent_at_and_records_error(self):
# Arrange
event = self.create_event()
user = self.create_user()
log = EventEmailLog.objects.create(
event=event,
user=user,
kind=EventEmailLog.KIND_EVENT_ANNOUNCEMENT,
sent_at=timezone.now(),
)
# Act
log.mark_failed("timeout")
# Assert
self.assertEqual(log.status, EventEmailLog.STATUS_FAILED)
self.assertEqual(log.error, "timeout")
self.assertIsNone(log.sent_at)
class EventModelTests(EventEmailLogFactoryMixin, TestCase):
def test_description_html_renders_markdown(self):
# Arrange
event = self.create_event(description="**bold** content")
# Act
rendered = event.description_html
# Assert
self.assertIn("<strong>bold</strong>", rendered)
def test_is_registration_open_follows_window(self):
# Arrange
now = timezone.now()
event = self.create_event(
registration_start_date=now - timedelta(hours=2),
registration_end_date=now + timedelta(hours=2),
)
# Act / Assert
with mock.patch("apps.events.models.timezone.now", return_value=now):
self.assertTrue(event.is_registration_open)
def test_is_registration_open_closed_outside_window(self):
# Arrange
now = timezone.now()
event = self.create_event(
registration_start_date=now + timedelta(hours=1),
registration_end_date=now + timedelta(hours=2),
)
# Act / Assert
with mock.patch("apps.events.models.timezone.now", return_value=now):
self.assertFalse(event.is_registration_open)
def test_current_attendees_count_filters_statuses(self):
# Arrange
event = self.create_event()
user_one = self.create_user()
user_two = self.create_user()
Registration.objects.create(
event=event,
user=user_one,
status=Registration.StatusChoices.CONFIRMED,
)
Registration.objects.create(
event=event,
user=user_two,
status=Registration.StatusChoices.CANCELLED,
)
Registration.objects.create(
event=event,
user=self.create_user(),
status=Registration.StatusChoices.ATTENDED,
)
# Act
count = event.current_attendees_count
# Assert
self.assertEqual(count, 2)
def test_has_available_slots_respects_capacity(self):
# Arrange
event = self.create_event(capacity=2)
for _ in range(2):
Registration.objects.create(
event=event,
user=self.create_user(),
status=Registration.StatusChoices.CONFIRMED,
)
# Act
available_after_full = event.has_available_slots
# Assert
self.assertFalse(available_after_full)
def test_has_available_slots_allows_unlimited_capacity(self):
# Arrange
event = self.create_event(capacity=None)
# Act
available = event.has_available_slots
# Assert
self.assertTrue(available)
class EventTaskBehaviorTests(EventEmailLogFactoryMixin, TestCase):
def test_event_recipients_filters_by_status_and_email(self):
# Arrange
event = self.create_event()
verified = self.create_user()
verified.is_email_verified = True
verified.save(update_fields=["is_email_verified"])
Registration.objects.create(
event=event,
user=verified,
status=Registration.StatusChoices.CONFIRMED,
)
unverified = self.create_user()
Registration.objects.create(
event=event,
user=unverified,
status=Registration.StatusChoices.CONFIRMED,
)
# Act
recipients = list(_event_recipients(event, statuses=[Registration.StatusChoices.CONFIRMED]))
# Assert
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0].user_id, verified.id)
@override_settings(DEFAULT_FROM_EMAIL="noreply@example.com")
@mock.patch("apps.events.tasks.EmailMultiAlternatives")
@mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>")
@mock.patch("apps.events.tasks.strip_tags", side_effect=lambda html: "ok")
@mock.patch("apps.events.tasks.markdown.markdown", return_value="converted")
def test_send_registration_confirmation_email_sends_message(
self,
mock_markdown,
mock_strip,
mock_render,
mock_email_class,
):
# Arrange
registration = SimpleNamespace(
pk=1,
user=SimpleNamespace(email="user@example.com", username="user-one"),
event=SimpleNamespace(
title="Title",
registration_success_markdown="**done**",
),
)
manager = mock.MagicMock()
manager.select_related.return_value.get.return_value = registration
mock_email_instance = mock_email_class.return_value
# Act
with mock.patch("apps.events.tasks.Registration.objects", manager):
send_registration_confirmation_email.run("1")
# Assert helpers
mock_markdown.assert_called_once_with(
"**done**",
extensions=["extra", "sane_lists", "toc"],
)
mock_render.assert_called_once_with(
"emails/event_registration_confirmation.html",
{
"user": registration.user,
"event": registration.event,
"registration": registration,
"success_html": "converted",
},
)
mock_strip.assert_called_once_with("<p>ok</p>")
# Assert
mock_email_class.assert_called_once()
mock_email_instance.attach_alternative.assert_called_once()
mock_email_instance.send.assert_called_once()
class EventAdminTests(EventEmailLogFactoryMixin, TestCase):
def setUp(self):
self.site = AdminSite()
self.event_admin = EventAdmin(Event, self.site)
self.registration_admin = RegistrationAdmin(Registration, self.site)
self.event_admin.message_user = mock.Mock()
self.registration_admin.message_user = mock.Mock()
def test_price_display_returns_label_for_free(self):
# Arrange
now = timezone.now()
event = Event(
title="Free Event",
description="desc",
start_time=now,
end_time=now + timedelta(hours=1),
price=None,
)
# Act
result = self.event_admin.price_display(event)
# Assert
self.assertEqual(result, "رایگان")
@mock.patch("apps.events.admin.jdate", return_value="JDATE")
def test_start_time_display_calls_jdate(self, mock_jdate):
event = self.create_event()
result = self.event_admin.start_time_display(event)
mock_jdate.assert_called_once_with(event.start_time)
self.assertEqual(result, "JDATE")
@mock.patch("apps.events.admin.jdate", return_value="JDATE")
def test_end_time_display_calls_jdate(self, mock_jdate):
event = self.create_event()
result = self.event_admin.end_time_display(event)
mock_jdate.assert_called_once_with(event.end_time)
self.assertEqual(result, "JDATE")
def test_capacity_display_handles_unlimited(self):
event = self.create_event(capacity=None)
result = self.event_admin.capacity_display(event)
self.assertEqual(result, "نامحدود")
@mock.patch("apps.events.admin.Event.current_attendees_count", new_callable=mock.PropertyMock, return_value=7)
def test_attendees_display_returns_current_attendees(self, _mock_count):
event = self.create_event()
result = self.event_admin.attendees_display(event)
self.assertEqual(result, 7)
@mock.patch("apps.events.admin.Event.is_registration_open", new_callable=mock.PropertyMock, return_value=True)
def test_is_registration_open_display_returns_bool(self, _mock_open):
event = self.create_event()
self.assertTrue(self.event_admin.is_registration_open_display(event))
def test_make_draft_updates_status(self):
event = self.create_event(status=Event.StatusChoices.PUBLISHED)
queryset = Event.all_objects.filter(pk=event.pk)
self.event_admin.make_draft(None, queryset)
self.assertEqual(Event.objects.get(pk=event.pk).status, Event.StatusChoices.DRAFT)
def test_make_cancelled_updates_status(self):
event = self.create_event(status=Event.StatusChoices.DRAFT)
queryset = Event.all_objects.filter(pk=event.pk)
self.event_admin.make_cancelled(None, queryset)
self.assertEqual(Event.objects.get(pk=event.pk).status, Event.StatusChoices.CANCELLED)
def test_make_completed_updates_status(self):
event = self.create_event(status=Event.StatusChoices.PUBLISHED)
queryset = Event.objects.filter(pk=event.pk)
self.event_admin.make_completed(None, queryset)
self.assertEqual(Event.objects.get(pk=event.pk).status, Event.StatusChoices.COMPLETED)
def test_restore_events_marks_is_deleted_false(self):
event = self.create_event()
event.delete()
queryset = Event.all_objects.filter(pk=event.pk)
self.event_admin.restore_events(None, queryset)
self.assertFalse(Event.all_objects.get(pk=event.pk).is_deleted)
def test_action_send_skyroom_credentials_queues_task(self):
event = self.create_event()
with mock.patch("apps.events.admin.queue_skyroom_credentials.delay") as mock_delay:
result = self.event_admin.action_send_skyroom_credentials(mock.Mock(), event.pk)
mock_delay.assert_called_once_with(event.pk)
self.assertEqual(result, mock.ANY)
def test_action_send_reminder_now_queues_task(self):
event = self.create_event()
with mock.patch("apps.events.admin.send_event_reminder_task.delay") as mock_delay:
result = self.event_admin.action_send_reminder_now(mock.Mock(), event.pk)
mock_delay.assert_called_once_with(event.pk)
self.assertEqual(result, mock.ANY)
def test_action_send_announcement_dispatches_queue(self):
event = self.create_event()
data = QueryDict(mutable=True)
data.update({"subject": "Hello", "body_html": "<p>hi</p>"})
data.setlist("statuses", [Registration.StatusChoices.CONFIRMED])
request = SimpleNamespace(method="POST", POST=data, user=self.create_user())
with mock.patch("apps.events.admin.queue_event_announcement") as mock_queue, \
mock.patch("apps.events.admin.redirect", return_value="redirected") as mock_redirect:
result = self.event_admin.action_send_announcement(request, event.pk)
mock_queue.delay.assert_called_once()
mock_redirect.assert_called_once()
self.assertEqual(result, "redirected")
def test_action_invite_other_users_queues_task(self):
event = self.create_event()
with mock.patch("apps.events.admin.queue_invites_to_non_registered_users.delay") as mock_delay:
result = self.event_admin.action_invite_other_users(mock.Mock(), event.pk)
mock_delay.assert_called_once_with(event.pk)
self.assertEqual(result, mock.ANY)
def test_make_published_updates_status(self):
event = self.create_event(status=Event.StatusChoices.DRAFT)
queryset = Event.objects.filter(pk=event.pk)
self.event_admin.make_published(None, queryset)
self.assertEqual(Event.objects.get(pk=event.pk).status, Event.StatusChoices.PUBLISHED)
def test_confirm_registrations_sets_status(self):
# Arrange
event = self.create_event()
user = self.create_user()
user.is_email_verified = False
user.save(update_fields=["is_email_verified"])
registration = Registration.objects.create(
event=event,
user=user,
status=Registration.StatusChoices.PENDING,
)
# Act
self.registration_admin.confirm_registrations(None, Registration.objects.filter(pk=registration.pk))
# Assert
self.assertEqual(
Registration.objects.get(pk=registration.pk).status,
Registration.StatusChoices.CONFIRMED,
)
class RegistrationAdminTests(EventEmailLogFactoryMixin, TestCase):
def setUp(self):
self.site = AdminSite()
self.admin = RegistrationAdmin(Registration, self.site)
self.admin.message_user = mock.Mock()
def test_cancel_registrations_sets_status(self):
registration = Registration.objects.create(
event=self.create_event(),
user=self.create_user(),
status=Registration.StatusChoices.PENDING,
)
self.admin.cancel_registrations(None, Registration.objects.filter(pk=registration.pk))
self.assertEqual(
Registration.objects.get(pk=registration.pk).status,
Registration.StatusChoices.CANCELLED,
)
def test_mark_attended_updates_status(self):
registration = Registration.objects.create(
event=self.create_event(),
user=self.create_user(),
status=Registration.StatusChoices.CONFIRMED,
)
self.admin.mark_attended(None, Registration.objects.filter(pk=registration.pk))
self.assertEqual(
Registration.objects.get(pk=registration.pk).status,
Registration.StatusChoices.ATTENDED,
)
def test_restore_registrations_calls_restore(self):
registration = Registration.objects.create(
event=self.create_event(),
user=self.create_user(),
status=Registration.StatusChoices.PENDING,
)
registration.delete()
with mock.patch.object(Registration, "objects", Registration.all_objects):
self.admin.restore_registrations(None, Registration.all_objects.filter(pk=registration.pk))
self.assertFalse(Registration.all_objects.get(pk=registration.pk).is_deleted)
def test_action_email_selected_sends_and_redirects(self):
event = self.create_event()
registration = Registration.objects.create(
event=event,
user=self.create_user(),
status=Registration.StatusChoices.PENDING,
)
data = QueryDict(mutable=True)
data.update({"subject": "Title", "body_html": "<p>body</p>"})
request = SimpleNamespace(method="POST", POST=data, user=self.create_user())
with mock.patch("apps.events.admin.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.admin._send_html_email") as mock_send, \
mock.patch("apps.events.admin.redirect", return_value="redirected") as mock_redirect:
self.admin.action_email_selected(request, registration.pk)
mock_send.assert_called_once()
mock_redirect.assert_called_once()
def test_action_send_skyroom_credentials_queues_task(self):
registration = Registration.objects.create(
event=self.create_event(),
user=self.create_user(),
status=Registration.StatusChoices.CONFIRMED,
)
with mock.patch("apps.events.admin.send_skyroom_credentials_individual_task.delay") as mock_delay:
result = self.admin.action_send_skyroom_credentials(mock.Mock(), registration.pk)
mock_delay.assert_called_once_with(registration.pk)
self.assertEqual(result, mock.ANY)
class EventEmailLogAdminTests(EventEmailLogFactoryMixin, TestCase):
def setUp(self):
self.site = AdminSite()
self.admin = EventEmailLogAdmin(EventEmailLog, self.site)
self.admin.message_user = mock.Mock()
def test_user_email_returns_dash_when_missing(self):
event = self.create_event()
user = self.create_user()
user.email = ""
log = EventEmailLog.objects.create(
event=event,
user=user,
kind=EventEmailLog.KIND_EVENT_ANNOUNCEMENT,
)
self.assertEqual(self.admin.user_email(log), "")
def test_resend_selected_emails_requeues_and_clears_error(self):
event = self.create_event()
user = self.create_user()
log = EventEmailLog.objects.create(
event=event,
user=user,
kind=EventEmailLog.KIND_INVITE_NON_REGISTERED,
status=EventEmailLog.STATUS_FAILED,
error="boom",
)
with mock.patch("apps.events.admin.send_invite_to_user.delay") as mock_delay:
self.admin.resend_selected_emails(mock.Mock(), EventEmailLog.objects.filter(pk=log.pk))
log.refresh_from_db()
self.assertEqual(log.status, EventEmailLog.STATUS_PENDING)
self.assertEqual(log.error, "")
mock_delay.assert_called_once_with(log.event_id, log.user_id)
class EventTasksCoverageTests(EventEmailLogFactoryMixin, TestCase):
def _dummy_registration(self):
user = self.create_user()
user.is_email_verified = True
user.save(update_fields=["is_email_verified"])
event = self.create_event()
registration = Registration.objects.create(
event=event,
user=user,
status=Registration.StatusChoices.CONFIRMED,
)
return registration
def test_send_registration_cancellation_email_returns_when_email_missing(self):
registration = SimpleNamespace(
user=SimpleNamespace(email=None),
event=SimpleNamespace(title="Title"),
)
manager = mock.MagicMock()
manager.select_related.return_value.get.return_value = registration
with mock.patch("apps.events.tasks.Registration.objects", manager), \
mock.patch("apps.events.tasks.EmailMultiAlternatives") as mock_email:
send_registration_cancellation_email.run("1")
mock_email.assert_not_called()
def test_send_registration_cancellation_email_retries_on_failure(self):
registration = SimpleNamespace(
user=SimpleNamespace(email="user@example.com"),
event=SimpleNamespace(title="Title"),
)
manager = mock.MagicMock()
manager.select_related.return_value.get.return_value = registration
email_instance = mock.MagicMock()
email_instance.send.side_effect = RuntimeError("boom")
mock_email_class = mock.MagicMock(return_value=email_instance)
with mock.patch("apps.events.tasks.Registration.objects", manager), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", mock_email_class), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"), \
mock.patch.object(send_registration_cancellation_email, "retry", side_effect=RuntimeError("retry")) as mock_retry:
with self.assertRaises(RuntimeError):
send_registration_cancellation_email.run("1")
mock_retry.assert_called_once()
def test_send_skyroom_credentials_individual_task_sends_email(self):
user = SimpleNamespace(email="user@example.com")
event = SimpleNamespace(
title="E",
slug="slug",
online_link="https://example.com",
)
registration = SimpleNamespace(
event=event,
user=user,
ticket_id="abcdefghijk",
)
manager = mock.MagicMock()
manager.get.return_value = registration
email_instance = mock.MagicMock()
with mock.patch("apps.events.tasks.Registration.objects", manager), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", mock.MagicMock(return_value=email_instance)), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"):
send_skyroom_credentials_individual_task.run(1)
email_instance.send.assert_called_once()
def test_send_event_reminder_task_sends_messages(self):
event = SimpleNamespace(title="Ev", slug="slug")
class DummyRegs:
def __init__(self, ids):
self.ids = ids
def select_related(self, *args, **kwargs):
return self
def distinct(self):
return self
def values_list(self, *args, **kwargs):
return self.ids
regs = DummyRegs([1])
with mock.patch("apps.events.tasks.Event.objects.get", return_value=event), \
mock.patch("apps.events.tasks._event_recipients", return_value=regs), \
mock.patch("apps.events.tasks.group") as mock_group:
mock_job = mock.MagicMock()
mock_group.return_value = mock_job
mock_job.apply_async.return_value = mock.MagicMock(id="gid")
result = send_event_reminder_task.run(1)
mock_group.assert_called_once()
mock_job.apply_async.assert_called_once()
self.assertEqual(result["queued"], 1)
self.assertEqual(result["group_id"], "gid")
def test_queue_event_announcement_builds_group(self):
event = self.create_event()
class DummyQS:
def __init__(self, ids):
self.ids = ids
def select_related(self, *args, **kwargs):
return self
def exclude(self, *args, **kwargs):
return self
def distinct(self):
return self
def values_list(self, *args, **kwargs):
return self.ids
with mock.patch("apps.events.tasks.Event.objects.get", return_value=event), \
mock.patch("apps.events.tasks._event_recipients", return_value=DummyQS([1, 2])), \
mock.patch("apps.events.tasks.group") as mock_group:
mock_job = mock.MagicMock()
mock_group.return_value = mock_job
result = queue_event_announcement.run(event.id, "subject", "<p>body</p>")
mock_group.assert_called_once()
mock_job.apply_async.assert_called_once()
self.assertEqual(result["queued"], 2)
def test_send_event_announcement_to_user_marks_sent(self):
event = self.create_event()
user = self.create_user()
registration = SimpleNamespace(
user=user,
event=event,
id=1,
)
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
with mock.patch("apps.events.tasks.Registration.objects.select_related") as mock_select, \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, False)), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", return_value=mock.MagicMock()):
mock_select.return_value.get.return_value = registration
send_event_announcement_to_user._orig_run(event.id, 1, "subject", "<p>body</p>")
log.mark_sent.assert_called_once()
def test_send_event_announcement_to_user_returns_skip(self):
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
with mock.patch("apps.events.tasks.Registration.objects.select_related") as mock_select, \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, True)):
mock_select.return_value.get.return_value = SimpleNamespace(user=SimpleNamespace(id=1), event=SimpleNamespace(slug="slug"))
result = send_event_announcement_to_user._orig_run(1, 1, "subject", "<p>body</p>")
self.assertEqual(result, {"skipped": True, "status": log.status})
def test_queue_invites_to_non_registered_users_uses_group(self):
event = self.create_event()
class DummyUserQS:
def __init__(self, ids):
self.ids = ids
def filter(self, *args, **kwargs):
return self
def exclude(self, *args, **kwargs):
return self
def distinct(self):
return self
def values_list(self, *args, **kwargs):
return self.ids
with mock.patch("apps.events.tasks.Event.objects.get", return_value=event), \
mock.patch("apps.events.tasks.User.objects.all", return_value=DummyUserQS([1])), \
mock.patch("apps.events.tasks.group") as mock_group:
mock_job = mock.MagicMock()
mock_group.return_value = mock_job
result = queue_invites_to_non_registered_users.run(event.id)
mock_job.apply_async.assert_called_once()
self.assertEqual(result["queued"], 1)
def test_send_invite_to_user_skips_when_claimed(self):
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
with mock.patch("apps.events.tasks.Event.objects.get", return_value=self.create_event()), \
mock.patch("apps.events.tasks.User.objects.get", return_value=self.create_user()), \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, True)):
result = send_invite_to_user._orig_run(1, 1)
self.assertEqual(result, {"skipped": True, "status": log.status})
def test_send_invite_to_user_sends_email(self):
msg_instance = mock.MagicMock()
target_user = self.create_user()
with mock.patch("apps.events.tasks.Event.objects.get", return_value=self.create_event()), \
mock.patch("apps.events.tasks.User.objects.get", return_value=target_user), \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(mock.MagicMock(), False)), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks._build_email_context", return_value="ctx"), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", return_value=msg_instance):
result = send_invite_to_user._orig_run(1, 1)
msg_instance.send.assert_called_once()
self.assertEqual(result, f"Email sent to {target_user.email}")
def test_queue_skyroom_credentials_builds_group(self):
event = self.create_event()
class DummyRegQS:
def __init__(self, ids):
self.ids = ids
def select_related(self, *args, **kwargs):
return self
def exclude(self, *args, **kwargs):
return self
def distinct(self):
return self
def values_list(self, *args, **kwargs):
return self.ids
with mock.patch("apps.events.tasks.Event.objects.get", return_value=event), \
mock.patch("apps.events.tasks._event_recipients", return_value=DummyRegQS([1])), \
mock.patch("apps.events.tasks.group") as mock_group:
mock_job = mock.MagicMock()
mock_group.return_value = mock_job
result = queue_skyroom_credentials.run(event.id)
mock_job.apply_async.assert_called_once()
self.assertEqual(result["queued"], 1)
def test_send_skyroom_credentials_to_user_skips_when_claimed(self):
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
with mock.patch("apps.events.tasks.Registration.objects.select_related") as mock_select, \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, True)):
mock_select.return_value.get.return_value = SimpleNamespace(
user=SimpleNamespace(id=1, email="user@example.com"),
event=SimpleNamespace(id=1, slug="slug", online_link="https://example.com", title="E"),
ticket_id=uuid.uuid4(),
)
result = send_skyroom_credentials_to_user._orig_run(1, 1)
self.assertEqual(result, {"skipped": True, "status": log.status})
def test_send_skyroom_credentials_to_user_sends_email(self):
msg_instance = mock.MagicMock()
with mock.patch("apps.events.tasks.Registration.objects.select_related") as mock_select, \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(mock.MagicMock(), False)), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", return_value=msg_instance):
mock_select.return_value.get.return_value = SimpleNamespace(
user=SimpleNamespace(email="user@example.com", id=1),
event=SimpleNamespace(title="Title", slug="slug", online_link="https://example.com"),
ticket_id=uuid.uuid4(),
)
send_skyroom_credentials_to_user._orig_run(1, 1)
msg_instance.send.assert_called_once()
def test_send_registration_confirmation_email_skips_without_email(self):
registration = SimpleNamespace(
user=SimpleNamespace(email=""),
event=SimpleNamespace(title="Title", registration_success_markdown=""),
)
manager = mock.MagicMock()
manager.select_related.return_value.get.return_value = registration
with mock.patch("apps.events.tasks.Registration.objects", manager), \
mock.patch("apps.events.tasks.EmailMultiAlternatives") as mock_email:
send_registration_confirmation_email.run("1")
mock_email.assert_not_called()
def test_send_registration_confirmation_email_retries_on_failure(self):
registration = SimpleNamespace(
user=SimpleNamespace(email="user@example.com"),
event=SimpleNamespace(title="Title", registration_success_markdown=""),
)
manager = mock.MagicMock()
manager.select_related.return_value.get.return_value = registration
email_instance = mock.MagicMock()
email_instance.send.side_effect = RuntimeError("boom")
mock_email_class = mock.MagicMock(return_value=email_instance)
with mock.patch("apps.events.tasks.Registration.objects", manager), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", mock_email_class), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"), \
mock.patch.object(send_registration_confirmation_email, "retry", side_effect=RuntimeError("retry")) as mock_retry:
with self.assertRaises(RuntimeError):
send_registration_confirmation_email.run("1")
mock_retry.assert_called_once()
def test_event_recipients_disregards_verification_flag(self):
event = self.create_event()
user = self.create_user()
user.is_email_verified = False
user.save(update_fields=["is_email_verified"])
registration = Registration.objects.create(
event=event,
user=user,
status=Registration.StatusChoices.PENDING,
)
recipients = _event_recipients(event, only_verified=False)
self.assertEqual(len(recipients), 1)
self.assertEqual(recipients[0].user_id, user.id)
def test_send_skyroom_credentials_individual_task_retries_on_failure(self):
user = SimpleNamespace(email="user@example.com")
event = SimpleNamespace(title="Title", slug="slug", online_link="https://example.com")
registration = SimpleNamespace(user=user, event=event, ticket_id="abcdef")
manager = mock.MagicMock()
manager.get.return_value = registration
with mock.patch("apps.events.tasks.Registration.objects", manager), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", mock.MagicMock(return_value=mock.MagicMock(send=mock.Mock(side_effect=RuntimeError("boom"))))), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"), \
mock.patch.object(send_skyroom_credentials_individual_task, "retry", side_effect=RuntimeError("retry")) as mock_retry:
with self.assertRaises(RuntimeError):
send_skyroom_credentials_individual_task.run(1)
self.assertTrue(mock_retry.called)
def test_send_event_reminder_task_propagates_failure(self):
event = SimpleNamespace(title="Ev", slug="slug")
class DummyRegs:
def __init__(self, ids):
self.ids = ids
def select_related(self, *args, **kwargs):
return self
def distinct(self):
return self
def values_list(self, *args, **kwargs):
return self.ids
regs = DummyRegs([1])
with mock.patch("apps.events.tasks.Event.objects.get", return_value=event), \
mock.patch("apps.events.tasks._event_recipients", return_value=regs), \
mock.patch("apps.events.tasks.group") as mock_group:
mock_job = mock.MagicMock()
mock_group.return_value = mock_job
mock_job.apply_async.side_effect = RuntimeError("boom")
with self.assertRaises(RuntimeError):
send_event_reminder_task.run(1)
def test_send_event_reminder_to_user_marks_sent(self):
event = self.create_event()
user = self.create_user()
registration = SimpleNamespace(user=user, event=event, id=1)
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
msg_instance = mock.MagicMock()
with mock.patch("apps.events.tasks.Registration.objects.select_related") as mock_select, \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, False)), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", return_value=msg_instance):
mock_select.return_value.get.return_value = registration
result = send_event_reminder_to_user._orig_run(event.id, 1)
msg_instance.send.assert_called_once()
log.mark_sent.assert_called_once()
self.assertEqual(result, f"Email sent to {user.email}")
def test_send_event_announcement_to_user_handles_soft_time_limit(self):
event = self.create_event()
user = self.create_user()
registration = SimpleNamespace(user=user, event=event, id=1)
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
with mock.patch("apps.events.tasks.Registration.objects.select_related") as mock_select, \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, False)), \
mock.patch("apps.events.tasks.render_to_string", side_effect=SoftTimeLimitExceeded("timeout")), \
mock.patch("apps.events.tasks.strip_tags") as mock_strip:
mock_select.return_value.get.return_value = registration
with self.assertRaises(SoftTimeLimitExceeded):
send_event_announcement_to_user._orig_run(1, 1, "subject", "<p>body</p>")
log.mark_failed.assert_called_once_with("Soft time limit exceeded")
def test_send_event_announcement_to_user_handles_failure(self):
event = self.create_event()
user = self.create_user()
registration = SimpleNamespace(user=user, event=event, id=1)
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
with mock.patch("apps.events.tasks.Registration.objects.select_related") as mock_select, \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, False)), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", return_value=mock.MagicMock(send=mock.Mock(side_effect=RuntimeError("boom")))):
mock_select.return_value.get.return_value = registration
with self.assertRaises(RuntimeError):
send_event_announcement_to_user._orig_run(1, 1, "subject", "<p>body</p>")
log.mark_failed.assert_called_once()
def test_send_invite_to_user_handles_failure(self):
event = self.create_event()
user = self.create_user()
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
with mock.patch("apps.events.tasks.Event.objects.get", return_value=event), \
mock.patch("apps.events.tasks.User.objects.get", return_value=user), \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, False)), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks._build_email_context", return_value="ctx"), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", return_value=mock.MagicMock(send=mock.Mock(side_effect=RuntimeError("boom")))):
with self.assertRaises(RuntimeError):
send_invite_to_user._orig_run(1, 1)
log.mark_failed.assert_called_once()
def test_send_skyroom_credentials_to_user_handles_failure(self):
event = self.create_event()
user = self.create_user()
log = mock.MagicMock(status=EventEmailLog.STATUS_PENDING)
with mock.patch("apps.events.tasks.Registration.objects.select_related") as mock_select, \
mock.patch("apps.events.tasks.EventEmailLog.claim", return_value=(log, False)), \
mock.patch("apps.events.tasks.render_to_string", return_value="<p>ok</p>"), \
mock.patch("apps.events.tasks.strip_tags", return_value="ok"), \
mock.patch("apps.events.tasks.EmailMultiAlternatives", return_value=mock.MagicMock(send=mock.Mock(side_effect=RuntimeError("boom")))):
mock_select.return_value.get.return_value = SimpleNamespace(
user=user,
event=event,
ticket_id=uuid.uuid4(),
)
with self.assertRaises(RuntimeError):
send_skyroom_credentials_to_user._orig_run(1, 1)
log.mark_failed.assert_called_once()
def test_queue_invites_to_non_registered_users_respects_filters(self):
event = self.create_event()
verified = self.create_user()
verified.is_email_verified = True
verified.save(update_fields=["is_email_verified"])
inactive = self.create_user()
inactive.is_email_verified = True
inactive.is_active = False
inactive.save(update_fields=["is_email_verified", "is_active"])
with mock.patch("apps.events.tasks.group") as mock_group:
mock_job = mock.MagicMock()
mock_group.return_value = mock_job
result = queue_invites_to_non_registered_users.run(event.id, only_verified=True, only_active=True)
mock_job.apply_async.assert_called_once()
self.assertEqual(result["queued"], 1)