# Part of Odoo. See LICENSE file for full copyright and licensing details. from contextlib import contextmanager from datetime import datetime, timedelta from freezegun import freeze_time from unittest.mock import patch from itertools import zip_longest from odoo import exceptions from odoo.addons.event.tests.common import EventCase from odoo.addons.whatsapp.tests.common import WhatsAppCommon from odoo.tests import tagged, users from odoo.tools import mute_logger @tagged('event_mail') class TestWhatsappSchedule(EventCase, WhatsAppCommon): @classmethod def setUpClass(cls): super().setUpClass() # test subscription whatsapp template cls.whatsapp_template_sub = cls.env['whatsapp.template'].create({ 'body': "{{1}} registration confirmation.", 'name': "Test subscription", 'model_id': cls.env['ir.model']._get_id('event.registration'), 'status': 'approved', 'phone_field': 'phone', 'wa_account_id': cls.whatsapp_account.id, }) cls.whatsapp_template_sub.variable_ids.write({ 'field_type': "field", 'field_name': "event_id", }) # test reminder whatsapp template cls.whatsapp_template_rem = cls.env['whatsapp.template'].create({ 'body': "{{1}} reminder.", 'name': "Test reminder", 'model_id': cls.env['ir.model']._get_id('event.registration'), 'status': 'approved', 'phone_field': 'phone', 'wa_account_id': cls.whatsapp_account.id, }) cls.whatsapp_template_rem.variable_ids.write({ 'field_type': "field", 'field_name': "event_id", }) # test event cls.reference_now = datetime(2023, 3, 10, 14, 30, 15, 0) cls.test_event = cls.env['event.event'].create({ 'date_begin': cls.reference_now + timedelta(days=5), 'date_end': cls.reference_now + timedelta(days=10), 'date_tz': 'Europe/Brussels', 'event_mail_ids': [ (5, 0), (0, 0, { # right at subscription 'interval_unit': 'now', 'interval_type': 'after_sub', 'notification_type': 'whatsapp', 'template_ref': 'whatsapp.template,%i' % cls.whatsapp_template_sub.id}), (0, 0, { # 3 days before event 'interval_nbr': 3, 'interval_unit': 'days', 'interval_type': 'before_event', 'notification_type': 'whatsapp', 'template_ref': 'whatsapp.template,%i' % cls.whatsapp_template_rem.id}), ], 'name': 'Test Event', }) with cls.mock_datetime_and_now(cls, cls.reference_now): cls.test_attendees = cls.env["event.registration"].create([ { "event_id": cls.test_event.id, "name": f"WA attendee {idx}", "email": f"_test_reg_{idx}@example.com", "phone": f"+324560000{idx}{idx}", } for idx in range(2) ]) @contextmanager def mock_datetime_and_now(self, mock_dt): """ Used when synchronization date (using env.cr.now()) is important in addition to standard datetime mocks. Used mainly to detect sync issues. """ with freeze_time(mock_dt), \ patch.object(self.env.cr, 'now', lambda: mock_dt): yield @users('user_eventmanager') def test_assert_initial_values(self): """ Be sure of our initial setup """ sub_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == "after_sub") self.assertEqual(len(sub_scheduler), 1) self.assertEqual(sub_scheduler.mail_count_done, 2) self.assertTrue(sub_scheduler.mail_done) self.assertEqual(sub_scheduler.scheduled_date, self.test_event.create_date.replace(microsecond=0), 'event: incorrect scheduled date for checking controller') before_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == "before_event") self.assertEqual(len(before_scheduler), 1) self.assertEqual(before_scheduler.mail_count_done, 0) self.assertFalse(before_scheduler.mail_done) self.assertEqual(before_scheduler.scheduled_date, self.test_event.date_begin + timedelta(days=-3)) @users('user_eventmanager') def test_whatsapp_schedule(self): test_event = self.env['event.event'].browse(self.test_event.ids) with self.mockWhatsappGateway(): new_regs = self._create_registrations(test_event, 3) # check subscription scheduler sub_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub')]) # verify that subscription scheduler was auto-executed after each registration self.assertEqual(len(sub_scheduler.mail_registration_ids), 5, "2 pre-existing + 3 new") self.assertTrue(all(m.mail_sent is True for m in sub_scheduler.mail_registration_ids)) self.assertEqual(sub_scheduler.mapped('mail_registration_ids.registration_id'), test_event.registration_ids) self.assertTrue(sub_scheduler.mail_done) self.assertEqual(sub_scheduler.mail_count_done, 5, "2 pre-existing + 3 new") # verify that message sent correctly after each registration for registration in new_regs: self.assertWAMessageFromRecord(registration, status='outgoing') # check before event scheduler before_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')]) # execute event reminder scheduler explicitly with self.mock_datetime_and_now(test_event.date_begin + timedelta(days=-3, hours=1)), \ self.mockWhatsappGateway(): before_scheduler.execute() self.assertTrue(before_scheduler.mail_done) self.assertEqual(before_scheduler.mail_count_done, 5, "2 pre-existing + 3 new") test_event.date_begin = self.reference_now + timedelta(hours=1) self.assertGreater(self.reference_now, before_scheduler.scheduled_date, 'Scheduler scheduled_date should trigger it.') for registration, state in zip_longest(test_event.registration_ids, ['draft', 'open', 'open', 'done'], fillvalue='cancel'): registration.state = state before_scheduler.mail_done = False with self.mock_datetime_and_now(self.reference_now), self.mockWhatsappGateway(): before_scheduler.execute() for registration in test_event.registration_ids.filtered(lambda reg: reg.state in ('open', 'done')): with self.subTest(registration_state=registration.state): self.assertWAMessageFromRecord(registration, status='outgoing') self.assertEqual(len(self._new_wa_msg), 3, 'Whatsapp messages should not be send to draft and cancel registrations') self.assertEqual(before_scheduler.filtered(lambda r: r.notification_type == 'whatsapp').mail_count_done, 3, 'Wrong Whatsapp Sent Count! Probably msg sent to unconfirmed attendees were not included into the Sent Count') @mute_logger('odoo.addons.event.models.event_mail') @users('user_eventmanager') def test_whatsapp_schedule_fail_global_composer(self): # Simulate a fail during composer usage e.g. invalid field path, template # model change, ... to check defensive behavior cron = self.env.ref("event.event_mail_scheduler").sudo() before_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == "before_event") def _patched_composer_send(self, *args, **kwargs): raise exceptions.ValidationError('Some error') with patch.object(type(self.env["whatsapp.composer"]), "_send_whatsapp_template", _patched_composer_send): with self.mock_datetime_and_now(self.reference_now + timedelta(days=3)), \ self.mockWhatsappGateway(): cron.method_direct_trigger() self.assertFalse(before_scheduler.mail_done) @mute_logger('odoo.addons.event.models.event_mail', 'odoo.addons.whatsapp_event.models.event_mail', 'odoo.addons.whatsapp_event.models.event_mail_registration') @users('user_eventmanager') def test_whatsapp_schedule_fail_global_no_registrations(self): """ Be sure no registrations = no crash in wa composer """ cron = self.env.ref("event.event_mail_scheduler").sudo() self.test_event.registration_ids.unlink() with self.mock_datetime_and_now(self.reference_now + timedelta(days=3)), \ self.mockWhatsappGateway(): cron.method_direct_trigger() @mute_logger('odoo.addons.whatsapp_event.models.event_mail') @users('user_eventmanager') def test_whatsapp_schedule_fail_global_template_draft(self): """ Test flow where scheduler fails due to template ref being in draft when sending global event communication (i.e. only through cron). """ cron = self.env.ref("event.event_mail_scheduler").sudo() before_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == "before_event") # ensure there is a single draft template (crash in composer) self.env["whatsapp.template"].sudo().search( [("model_id", "=", self.env["ir.model"]._get_id("event.registration"))] ).unlink() tpl_draft = self.env['whatsapp.template'].sudo().create({ "body": "Test reminder", "model_id": self.env["ir.model"]._get_id("event.registration"), "name": "Draft Fail", "phone_field": "phone", "status": "draft", "wa_account_id": self.whatsapp_account.id, }) before_scheduler.template_ref = tpl_draft with self.mock_datetime_and_now(self.reference_now + timedelta(days=3)), \ self.mockWhatsappGateway(): cron.method_direct_trigger() self.assertFalse(before_scheduler.mail_done) @mute_logger('odoo.addons.whatsapp_event.models.event_mail') @users('user_eventmanager') def test_whatsapp_schedule_fail_global_template_removed(self): """ Test flow where scheduler fails due to template ref being removed when sending global event communication (i.e. only through cron). """ cron = self.env.ref("event.event_mail_scheduler").sudo() before_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == "before_event") # make before event scheduler crash, remove linked template self.whatsapp_template_rem.sudo().unlink() test_event = self.env['event.event'].browse(self.test_event.ids) with self.mockWhatsappGateway(): _new_regs = self._create_registrations(test_event, 3) # execute event reminder scheduler explicitly: should not crash, just skip with self.mock_datetime_and_now(self.reference_now + timedelta(days=3)), \ self.mockWhatsappGateway(): cron.method_direct_trigger() self.assertFalse(before_scheduler.mail_done) @mute_logger('odoo.addons.whatsapp_event.models.event_mail_registration') @users('user_eventmanager') def test_whatsapp_schedule_fail_registration_composer(self): """ Simulate a fail during composer usage e.g. invalid field path, template # model change, ... to check defensive behavior """ onsub_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == "after_sub") def _patched_composer_send(self, *args, **kwargs): raise exceptions.ValidationError('Some error') with patch.object(type(self.env["whatsapp.composer"]), "_send_whatsapp_template", _patched_composer_send): with self.mockWhatsappGateway(): registration = self.env['event.registration'].create({ "email": "test@email.com", "event_id": self.test_event.id, "name": "Mitchell Admin", "phone": "(255)-595-8393", }) self.assertTrue(registration.exists(), "Registration record should exist after creation.") self.assertEqual(onsub_scheduler.mail_count_done, 2) self.assertFalse(onsub_scheduler.mail_done) @mute_logger('odoo.addons.whatsapp_event.models.event_mail') @users('user_eventmanager') def test_whatsapp_schedule_fail_registration_template_draft(self): """ Test flow where scheduler fails due to template being draft. """ # ensure there is a single draft template (crash in composer) self.env["whatsapp.template"].sudo().search( [("model_id", "=", self.env["ir.model"]._get_id("event.registration"))] ).unlink() tpl_draft = self.env['whatsapp.template'].sudo().create({ "body": "Test reminder", "model_id": self.env["ir.model"]._get_id("event.registration"), "name": "Draft Fail", "phone_field": "phone", "status": "draft", "wa_account_id": self.whatsapp_account.id, }) self.test_event.write({ 'event_mail_ids': [ (5, 0), (0, 0, { # right at subscription 'interval_unit': 'now', 'interval_type': 'after_sub', 'notification_type': 'whatsapp', 'template_ref': 'whatsapp.template,%i' % tpl_draft.id}), ] }) with self.mockWhatsappGateway(): registration = self.env['event.registration'].create({ "email": "test@email.com", "event_id": self.test_event.id, "name": "Mitchell Admin", "phone": "(255)-595-8393", }) self.assertTrue(registration.exists(), "Registration record should exist after creation.") sub_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == "after_sub") self.assertFalse(sub_scheduler.mail_done) @mute_logger('odoo.addons.whatsapp_event.models.event_mail') @users('user_eventmanager') def test_whatsapp_schedule_fail_registration_template_removed(self): """ Test flow where scheduler fails due to template being removed. """ # make on subscription scheduler crash, remove linked template self.whatsapp_template_sub.sudo().unlink() with self.mockWhatsappGateway(): registration = self.env['event.registration'].create({ "email": "test@email.com", "event_id": self.test_event.id, "name": "Mitchell Admin", "phone": "(255)-595-8393", }) self.assertTrue(registration.exists(), "Registration record should exist after creation.") sub_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == "after_sub") self.assertFalse(sub_scheduler.mail_done)