File: test_event_mail_schedule.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (676 lines) | stat: -rw-r--r-- 35,937 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from datetime import datetime
from dateutil.relativedelta import relativedelta
from unittest.mock import patch

from odoo import exceptions
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
from odoo.addons.event.tests.common import EventCase
from odoo.addons.mail.tests.common import MockEmail
from odoo.tests import tagged, users, warmup
from odoo.tools import formataddr, mute_logger


@tagged('event_mail', 'post_install', '-at_install')
class TestMailSchedule(EventCase, MockEmail, CronMixinCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        cls.env.company.write({
            'email': 'info@yourcompany.example.com',
            'name': 'YourCompany',
        })
        cls.event_cron_id = cls.env.ref('event.event_mail_scheduler')

        # deactivate other schedulers to avoid messing with crons
        cls.env['event.mail'].search([]).unlink()
        # consider asynchronous sending as default sending
        cls.env["ir.config_parameter"].set_param("event.event_mail_async", False)

        # freeze some datetimes, and ensure more than 1D+1H before event starts
        # to ease time-based scheduler check
        # Since `now` is used to set the `create_date` of an event and create_date
        # has often microseconds, we set it to ensure that the scheduler we still be
        # launched if scheduled_date == create_date - microseconds
        cls.reference_now = datetime(2021, 3, 20, 14, 30, 15, 123456)
        cls.event_date_begin = datetime(2021, 3, 22, 8, 0, 0)
        cls.event_date_end = datetime(2021, 3, 24, 18, 0, 0)

        cls._setup_test_reports()
        with cls.mock_datetime_and_now(cls, cls.reference_now):
            # create with admin to force create_date
            cls.test_event = cls.env['event.event'].create({
                'name': 'TestEventMail',
                'user_id': cls.user_eventmanager.id,
                'date_begin': cls.event_date_begin,
                'date_end': cls.event_date_end,
                'event_mail_ids': [
                    (0, 0, {  # right at subscription
                        'interval_unit': 'now',
                        'interval_type': 'after_sub',
                        'template_ref': f'mail.template,{cls.template_subscription.id}',
                    }),
                    (0, 0, {  # one hour after subscription
                        'interval_nbr': 1,
                        'interval_unit': 'hours',
                        'interval_type': 'after_sub',
                        'template_ref': f'mail.template,{cls.template_subscription.id}',
                    }),
                    (0, 0, {  # 1 days before event
                        'interval_nbr': 1,
                        'interval_unit': 'days',
                        'interval_type': 'before_event',
                        'template_ref': f'mail.template,{cls.template_reminder.id}',
                    }),
                    (0, 0, {  # immediately after event
                        'interval_nbr': 1,
                        'interval_unit': 'hours',
                        'interval_type': 'after_event',
                        'template_ref': f'mail.template,{cls.template_reminder.id}',
                    }),
                ]
            })

    def test_assert_initial_values(self):
        """ Ensure base values for tests """
        test_event = self.test_event

        # event data
        self.assertEqual(test_event.create_date, self.reference_now)

        # check subscription scheduler
        after_sub_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub'), ('interval_unit', '=', 'now')])
        self.assertEqual(len(after_sub_scheduler), 1, 'event: wrong scheduler creation')
        self.assertEqual(after_sub_scheduler.scheduled_date, test_event.create_date.replace(microsecond=0))
        self.assertEqual(after_sub_scheduler.mail_state, 'running')
        self.assertEqual(after_sub_scheduler.mail_count_done, 0)
        after_sub_scheduler_2 = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub'), ('interval_unit', '=', 'hours')])
        self.assertEqual(len(after_sub_scheduler_2), 1, 'event: wrong scheduler creation')
        self.assertEqual(after_sub_scheduler_2.scheduled_date, test_event.create_date.replace(microsecond=0) + relativedelta(hours=1))
        self.assertEqual(after_sub_scheduler_2.mail_state, 'running')
        self.assertEqual(after_sub_scheduler_2.mail_count_done, 0)
        # check before event scheduler
        event_prev_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')])
        self.assertEqual(len(event_prev_scheduler), 1, 'event: wrong scheduler creation')
        self.assertEqual(event_prev_scheduler.scheduled_date, self.event_date_begin + relativedelta(days=-1))
        self.assertFalse(event_prev_scheduler.mail_done)
        self.assertEqual(event_prev_scheduler.mail_state, 'scheduled')
        self.assertEqual(event_prev_scheduler.mail_count_done, 0)
        # check after event scheduler
        event_next_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_event')])
        self.assertEqual(len(event_next_scheduler), 1, 'event: wrong scheduler creation')
        self.assertEqual(event_next_scheduler.scheduled_date, self.event_date_end + relativedelta(hours=1))
        self.assertFalse(event_next_scheduler.mail_done)
        self.assertEqual(event_next_scheduler.mail_state, 'scheduled')
        self.assertEqual(event_next_scheduler.mail_count_done, 0)

    @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
    @users('user_eventmanager')
    def test_event_mail_schedule(self):
        """ Test mail scheduling for events """
        test_event = self.test_event.with_env(self.env)
        now = self.reference_now
        schedulers = self.env['event.mail'].search([('event_id', '=', test_event.id)])
        after_sub_scheduler = schedulers.filtered(lambda s: s.interval_type == 'after_sub' and s.interval_unit == 'now')
        after_sub_scheduler_2 = schedulers.filtered(lambda s: s.interval_type == 'after_sub' and s.interval_unit == 'hours')
        event_prev_scheduler = schedulers.filtered(lambda s: s.interval_type == 'before_event')
        event_next_scheduler = schedulers.filtered(lambda s: s.interval_type == 'after_event')

        # check iterative work, update params to check call count
        batch_size, render_limit = 2, 10
        self.env['ir.config_parameter'].sudo().set_param('mail.batch_size', batch_size)
        self.env['ir.config_parameter'].sudo().set_param('mail.render.cron.limit', render_limit)

        # create some registrations
        EventMailRegistration = type(self.env['event.mail.registration'])
        exec_origin = EventMailRegistration._execute_on_registrations
        with patch.object(
                EventMailRegistration, '_execute_on_registrations', autospec=True, wraps=EventMailRegistration, side_effect=exec_origin,
             ) as mock_exec, \
             self.mock_datetime_and_now(now), self.mock_mail_gateway(), \
             self.capture_triggers('event.event_mail_scheduler') as capture:
            attendees = self.env['event.registration'].with_user(self.user_eventuser).create([
                {
                    'event_id': test_event.id,
                    'name': f'Reg{idx}',
                    'email': f'reg1{idx}@example.com',
                } for idx in range(15)] + [{
                    'event_id': test_event.id,
                    'name': 'RegDraft',
                    'email': 'reg_draft@example.com',
                    'state': 'draft',
                }, {
                    'event_id': test_event.id,
                    'name': 'RegCancel',
                    'email': 'reg_cancel@example.com',
                    'state': 'cancel',
                }
            ])

        # iterative check
        self.assertEqual(
            mock_exec.call_count, 5,
            "Should have called 5 times execution (batch of 2 until 10 registrations)"
        )

        # REGISTRATIONS / PRE SCHEDULERS
        # --------------------------------------------------

        # check registration state
        self.assertTrue(all(reg.state == 'open' for reg in attendees[:15]), 'Registrations: should be auto-confirmed')
        self.assertListEqual(attendees[15:].mapped('state'), ['draft', 'cancel'])
        self.assertTrue(all(reg.create_date == now for reg in attendees), 'Registrations: should have open date set to confirm date')

        # verify that subscription scheduler was auto-executed after each registration
        self.assertEqual(
            len(after_sub_scheduler.mail_registration_ids), 15,
            'Should have 15 scheduled communication (1 / registration), as we schedule more'
            'than cron limit as create should be quick.')
        for idx, mail_registration in enumerate(after_sub_scheduler.mail_registration_ids):
            self.assertEqual(mail_registration.scheduled_date, now.replace(microsecond=0))
            if idx < 10:
                self.assertTrue(mail_registration.mail_sent, 'event: registration mail should be sent at registration creation')
            else:
                self.assertFalse(mail_registration.mail_sent, 'event: registration mail should be scheduled, too much for limit')
        self.assertEqual(after_sub_scheduler.mail_state, 'running')
        self.assertEqual(after_sub_scheduler.mail_count_done, 10, 'event: not all subscription mails should have been sent as too much for limit')

        # cron should have been triggered for the remaining registrations
        self.assertSchedulerCronTriggers(capture, [now])

        # check emails effectively sent
        self.assertEqual(len(self._new_mails), 10, 'event: should have 10 scheduled emails (1 / executed registration)')
        self.assertMailMailWEmails(
            [formataddr((reg.name, reg.email)) for reg in attendees[:10]],
            'outgoing',
            content=None,
            fields_values={
                'email_from': self.user_eventmanager.company_id.email_formatted,
                'subject': f'Confirmation for {test_event.name}',
            })

        # same for second scheduler: scheduled but not sent
        self.assertEqual(
            len(after_sub_scheduler_2.mail_registration_ids), 15,
            'Should have 15 scheduled communication (1 / registration)')
        for mail_registration in after_sub_scheduler_2.mail_registration_ids:
            self.assertEqual(mail_registration.scheduled_date, now.replace(microsecond=0) + relativedelta(hours=1))
            self.assertFalse(mail_registration.mail_sent, 'event: registration mail should be scheduled, not sent')
        self.assertEqual(after_sub_scheduler_2.mail_count_done, 0, 'event: all subscription mails should be scheduled, not sent')

        # RE-RUN SCHEDULER TO COMPLETE SENDING
        # --------------------------------------------------

        with patch.object(
                EventMailRegistration, '_execute_on_registrations', autospec=True, wraps=EventMailRegistration, side_effect=exec_origin,
             ) as mock_exec, \
             self.mock_datetime_and_now(now), self.mock_mail_gateway(), \
             self.capture_triggers('event.event_mail_scheduler') as capture:
            self.event_cron_id.method_direct_trigger()

        # iterative check
        self.assertEqual(
            mock_exec.call_count, 3,
            "Should have called 3 times execution (batch of 2 with 5 registrations left = 3 iterations)"
        )

        # verify that subscription scheduler was auto-executed after each registration
        self.assertEqual(len(after_sub_scheduler.mail_registration_ids), 15)
        for mail_registration in after_sub_scheduler.mail_registration_ids:
            self.assertEqual(mail_registration.scheduled_date, now.replace(microsecond=0))
            self.assertTrue(mail_registration.mail_sent)
        self.assertEqual(after_sub_scheduler.mail_state, 'running')
        self.assertEqual(
            after_sub_scheduler.mail_count_done, 15,
            'Should have sent all mails, as cron limit is set to 20'
        )

        # check emails effectively sent
        self.assertEqual(len(self._new_mails), 5, 'event: should have 5 scheduled emails (1 / executed registration)')
        self.assertMailMailWEmails(
            [formataddr((reg.name, reg.email)) for reg in attendees[10:15]],
            'outgoing',
            content=None,
            fields_values={
                'email_from': self.user_eventmanager.company_id.email_formatted,
                'subject': f'Confirmation for {test_event.name}',
            })

        # SECOND ATTENDEE-BASED SCHEDULER (LATER) - UPDATE ITERATIVE
        # --------------------------------------------------

        # check default behavior, batch of 50 to run up to 1000 attendees
        self.env['ir.config_parameter'].sudo().set_param('mail.batch_size', False)
        self.env['ir.config_parameter'].sudo().set_param('mail.render.cron.limit', False)

        # execute event reminder scheduler explicitly, before scheduled date -> should not do anything
        with self.mock_datetime_and_now(now), self.mock_mail_gateway():
            after_sub_scheduler_2.execute()
        self.assertFalse(any(mail_reg.mail_sent for mail_reg in after_sub_scheduler_2.mail_registration_ids))
        self.assertEqual(after_sub_scheduler_2.mail_count_done, 0)
        self.assertEqual(len(self._new_mails), 0, 'event: should not send mails before scheduled date')

        # execute event reminder scheduler, right at scheduled date -> should sent mails
        now_registration = now + relativedelta(hours=1)
        with patch.object(
                EventMailRegistration, '_execute_on_registrations', autospec=True, wraps=EventMailRegistration, side_effect=exec_origin,
             ) as mock_exec, \
             self.mock_datetime_and_now(now_registration), self.mock_mail_gateway(), \
             self.capture_triggers('event.event_mail_scheduler') as capture:
            self.event_cron_id.method_direct_trigger()

        # iterative check
        self.assertEqual(
            mock_exec.call_count, 1,
            "Should have called 1 times execution (batch of 50 with 15 registrations = 1 iteration)"
        )

        # verify that subscription scheduler was auto-executed after each registration
        self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 15, 'event: should have 15 scheduled communication (1 / registration)')
        self.assertTrue(all(mail_reg.mail_sent for mail_reg in after_sub_scheduler_2.mail_registration_ids))
        self.assertEqual(after_sub_scheduler_2.mail_state, 'running')
        self.assertEqual(after_sub_scheduler_2.mail_count_done, 15,
                         'All subscriptions emails should have been sent')

        # check emails effectively sent
        self.assertEqual(len(self._new_mails), 15, 'event: should have 15 scheduled emails (1 / registration)')
        self.assertMailMailWEmails(
            [formataddr((reg.name, reg.email)) for reg in attendees[:15]],
            'outgoing',
            content=None,
            fields_values={
                'email_from': self.user_eventmanager.company_id.email_formatted,
                'subject': f'Confirmation for {test_event.name}',
            })

        # PRE SCHEDULERS (MOVE FORWARD IN TIME)
        # --------------------------------------------------

        self.assertFalse(event_prev_scheduler.mail_done)
        self.assertEqual(event_prev_scheduler.mail_state, 'scheduled')

        # simulate cron running before scheduled date -> should not do anything
        now_start = self.event_date_begin + relativedelta(hours=-25, microsecond=654321)
        with self.mock_datetime_and_now(now_start), self.mock_mail_gateway():
            self.event_cron_id.method_direct_trigger()

        self.assertFalse(event_prev_scheduler.mail_done)
        self.assertEqual(event_prev_scheduler.mail_state, 'scheduled')
        self.assertEqual(event_prev_scheduler.mail_count_done, 0)
        self.assertEqual(len(self._new_mails), 0)

        # execute cron to run schedulers after scheduled date
        now_start = self.event_date_begin + relativedelta(hours=-23, microsecond=654321)
        with self.mock_datetime_and_now(now_start), self.mock_mail_gateway():
            self.event_cron_id.method_direct_trigger()

        # check that scheduler is finished
        self.assertTrue(event_prev_scheduler.mail_done, 'event: reminder scheduler should have run')
        self.assertEqual(event_prev_scheduler.mail_state, 'sent', 'event: reminder scheduler should have run')

        # check emails effectively sent
        self.assertEqual(len(self._new_mails), 15, 'event: should have scheduled 15 mails (1 / registration)')
        self.assertMailMailWEmails(
            [formataddr((reg.name, reg.email)) for reg in attendees[:15]],
            'outgoing',
            content=None,
            fields_values={
                'email_from': self.user_eventmanager.company_id.email_formatted,
                'subject': f'Reminder for {test_event.name}: tomorrow',
            })

        # NEW REGISTRATION EFFECT ON SCHEDULERS
        # --------------------------------------------------

        with self.mock_datetime_and_now(now_start), self.mock_mail_gateway():
            new_attendee = self.env['event.registration'].create({
                'event_id': test_event.id,
                'name': 'Reg3',
                'email': 'reg3@example.com',
                'state': 'draft',
            })

        # no more seats
        self.assertEqual(new_attendee.state, 'draft')

        # schedulers state untouched
        self.assertTrue(event_prev_scheduler.mail_done)
        self.assertFalse(event_next_scheduler.mail_done)

        # confirm registration -> should trigger registration schedulers
        # NOTE: currently all schedulers are based on create_date
        # meaning several communications may be sent in the time time
        with self.mock_datetime_and_now(now_start + relativedelta(hours=1)), self.mock_mail_gateway():
            new_attendee.action_confirm()

        # verify that subscription scheduler was auto-executed after new registration confirmed
        self.assertEqual(len(after_sub_scheduler.mail_registration_ids), 16, 'event: should have 16 scheduled communication (1 / registration)')
        new_mail_reg = after_sub_scheduler.mail_registration_ids.filtered(lambda mail_reg: mail_reg.registration_id == new_attendee)
        self.assertEqual(new_mail_reg.scheduled_date, now_start.replace(microsecond=0))
        self.assertTrue(new_mail_reg.mail_sent, 'event: registration mail should be sent at registration creation')
        self.assertEqual(after_sub_scheduler.mail_count_done, 16,
                         'event: all subscription mails should have been sent')
        # verify that subscription scheduler was auto-executed after new registration confirmed
        self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 16, 'event: should have 16 scheduled communication (1 / registration)')
        new_mail_reg = after_sub_scheduler_2.mail_registration_ids.filtered(lambda mail_reg: mail_reg.registration_id == new_attendee)
        self.assertEqual(new_mail_reg.scheduled_date, now_start.replace(microsecond=0) + relativedelta(hours=1))
        self.assertTrue(new_mail_reg.mail_sent, 'event: registration mail should be sent at registration creation')
        self.assertEqual(after_sub_scheduler_2.mail_count_done, 16,
                         'event: all subscription mails should have been sent')

        # check emails effectively sent
        self.assertEqual(len(self._new_mails), 2, 'event: should have 1 scheduled emails (new registration only)')
        # manual check because 2 identical mails are sent and mail tools do not support it easily
        for mail in self._new_mails:
            self.assertEqual(mail.email_from, self.user_eventmanager.company_id.email_formatted)
            self.assertEqual(mail.subject, f'Confirmation for {test_event.name}')
            self.assertEqual(mail.state, 'outgoing')
            self.assertEqual(mail.email_to, formataddr((new_attendee.name, new_attendee.email)))

        # POST SCHEDULERS (MOVE FORWARD IN TIME)
        # --------------------------------------------------

        self.assertFalse(event_next_scheduler.mail_done)

        # execute event reminder scheduler explicitly after its schedule date
        new_end = self.event_date_end + relativedelta(hours=2)
        with self.mock_datetime_and_now(new_end), self.mock_mail_gateway():
            (attendees + new_attendee).invalidate_recordset(['event_date_range'])
            self.event_cron_id.method_direct_trigger()

        # check that scheduler is finished
        self.assertTrue(event_next_scheduler.mail_done, 'event: reminder scheduler should should have run')
        self.assertEqual(event_next_scheduler.mail_state, 'sent', 'event: reminder scheduler should have run')
        self.assertEqual(event_next_scheduler.mail_count_done, 16)

        # check emails effectively sent
        self.assertEqual(len(self._new_mails), 16, 'event: should have scheduled 3 mails, one for each registration')
        self.assertMailMailWEmails(
        [formataddr((reg.name, reg.email)) for reg in attendees[:15] + new_attendee],
            'outgoing',
            content=None,
            fields_values={
                'email_from': self.user_eventmanager.company_id.email_formatted,
                'subject': f"Reminder for {test_event.name}: today",
            })

    @mute_logger('odoo.addons.event.models.event_mail')
    @users('user_eventmanager')
    def test_event_mail_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")
        self.assertTrue(before_scheduler)
        self._create_registrations(self.test_event, 2)

        def _patched_send_mail(self, *args, **kwargs):
            raise exceptions.ValidationError('Some error')

        with patch.object(type(self.env["mail.compose.message"]), "_action_send_mail_mass_mail", _patched_send_mail), \
             self.mock_datetime_and_now(self.reference_now + relativedelta(days=3)), \
             self.mock_mail_gateway():
            cron.method_direct_trigger()
        self.assertFalse(before_scheduler.mail_done)

    @users('user_eventmanager')
    def test_event_mail_schedule_fail_global_no_registrations(self):
        """ Be sure no registrations = no crash in composer """
        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")

        self.test_event.registration_ids.unlink()
        with self.mock_datetime_and_now(self.reference_now + relativedelta(days=3)), \
             self.mock_mail_gateway():
            cron.method_direct_trigger()
        self.assertTrue(before_scheduler.mail_done)

    @mute_logger(
        'odoo.addons.event.models.event_mail',
        'odoo.addons.event.models.event_mail_registration',
        'odoo.addons.event.models.event_registration',
    )
    def test_event_mail_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" and s.interval_unit == "now")
        self.assertTrue(onsub_scheduler)
        self.assertEqual(onsub_scheduler.mail_count_done, 0)

        def _patched_send_mail(self, *args, **kwargs):
            raise exceptions.ValidationError('Some error')

        with patch.object(type(self.env["mail.compose.message"]), "_action_send_mail_mass_mail", _patched_send_mail), \
             self.mock_mail_gateway():
            registration = self.env['event.registration'].with_user(self.user_eventmanager).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, 0)

    @mute_logger('odoo.addons.event.models.event_mail')
    @users('user_eventmanager')
    def test_event_mail_schedule_fail_registration_template_removed(self):
        """ Test flow where scheduler fails due to template being removed. """
        after_sub_scheduler = self.test_event.event_mail_ids.filtered(lambda s: s.interval_type == 'after_sub')
        self.assertTrue(after_sub_scheduler)
        self.template_subscription.sudo().unlink()
        self.assertFalse(after_sub_scheduler.exists(), "When removing template, scheduler should be removed")

    @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
    @users('user_eventmanager')
    @warmup
    def test_event_mail_schedule_on_subscription(self):
        """ Test emails sent on subscription, notably to avoid bottlenecks """
        test_event = self.test_event.with_env(self.env)
        reference_now = self.reference_now

        # remove on subscription, to create hanging registrations
        schedulers = self.env['event.mail'].search([('event_id', '=', test_event.id)])
        _sub_scheduler = schedulers.filtered(lambda s: s.interval_type == 'after_sub' and s.interval_unit == 'now')
        _sub_scheduler.unlink()

        # consider having hanging registrations, still not processed (e.g. adding
        # a new scheduler after)
        self.env.invalidate_all()
        # event 19
        with self.assertQueryCount(37), self.mock_datetime_and_now(reference_now), \
             self.mock_mail_gateway():
            _existing = self.env['event.registration'].create([
                {
                    'email': f'existing.attendee.{idx}@test.example.com',
                    'event_id': test_event.id,
                    'name': f'Attendee {idx}',
                } for idx in range(5)
            ])
        self.assertEqual(len(self._new_mails), 0)
        self.assertEqual(self.mail_mail_create_mocked.call_count, 0)

        # add on subscription scheduler, then new registrations ! yay ! check what
        # happens with old ones
        test_event.write({'event_mail_ids': [
            (0, 0, {  # right at subscription
                'interval_unit': 'now',
                'interval_type': 'after_sub',
                'template_ref': f'mail.template,{self.template_subscription.id}',
            }),
        ]})
        self.env.invalidate_all()
        # event 50
        with self.assertQueryCount(67), \
             self.mock_datetime_and_now(reference_now + relativedelta(minutes=10)), \
             self.mock_mail_gateway():
            _new = self.env['event.registration'].create([
                {
                    'email': f'new.attendee.{idx}@test.example.com',
                    'event_id': test_event.id,
                    'name': f'New Attendee {idx}',
                } for idx in range(2)
            ])
        self.assertEqual(len(self._new_mails), 2,
                         'EventMail: should be limited to new registrations')
        self.assertEqual(self.mail_mail_create_mocked.call_count, 1,
                         'EventMail: should create mails in batch for new registrations')

    @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
    @users('user_eventmanager')
    def test_event_mail_schedule_on_subscription_async(self):
        """ Async mode for schedulers activated, should not send communication
        in the same transaction. """
        test_event = self.test_event.with_env(self.env)
        cron = self.env.ref('event.event_mail_scheduler')
        reference_now = self.reference_now

        self.env['ir.config_parameter'].sudo().set_param('event.event_mail_async', True)
        with self.capture_triggers(cron.id) as capt, \
             self.mock_datetime_and_now(reference_now + relativedelta(minutes=10)), \
             self.mock_mail_gateway():
            existing = self.env['event.registration'].create([
                {
                    'email': f'new.async.attendee.{idx}@test.example.com',
                    'event_id': test_event.id,
                    'name': f'New Async Attendee {idx}',
                } for idx in range(5)
            ])
        self.assertEqual(len(self._new_mails), 0)
        self.assertEqual(self.mail_mail_create_mocked.call_count, 0)
        capt.records.ensure_one()
        self.assertEqual(capt.records.call_at, reference_now.replace(microsecond=0) + relativedelta(minutes=10))

        # run cron: emails should be send for registrations
        with self.mock_datetime_and_now(reference_now + relativedelta(minutes=10)), \
             self.mock_mail_gateway():
            cron.sudo().method_direct_trigger()
        self.assertMailMailWEmails(
            [formataddr((reg.name, reg.email)) for reg in existing],
            "outgoing",
            content=f"Hello your registration to {test_event.name} is confirmed",
            fields_values={
                'email_from': self.user_eventmanager.company_id.email_formatted,
                'subject': f'Confirmation for {test_event.name}',
            })

    @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
    def test_unique_event_mail_ids(self):
        # create event with default event_mail_ids lines
        test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
            'name': "TestEvent",
            'date_begin': datetime.now(),
            'date_end': datetime.now() + relativedelta(days=1),
            'seats_max': 2,
            'seats_limited': True,
        })

        event_mail_ids_initial = test_event.event_mail_ids
        self._create_registrations(test_event, 1)

        aftersub = test_event.event_mail_ids.filtered(lambda mail: mail.interval_type == "after_sub")
        self.assertTrue(aftersub)

        self.assertEqual(len(test_event.event_mail_ids), 3, "Should have 3 communication lines")
        self.assertEqual(aftersub.mail_count_done, 1, "Should have sent first mail immediately")

        # change the event type that has event_type_mail_ids having one identical and one non-identical configuration
        event_type = self.env['event.type'].create({
            'name': "Go Sports",
            'event_type_mail_ids': [
                (0, 0, {
                    'interval_nbr': 0,
                    'interval_unit': 'now',
                    'interval_type': 'after_sub',
                    'template_ref': 'mail.template,%i' % self.env['ir.model.data']._xmlid_to_res_id('event.event_subscription')
                }), (0, 0, {
                    'interval_nbr': 5,
                    'interval_unit': 'hours',
                    'interval_type': 'before_event',
                    'template_ref': 'mail.template,%i' % self.env['ir.model.data']._xmlid_to_res_id('event.event_reminder')
                }),
            ]
        })
        test_event.event_type_id = event_type

        self.assertTrue(aftersub in test_event.event_mail_ids, "Sent communication should not have been removed")
        mail_not_done = event_mail_ids_initial - aftersub
        self.assertFalse(test_event.event_mail_ids & mail_not_done, "Other default communication lines should have been removed")

        self.assertEqual(len(test_event.event_mail_ids), 2, "Should now have only two communication lines")
        mails_to_send = test_event.event_mail_ids - aftersub
        duplicate_mails = mails_to_send.filtered(lambda mail:
            mail.notification_type == 'mail' and\
            mail.interval_nbr == 0 and\
            mail.interval_unit == 'now' and\
            mail.interval_type == 'after_sub' and\
            mail.template_ref.id == self.env['ir.model.data']._xmlid_to_res_id('event.event_subscription'))

        self.assertEqual(len(duplicate_mails), 0,
            "The duplicate configuration (first one from event_type.event_type_mail_ids which has same configuration as the sent one) should not have been added")

    @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
    def test_archived_event_mail_schedule(self):
        """ Test mail scheduling for archived events """
        event_cron_id = self.env.ref('event.event_mail_scheduler')

        # deactivate other schedulers to avoid messing with crons
        self.env['event.mail'].search([]).unlink()

        # freeze some datetimes, and ensure more than 1D+1H before event starts
        # to ease time-based scheduler check
        now = datetime(2023, 7, 24, 14, 30, 15)
        event_date_begin = datetime(2023, 7, 26, 8, 0, 0)
        event_date_end = datetime(2023, 7, 28, 18, 0, 0)

        with self.mock_datetime_and_now(now):
            test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
                'name': 'TestEventMail',
                'date_begin': event_date_begin,
                'date_end': event_date_end,
                'event_mail_ids': [
                    (0, 0, {  # right at subscription
                        'interval_unit': 'now',
                        'interval_type': 'after_sub',
                        'template_ref': 'mail.template,%i' % self.env['ir.model.data']._xmlid_to_res_id('event.event_subscription')}),
                    (0, 0, {  # 3 hours before event
                        'interval_nbr': 3,
                        'interval_unit': 'hours',
                        'interval_type': 'before_event',
                        'template_ref': 'mail.template,%i' % self.env['ir.model.data']._xmlid_to_res_id('event.event_reminder')})
                ]
            })

        # check event scheduler
        scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id)])
        self.assertEqual(len(scheduler), 2, 'event: wrong scheduler creation')

        event_prev_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')])

        with self.mock_datetime_and_now(now), self.mock_mail_gateway():
            self.env['event.registration'].create({
                'event_id': test_event.id,
                'name': 'Reg1',
                'email': 'reg1@example.com',
            })
            self.env['event.registration'].create({
                'event_id': test_event.id,
                'name': 'Reg2',
                'email': 'reg2@example.com',
            })
        # check emails effectively sent
        self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)')

        # Archive the Event
        test_event.action_archive()

        # execute cron to run schedulers
        now_start = event_date_begin + relativedelta(hours=-3)
        with self.mock_datetime_and_now(now_start), self.mock_mail_gateway():
            event_cron_id.method_direct_trigger()

        # check that scheduler is not executed
        self.assertFalse(event_prev_scheduler.mail_done, 'event: reminder scheduler should should have run')