From 80224b7c57a0254742a1e1653fbc89bdffa134d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 7 Apr 2021 10:09:45 +0000 Subject: [PATCH 1/4] [MOV] mail, test_mail: merge and improve composer tests This commits is a bit a followup of odoo/odoo#62061 where new composer tests were added. This commit finalize this work by merging all composer related tests within the same file. It also backports some of tooling update done in odoo/odoo#64459 or odoo/odoo#63677 in order to have coherent naming through 14+ versions. Some additional or deeper tests are added. Notably it now more cleanly uses asserts for created mail.mail and sent emails. Task ID-2500615 COM PR odoo/odoo#68874 X-Original-Commit odoo/odoo@67f31cbfcdf9d6883f2c15418ab0ba4dc216be95 X-original-commit: e147ca2eb51a7008251f20f134bd021a1ae3705c --- addons/mail/tests/common.py | 50 +- addons/test_mail/tests/__init__.py | 1 - addons/test_mail/tests/test_mail_composer.py | 499 +++++++++++++++++- .../test_mail/tests/test_message_composer.py | 345 ------------ 4 files changed, 537 insertions(+), 358 deletions(-) delete mode 100644 addons/test_mail/tests/test_message_composer.py diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index d0ff1259e20..b28e04edd19 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -193,7 +193,7 @@ class MockEmail(common.BaseCase): if all(p in mail.recipient_ids for p in recipients): break else: - raise AssertionError('mail.mail not found for message %s / status %s / recipients %s' % (mail_message, status, recipients.ids)) + raise AssertionError('mail.mail not found for message %s / status %s / recipients %s / author %s' % (mail_message, status, recipients.ids, author)) return mail def _find_mail_mail_wemail(self, email_to, status, mail_message=None, author=None): @@ -222,15 +222,21 @@ class MockEmail(common.BaseCase): mail = self._find_mail(author, recipients, mail_message) self.assertEqual(mail.state, 'exception') - def assertMailMail(self, recipients, status, check_mail_mail=True, mail_message=None, author=None, email_values=None, fields_values=None): + def assertMailMail(self, recipients, status, + check_mail_mail=True, mail_message=None, author=None, + content=None, fields_values=None, email_values=None): if check_mail_mail: - mail = self._find_mail_mail_wpartners(recipients, status, mail_message=mail_message, author=author) - self.assertTrue(bool(mail)) + found_mail = self._find_mail_mail_wpartners(recipients, status, mail_message=mail_message, author=author) + self.assertTrue(bool(found_mail)) + if content: + self.assertIn(content, found_mail.body_html) for fname, fvalue in (fields_values or {}).items(): - self.assertEqual(mail[fname], fvalue) + self.assertEqual( + found_mail[fname], fvalue, + 'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname])) if status == 'sent': for recipient in recipients: - self.assertSentEmail(author, [recipient], **email_values) + self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [recipient], **(email_values or {})) def assertMailMailWEmails(self, emails, status, content, fields_values=None): """ Will check in self._new_mails to find a sent mail.mail. To use with @@ -302,6 +308,15 @@ class MockEmail(common.BaseCase): for val in ['reply_to', 'subject', 'references', 'attachments']: if val in expected: self.assertEqual(expected[val], sent_mail[val], 'Value for %s: expected %s, received %s' % (val, expected[val], sent_mail[val])) + if 'attachments_info' in values: + attachments = sent_mail['attachments'] + for attachment_info in values['attachments_info']: + attachment = next(attach for attach in attachments if attach[0] == attachment_info['name']) + if attachment_info.get('raw'): + self.assertEqual(attachment[1], attachment_info['raw']) + if attachment_info.get('type'): + self.assertEqual(attachment[2], attachment_info['type']) + self.assertEqual(len(values['attachments_info']), len(attachments)) for val in ['body']: if val in expected: self.assertHtmlEqual(expected[val], sent_mail[val], 'Value for %s: expected %s, received %s' % (val, expected[val], sent_mail[val])) @@ -655,6 +670,29 @@ class MailCase(MockEmail): return bus_notifs + def assertNotified(self, message, recipients_info, is_complete=False): + """ Lightweight check for notifications (mail.notification). + + :param recipients_info: list notified recipients: [ + {'partner': res.partner record (may be empty), + 'type': notification_type to check, + 'is_read': is_read to check, + }, {...}] + """ + notifications = self._new_notifs.filtered(lambda notif: notif in message.notification_ids) + if is_complete: + self.assertEqual(len(notifications), len(recipients_info)) + for rinfo in recipients_info: + recipient_notif = next( + (notif + for notif in notifications + if notif.res_partner_id == rinfo['partner'] + ), False + ) + self.assertTrue(recipient_notif) + self.assertEqual(recipient_notif.is_read, rinfo['is_read']) + self.assertEqual(recipient_notif.notification_type, rinfo['type']) + def assertTracking(self, message, data): tracking_values = message.sudo().tracking_value_ids for field_name, value_type, old_value, new_value in data: diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py index bd72e0bf019..15d441d888c 100644 --- a/addons/test_mail/tests/__init__.py +++ b/addons/test_mail/tests/__init__.py @@ -10,7 +10,6 @@ from . import test_mail_mail from . import test_mail_gateway from . import test_mail_template from . import test_mail_thread_internals -from . import test_message_composer from . import test_message_management from . import test_message_post from . import test_message_track diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 589f4105176..e22a875a5a6 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -3,7 +3,10 @@ import base64 +from unittest.mock import patch + from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.test_mail.models.test_mail_models import MailTestTicket from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients from odoo.tests import tagged from odoo.tests.common import users, Form @@ -18,9 +21,14 @@ class TestMailComposer(TestMailCommon, TestRecipients): super(TestMailComposer, cls).setUpClass() cls._init_mail_gateway() + # ensure employee can create partners, necessary for templates + cls.user_employee.write({ + 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], + }) + cls.user_employee_2 = mail_new_test_user( cls.env, login='employee2', groups='base.group_user', - notification_type='inbox', email='eglantine@example.com', + notification_type='email', email='eglantine@example.com', name='Eglantine Employee', signature='--\nEglantine') cls.partner_employee_2 = cls.user_employee_2.partner_id @@ -37,6 +45,7 @@ class TestMailComposer(TestMailCommon, TestRecipients): 'report_type': 'qweb-pdf', 'report_name': 'test_mail.mail_test_ticket_test_template', }) + cls.test_record_report = cls.test_report._render_qweb_pdf(cls.test_report.ids) cls.test_from = '"John Doe" ' @@ -54,9 +63,10 @@ class TestMailComposer(TestMailCommon, TestRecipients): 'body_html': '

TemplateBody ${object.name}

', 'partner_to': '${object.customer_id.id if object.customer_id else ""}', 'email_to': '${(object.email_from if not object.customer_id else "") | safe}', - 'email_from': '${object.user_id.email_formatted | safe}', + 'email_from': '${(object.user_id.email_formatted or user.email_formatted) | safe}', 'model_id': cls.env['ir.model']._get('mail.test.ticket').id, 'mail_server_id': cls.mail_server.id, + 'auto_delete': True, }) def _generate_attachments_data(self, count): @@ -102,7 +112,59 @@ class TestComposerForm(TestMailComposer): # self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name') self.assertFalse(composer_form.no_auto_thread) self.assertEqual(composer_form.composition_mode, 'comment') - self.assertEqual(composer_form.model, self.test_records._name) + self.assertEqual(composer_form.model, self.test_record._name) + + @users('employee') + def test_mail_composer_comment_attachments(self): + """Tests that all attachments are added to the composer, static attachments + are not duplicated and while reports are re-generated, and that intermediary + attachments are dropped.""" + attachment_data = self._generate_attachments_data(2) + template_1 = self.template.copy({ + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'report_name': 'TestReport for ${object.name}.html', # test cursor forces html + 'report_template': self.test_report.id, + }) + template_1_attachments = template_1.attachment_ids + self.assertEqual(len(template_1_attachments), 2) + template_2 = self.template.copy({ + 'attachment_ids': False, + 'report_template': self.test_report.id, + }) + + # begins without attachments + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=True, default_attachment_ids=[]) + )) + self.assertEqual(len(composer_form.attachment_ids), 0) + + # change template: 2 static (attachment_ids) and 1 dynamic (report) + composer_form.template_id = template_1 + self.assertEqual(len(composer_form.attachment_ids), 3) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, template_1_attachments) + + # change template: 0 static (attachment_ids) and 1 dynamic (report) + composer_form.template_id = template_2 + self.assertEqual(len(composer_form.attachment_ids), 1) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, self.env['ir.attachment']) + + # change back to template 1 + composer_form.template_id = template_1 + self.assertEqual(len(composer_form.attachment_ids), 3) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, template_1_attachments) + + # reset template + composer_form.template_id = self.env['mail.template'] + self.assertEqual(len(composer_form.attachment_ids), 0) @users('employee') def test_mail_composer_mass(self): @@ -139,6 +201,7 @@ class TestComposerInternals(TestMailComposer): attachment_data = self._generate_attachments_data(3) self.template.write({ 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'report_name': 'TestReport for ${object.name}.html', # test cursor forces html 'report_template': self.test_report.id, }) attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) @@ -161,6 +224,7 @@ class TestComposerInternals(TestMailComposer): self.assertIn(attach, composer.attachment_ids) generated = composer.attachment_ids - attachs self.assertEqual(len(generated), 1, 'MailComposer: should have 1 additional attachment for report') + self.assertEqual(generated.name, 'TestReport for %s.html' % self.test_record.name) self.assertEqual(generated.res_model, 'mail.compose.message') self.assertEqual(generated.res_id, 0) @@ -207,9 +271,9 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.email_from, self.test_from) @users('employee') - def test_mail_composer_content_comment(self): - """ Test content management (subject, body, server) in comment mode. - Template update is also tested. """ + def test_mail_composer_content(self): + """ Test content management (subject, body, server) in both comment and + mass mailing mode. Template update is also tested. """ for composition_mode in ['comment', 'mass_mail']: if composition_mode == 'comment': ctx = self._get_web_context(self.test_record, add_web=False) @@ -314,3 +378,426 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.subject, 'My amazing subject') self.assertEqual(composer.body, '

Test Body

') self.assertEqual(composer.mail_server_id.id, False) + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_parent(self): + """ Test specific management in comment mode when having parent_id set: + record_name, subject, parent's partners. """ + parent = self.test_record.message_post(body='Test', partner_ids=(self.partner_1 + self.partner_2).ids) + + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=False, default_parent_id=parent.id) + ).create({ + 'body': '

Test Body

', + }) + + # creation values taken from parent + self.assertEqual(composer.subject, 'Re: %s' % self.test_record.name) + self.assertEqual(composer.body, '

Test Body

') + self.assertEqual(composer.partner_ids, self.partner_1 + self.partner_2) + + def test_mail_composer_rights_portal(self): + portal_user = self._create_portal_user() + + with patch.object(MailTestTicket, 'check_access_rights', return_value=True): + self.env['mail.compose.message'].with_user(portal_user).with_context( + self._get_web_context(self.test_record) + ).create({ + 'subject': 'Subject', + 'body': '

Body text

', + 'partner_ids': [] + }).send_mail() + + self.assertEqual(self.test_record.message_ids[0].body, '

Body text

') + self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) + + self.env['mail.compose.message'].with_user(portal_user).with_context({ + 'default_composition_mode': 'comment', + 'default_parent_id': self.test_record.message_ids.ids[0], + }).create({ + 'subject': 'Subject', + 'body': '

Body text 2

' + }).send_mail() + + self.assertEqual(self.test_record.message_ids[0].body, '

Body text 2

') + self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) + + @users('employee') + def test_mail_composer_save_template(self): + self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=False) + ).create({ + 'subject': 'Template Subject', + 'body': '

Template Body

', + }).save_as_template() + + # Test: email_template subject, body_html, model + template = self.env['mail.template'].search([ + ('model', '=', self.test_record._name), + ('subject', '=', 'Template Subject') + ], limit=1) + self.assertEqual(template.name, "%s: %s" % (self.env['ir.model']._get(self.test_record._name).name, 'Template Subject')) + self.assertEqual(template.body_html, '

Template Body

', 'email_template incorrect body_html') + + +@tagged('mail_composer') +class TestComposerResultsComment(TestMailComposer): + """ Test global output of composer used in comment mode. Test notably + notification and emails generated during this process. """ + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_recipients(self): + """ Test partner_ids given to composer are given to the final message. """ + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record) + ).create({ + 'body': '

Test Body

', + 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] + }) + composer.send_mail() + + message = self.test_record.message_ids[0] + self.assertEqual(message.body, '

Test Body

') + self.assertEqual(message.author_id, self.user_employee.partner_id) + self.assertEqual(message.subject, 'Re: %s' % self.test_record.name) + self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) + self.assertEqual(message.partner_ids, self.partner_1 | self.partner_2) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_complete(self): + """ Test a posting process using a complex template, holding several + additional recipients and attachments. + + This tests notifies: 2 new email_to (+ 1 duplicated), 1 email_cc, + test_record followers and partner_admin added in partner_to.""" + attachment_data = self._generate_attachments_data(2) + email_to_1 = 'test.to.1@test.example.com' + email_to_2 = 'test.to.2@test.example.com' + email_to_3 = 'test.to.1@test.example.com' # duplicate: should not sent twice the email + email_cc_1 = 'test.cc.1@test.example.com' + self.template.write({ + 'auto_delete': False, # keep sent emails to check content + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'email_to': '%s, %s, %s' % (email_to_1, email_to_2, email_to_3), + 'email_cc': email_cc_1, + 'partner_to': '%s, ${object.customer_id.id if object.customer_id else ""}' % self.partner_admin.id, + 'report_name': 'TestReport for ${object.name}', # test cursor forces html + 'report_template': self.test_report.id, + }) + attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) + self.assertEqual(len(attachs), 2) + + # ensure initial data + self.assertEqual(self.test_record.user_id, self.user_employee_2) + self.assertEqual(self.test_record.message_partner_ids, self.partner_employee_2) + + # open a composer and run it in comment mode + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=True, + default_template_id=self.template.id) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + composer.send_mail() + + # check new partners have been created based on emails given + new_partners = self.env['res.partner'].search([ + ('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1]) + ]) + self.assertEqual(len(new_partners), 3) + self.assertEqual(set(new_partners.mapped('email')), + set(['test.to.1@test.example.com', 'test.to.2@test.example.com', 'test.cc.1@test.example.com']) + ) + + # global outgoing: one mail.mail (all customer recipients, then all employee recipients) + # and 5 emails, and 1 inbox notification (admin) + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail') + self.assertEqual(len(self._mails), 5, 'Should have sent 5 emails, one per recipient') + + # template is sent only to partners (email_to are transformed) + message = self.test_record.message_ids[0] + self.assertMailMail(self.partner_employee_2, 'sent', + check_mail_mail=True, + mail_message=message, + author=self.partner_employee, # author != email_from (template sets only email_from) + email_values={ + 'body_content': 'TemplateBody %s' % self.test_record.name, + 'email_from': self.test_record.user_id.email_formatted, # set by template + 'subject': 'TemplateSubject %s' % self.test_record.name, + 'attachments_info': [ + {'name': '00.txt', 'raw': b'Att00', 'type': 'text/plain'}, + {'name': '01.txt', 'raw': b'Att01', 'type': 'text/plain'}, + {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, + ] + }, + fields_values={}, + ) + self.assertMailMail(self.test_record.customer_id + new_partners, 'sent', + check_mail_mail=True, + mail_message=message, + author=self.partner_employee, # author != email_from (template sets only email_from) + email_values={ + 'body_content': 'TemplateBody %s' % self.test_record.name, + 'email_from': self.test_record.user_id.email_formatted, # set by template + 'subject': 'TemplateSubject %s' % self.test_record.name, + 'attachments_info': [ + {'name': '00.txt', 'raw': b'Att00', 'type': 'text/plain'}, + {'name': '01.txt', 'raw': b'Att01', 'type': 'text/plain'}, + {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, + ] + }, + fields_values={}, + ) + + # message is posted and notified admin + self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) + self.assertNotified(message, [{'partner': self.partner_admin, 'is_read': False, 'type': 'inbox'}]) + # attachments are copied on message and linked to document + self.assertEqual( + set(message.attachment_ids.mapped('name')), + set(['00.txt', '01.txt', 'TestReport for %s.html' % self.test_record.name]) + ) + self.assertEqual(set(message.attachment_ids.mapped('res_model')), set([self.test_record._name])) + self.assertEqual(set(message.attachment_ids.mapped('res_id')), set(self.test_record.ids)) + self.assertTrue(all(attach not in message.attachment_ids for attach in attachs), 'Should have copied attachments') + + +@tagged('mail_composer') +class TestComposerResultsMass(TestMailComposer): + + @classmethod + def setUpClass(cls): + super(TestComposerResultsMass, cls).setUpClass() + # ensure employee can create partners, necessary for templates + cls.user_employee.write({ + 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], + }) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl(self): + self.template.auto_delete = False # keep sent emails to check content + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + + for record in self.test_records: + # message copy is kept + message = record.message_ids[0] + + # template is sent directly using customer field, meaning we have recipients + self.assertMailMail(record.customer_id, 'sent', check_mail_mail=True, + mail_message=message, + author=self.partner_employee) + + # message content + self.assertEqual(message.subject, 'TemplateSubject %s' % record.name) + self.assertEqual(message.body, '

TemplateBody %s

' % record.name) + self.assertEqual(message.author_id, self.user_employee.partner_id) + # post-related fields are void + self.assertEqual(message.subtype_id, self.env['mail.message.subtype']) + self.assertEqual(message.partner_ids, self.env['res.partner']) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_complete(self): + """ Test a composer in mass mode with a quite complete template, containing + notably email-based recipients and attachments. """ + attachment_data = self._generate_attachments_data(2) + email_to_1 = 'test.to.1@test.example.com' + email_to_2 = 'test.to.2@test.example.com' + email_to_3 = 'test.to.1@test.example.com' # duplicate: should not sent twice the email + email_cc_1 = 'test.cc.1@test.example.com' + self.template.write({ + 'auto_delete': False, # keep sent emails to check content + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'email_to': '%s, %s, %s' % (email_to_1, email_to_2, email_to_3), + 'email_cc': email_cc_1, + 'partner_to': '%s, ${object.customer_id.id if object.customer_id else ""}' % self.partner_admin.id, + 'report_name': 'TestReport for ${object.name}', # test cursor forces html + 'report_template': self.test_report.id, + }) + attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) + self.assertEqual(len(attachs), 2) + + # ensure initial data + self.assertEqual(self.test_records.user_id, self.env['res.users']) + self.assertEqual(self.test_records.message_partner_ids, self.env['res.partner']) + + # launch composer in mass mode + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False): + composer.send_mail() + + new_partners = self.env['res.partner'].search([ + ('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1]) + ]) + self.assertEqual(len(new_partners), 3) + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 10, 'Should have sent 5 emails per record') + + # hack to use assertEmails: filtering on from/to only is not sufficient to distinguish emails + _mails_records = [ + [mail for mail in self._mails if '%s-%s' % (record.id, record._name) in mail['message_id']] + for record in self.test_records + ] + _mails_record2 = [mail for mail in self._mails if '%s-%s' % (self.test_records[1].id, self.test_records._name) in mail['message_id']] + + for record, _mails in zip(self.test_records, _mails_records): + # message copy is kept + message = record.message_ids[0] + + # template is sent only to partners (email_to are transformed) + self._mails = _mails + self.assertMailMail(record.customer_id + new_partners + self.partner_admin, + 'sent', + check_mail_mail=True, + mail_message=message, + author=self.partner_employee, + email_values={ + 'body_content': 'TemplateBody %s' % record.name, + 'subject': 'TemplateSubject %s' % record.name, + # 'attachments': [ + # ('00.txt', b'Att00', 'text/plain'), + # ('01.txt', b'Att01', 'text/plain'), + # ('report.test_mail.mail_test_ticket_test_template.html', b'My second attachment', 'text/plain') + # ] + } + ) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_delete(self): + self.template.auto_delete = True + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records') + + for record in self.test_records: + # message copy is kept + message = record.message_ids[0] + + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + # message content + self.assertEqual(message.subject, 'TemplateSubject %s' % record.name) + self.assertEqual(message.body, '

TemplateBody %s

' % record.name) + self.assertEqual(message.author_id, self.user_employee.partner_id) + # post-related fields are void + self.assertEqual(message.subtype_id, self.env['mail.message.subtype']) + self.assertEqual(message.partner_ids, self.env['res.partner']) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_delete_notif(self): + self.template.auto_delete = True + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id, + default_auto_delete_message=True) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records') + + for record in self.test_records: + # message copy is unlinked + self.assertEqual(record.message_ids, self.env['mail.message'], 'Should have deleted mail.message records') + + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_recipients(self): + """ Test various combinations of recipients: active_domain, active_id, + active_ids, ... to ensure fallback behavior are working. """ + # 1: active_domain + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id, + active_ids=[], + default_use_active_domain=True, + default_active_domain=[('id', 'in', self.test_records.ids)]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + + for record in self.test_records: + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + # 2: active_domain not taken into account if use_active_domain is False + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id, + default_use_active_domain=False, + default_active_domain=[('id', 'in', -1)]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + + # 3: fallback on active_id if not active_ids + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id, + active_ids=[]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 1, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 1, 'Should have sent 1 email per record') + + # 3: void is void + composer_form = Form(self.env['mail.compose.message'].with_context( + default_model='mail.test.ticket', + default_template_id=self.template.id + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False), self.assertRaises(ValueError): + composer.send_mail() diff --git a/addons/test_mail/tests/test_message_composer.py b/addons/test_mail/tests/test_message_composer.py deleted file mode 100644 index ea6aa91803f..00000000000 --- a/addons/test_mail/tests/test_message_composer.py +++ /dev/null @@ -1,345 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import base64 - -from unittest.mock import patch - -from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients -from odoo.addons.test_mail.models.test_mail_models import MailTestSimple -from odoo.tests import tagged -from odoo.tools import mute_logger - - -@tagged('mail_composer') -class TestComposer(TestMailCommon, TestRecipients): - """ Test Composer integration and results """ - - @classmethod - def setUpClass(cls): - super(TestComposer, cls).setUpClass() - cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) - cls._reset_mail_context(cls.test_record) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_comment(self): - composer = self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'comment', - 'default_model': self.test_record._name, - 'default_res_id': self.test_record.id, - }).with_user(self.user_employee).create({ - 'body': '

Test Body

', - 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] - }) - composer.send_mail() - - message = self.test_record.message_ids[0] - self.assertEqual(message.body, '

Test Body

') - self.assertEqual(message.author_id, self.user_employee.partner_id) - self.assertEqual(message.subject, 'Re: %s' % self.test_record.name) - self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) - self.assertEqual(message.partner_ids, self.partner_1 | self.partner_2) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_comment_parent(self): - parent = self.test_record.message_post(body='Test') - - self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'comment', - 'default_parent_id': parent.id - }).with_user(self.user_employee).create({ - 'body': '

Mega

', - }).send_mail() - - message = self.test_record.message_ids[0] - self.assertEqual(message.body, '

Mega

') - self.assertEqual(message.parent_id, parent) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_mass_mail(self): - test_record_2 = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test2'}) - - composer = self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'mass_mail', - 'default_model': self.test_record._name, - 'default_res_id': False, - 'active_ids': [self.test_record.id, test_record_2.id] - }).with_user(self.user_employee).create({ - 'subject': 'Testing ${object.name}', - 'body': '

${object.name}

', - 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] - }) - composer.with_context({ - 'default_res_id': -1, - 'active_ids': [self.test_record.id, test_record_2.id] - }).send_mail() - - # check mail_mail - mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'Testing')]) - for mail in mails: - self.assertEqual(mail.recipient_ids, self.partner_1 | self.partner_2, - 'compose wizard: mail_mail mass mailing: mail.mail in mass mail incorrect recipients') - - # check message on test_record - message1 = self.test_record.message_ids[0] - self.assertEqual(message1.subject, 'Testing %s' % self.test_record.name) - self.assertEqual(message1.body, '

%s

' % self.test_record.name) - - # check message on test_record_2 - message1 = test_record_2.message_ids[0] - self.assertEqual(message1.subject, 'Testing %s' % test_record_2.name) - self.assertEqual(message1.body, '

%s

' % test_record_2.name) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_mass_mail_active_domain(self): - test_record_2 = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test2'}) - - self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'mass_mail', - 'default_model': self.test_record._name, - 'default_use_active_domain': True, - 'active_ids': [self.test_record.id], - 'active_domain': [('name', 'in', ['%s' % self.test_record.name, '%s' % test_record_2.name])], - }).with_user(self.user_employee).create({ - 'subject': 'From Composer Test', - 'body': '${object.name}', - }).send_mail() - - self.assertEqual(self.test_record.message_ids[0].subject, 'From Composer Test') - self.assertEqual(test_record_2.message_ids[0].subject, 'From Composer Test') - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_message_compose_mass_mail_no_active_domain(self): - test_record_2 = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test2'}) - - self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'mass_mail', - 'default_model': self.test_record._name, - 'default_use_active_domain': False, - 'active_ids': [self.test_record.id], - 'active_domain': [('name', 'in', ['%s' % self.test_record.name, '%s' % test_record_2.name])], - }).with_user(self.user_employee).create({ - 'subject': 'From Composer Test', - 'body': '${object.name}', - }).send_mail() - - self.assertEqual(self.test_record.message_ids[0].subject, 'From Composer Test') - self.assertFalse(test_record_2.message_ids.ids) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_message_compose_portal_ok(self): - portal_user = self._create_portal_user() - - with patch.object(MailTestSimple, 'check_access_rights', return_value=True): - ComposerPortal = self.env['mail.compose.message'].with_user(portal_user) - - ComposerPortal.with_context({ - 'default_composition_mode': 'comment', - 'default_model': self.test_record._name, - 'default_res_id': self.test_record.id, - }).create({ - 'subject': 'Subject', - 'body': '

Body text

', - 'partner_ids': []}).send_mail() - - self.assertEqual(self.test_record.message_ids[0].body, '

Body text

') - self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) - - ComposerPortal.with_context({ - 'default_composition_mode': 'comment', - 'default_parent_id': self.test_record.message_ids.ids[0], - }).create({ - 'subject': 'Subject', - 'body': '

Body text 2

'}).send_mail() - - self.assertEqual(self.test_record.message_ids[0].body, '

Body text 2

') - self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) - - -@tagged('mail_composer') -class TestComposerWTpl(TestMailCommon, TestRecipients): - - @classmethod - def setUpClass(cls): - super(TestComposerWTpl, cls).setUpClass() - cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) - - cls.user_employee.write({ - 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], - }) - - cls._attachments = [{ - 'name': 'first.txt', - 'datas': base64.b64encode(b'My first attachment'), - 'res_model': 'res.partner', - 'res_id': cls.user_admin.partner_id.id - }, { - 'name': 'second.txt', - 'datas': base64.b64encode(b'My second attachment'), - 'res_model': 'res.partner', - 'res_id': cls.user_admin.partner_id.id - }] - - cls.email_1 = 'test1@example.com' - cls.email_2 = 'test2@example.com' - cls.email_3 = cls.partner_1.email - cls._create_template('mail.test.simple', { - 'attachment_ids': [(0, 0, cls._attachments[0]), (0, 0, cls._attachments[1])], - 'partner_to': '%s,%s' % (cls.partner_2.id, cls.user_admin.partner_id.id), - 'email_to': '%s, %s' % (cls.email_1, cls.email_2), - 'email_cc': '%s' % cls.email_3, - }) - - # admin should receive emails - cls.user_admin.write({'notification_type': 'email'}) - # Force the attachments of the template to be in the natural order. - cls.email_template.invalidate_cache(['attachment_ids'], ids=cls.email_template.ids) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_w_template(self): - composer = self.env['mail.compose.message'].with_user(self.user_employee).with_context({ - 'default_composition_mode': 'comment', - 'default_model': 'mail.test.simple', - 'default_res_id': self.test_record.id, - 'default_template_id': self.email_template.id, - }).create({'subject': 'Forget me subject', 'body': 'Dummy body'}) - - # perform onchange and send emails - values = composer.onchange_template_id(self.email_template.id, 'comment', self.test_record._name, self.test_record.id)['value'] - composer.write(values) - with self.mock_mail_gateway(mail_unlink_sent=True): - composer.send_mail() - - new_partners = self.env['res.partner'].search([('email', 'in', [self.email_1, self.email_2])]) - self.assertMailMail( - [self.partner_1, self.partner_2, new_partners[0], new_partners[1], self.partner_admin], - 'sent', - author=self.user_employee.partner_id, - mail_message=False, - check_mail_mail=False, - email_values={ - 'subject': 'About %s' % self.test_record.name, - 'body_content': self.test_record.name, - 'attachments': [ - ('first.txt', b'My first attachment', 'text/plain'), - ('second.txt', b'My second attachment', 'text/plain') - ] - } - ) - - def test_composer_template_onchange_attachments(self): - """Tests that all attachments are added to the composer, - static attachments are not duplicated and while reports are re-generated, - and that intermediary attachments are dropped.""" - - composer = self.env['mail.compose.message'].with_context(default_attachment_ids=[]).create({}) - report_template = self.env.ref('web.action_report_externalpreview') - template_1 = self.email_template.copy({ - 'report_template': report_template.id, - }) - template_2 = self.email_template.copy({ - 'attachment_ids': False, - 'report_template': report_template.id, - }) - - onchange_templates = [template_1, template_2, template_1, False] - attachments_onchange = [composer.attachment_ids] - # template_1 has two static attachments and one dynamically generated report, - # template_2 only has the report, so we should get 3, 1, 3 attachments - attachment_numbers = [0, 3, 1, 3, 0] - - for template in onchange_templates: - onchange = composer.onchange_template_id( - template.id if template else False, 'comment', self.test_record._name, self.test_record.id - ) - composer.update(onchange['value']) - attachments_onchange.append(composer.attachment_ids) - - self.assertEqual( - [len(attachments) for attachments in attachments_onchange], - attachment_numbers, - ) - - self.assertTrue( - len(attachments_onchange[1] & attachments_onchange[3]) == 2, - "The two static attachments on the template should be common to the two onchanges" - ) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_w_template_mass_mailing(self): - test_record_2 = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test2', 'email_from': 'laurie.poiret@example.com'}) - - composer = self.env['mail.compose.message'].with_user(self.user_employee).with_context({ - 'default_composition_mode': 'mass_mail', - # 'default_notify': True, - 'default_notify': False, - 'default_model': 'mail.test.simple', - 'default_res_id': self.test_record.id, - 'default_template_id': self.email_template.id, - 'active_ids': [self.test_record.id, test_record_2.id] - }).create({}) - values = composer.onchange_template_id(self.email_template.id, 'mass_mail', 'mail.test.simple', self.test_record.id)['value'] - composer.write(values) - with self.mock_mail_gateway(mail_unlink_sent=True): - composer.send_mail() - - new_partners = self.env['res.partner'].search([('email', 'in', [self.email_1, self.email_2])]) - # hack to use assertEmails - self._mails_record1 = [dict(mail) for mail in self._mails if '%s-%s' % (self.test_record.id, self.test_record._name) in mail['message_id']] - self._mails_record2 = [dict(mail) for mail in self._mails if '%s-%s' % (test_record_2.id, test_record_2._name) in mail['message_id']] - - self._mails = self._mails_record1 - self.assertMailMail( - [self.partner_1, self.partner_2, new_partners[0], new_partners[1], self.partner_admin], - 'sent', - author=self.user_employee.partner_id, - mail_message=False, - check_mail_mail=False, - email_values={ - 'subject': 'About %s' % self.test_record.name, - 'body_content': self.test_record.name, - 'attachments': [ - ('first.txt', b'My first attachment', 'text/plain'), - ('second.txt', b'My second attachment', 'text/plain') - ] - } - ) - - self._mails = self._mails_record2 - self.assertMailMail( - [self.partner_1, self.partner_2, new_partners[0], new_partners[1], self.partner_admin], - 'sent', - author=self.user_employee.partner_id, - mail_message=False, - check_mail_mail=False, - email_values={ - 'subject': 'About %s' % test_record_2.name, - 'body_content': test_record_2.name, - 'attachments': [ - ('first.txt', b'My first attachment', 'text/plain'), - ('second.txt', b'My second attachment', 'text/plain') - ] - } - ) - - message_1 = self.test_record.message_ids[0] - message_2 = test_record_2.message_ids[0] - - # messages effectively posted - self.assertEqual(message_1.subject, 'About %s' % self.test_record.name) - self.assertEqual(message_2.subject, 'About %s' % test_record_2.name) - self.assertIn(self.test_record.name, message_1.body) - self.assertIn(test_record_2.name, message_2.body) - - def test_composer_template_save(self): - self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'comment', - 'default_model': 'mail.test.simple', - 'default_res_id': self.test_record.id, - }).create({ - 'subject': 'Forget me subject', - 'body': '

Dummy body

' - }).save_as_template() - # Test: email_template subject, body_html, model - last_template = self.env['mail.template'].search([('model', '=', 'mail.test.simple'), ('subject', '=', 'Forget me subject')], limit=1) - self.assertEqual(last_template.body_html, '

Dummy body

', 'email_template incorrect body_html') From 4c2ad377093df261ac1d7042879327bddcc2438a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 7 Apr 2021 09:17:19 +0000 Subject: [PATCH 2/4] [IMP] event, mail, mass_mailing, sms, test_mail: clean some mail tests tools helpers and parameters In this commit we backport some of 14.1+ improvements done in mail tools in order to keep a coherent definition through sub versions. We also improve docstring and add some explanations on available toold and asserts. Task ID-2500615 COM PR odoo/odoo#68874 X-original-commit: d44c47697389866603f28ff2d5da60a97574ec5f --- .../event/tests/test_event_mail_schedule.py | 12 +- addons/mail/tests/common.py | 173 +++++++++++++----- addons/mail/tests/test_mail_channel.py | 9 +- addons/mass_mailing/tests/common.py | 10 +- addons/sms/tests/test_sms_template.py | 9 +- addons/test_mail/tests/test_mail_composer.py | 5 +- addons/test_mail/tests/test_message_post.py | 10 +- 7 files changed, 158 insertions(+), 70 deletions(-) diff --git a/addons/event/tests/test_event_mail_schedule.py b/addons/event/tests/test_event_mail_schedule.py index 7b9bb735c42..34eacf1d701 100644 --- a/addons/event/tests/test_event_mail_schedule.py +++ b/addons/event/tests/test_event_mail_schedule.py @@ -120,7 +120,8 @@ class TestMailSchedule(TestEventCommon, MockEmail): self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)') self.assertMailMailWEmails( [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], - 'outgoing', '', + 'outgoing', + content=None, fields_values={'subject': 'Your registration at %s' % test_event.name, 'email_from': self.user_eventmanager.company_id.email_formatted, }) @@ -157,7 +158,8 @@ class TestMailSchedule(TestEventCommon, MockEmail): self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)') self.assertMailMailWEmails( [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], - 'outgoing', '', + 'outgoing', + content=None, fields_values={'subject': 'Your registration at %s' % test_event.name, 'email_from': self.user_eventmanager.company_id.email_formatted, }) @@ -191,7 +193,8 @@ class TestMailSchedule(TestEventCommon, MockEmail): self.assertEqual(len(self._new_mails), 2, 'event: should have scheduled 2 mails (1 / registration)') self.assertMailMailWEmails( [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], - 'outgoing', '', + 'outgoing', + content=None, fields_values={'subject': '%s: tomorrow' % test_event.name, 'email_from': self.user_eventmanager.company_id.email_formatted, }) @@ -265,7 +268,8 @@ class TestMailSchedule(TestEventCommon, MockEmail): self.assertEqual(len(self._new_mails), 3, 'event: should have scheduled 3 mails, one for each registration') self.assertMailMailWEmails( [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email)), formataddr((reg3.name, reg3.email))], - 'outgoing', '', + 'outgoing', + content=None, fields_values={'subject': '%s: today' % test_event.name, 'email_from': self.user_eventmanager.company_id.email_formatted, }) diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index b28e04edd19..b47bb185bd5 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -8,6 +8,7 @@ import time from collections import defaultdict from contextlib import contextmanager from functools import partial +from lxml import html from unittest.mock import patch from smtplib import SMTPServerDisconnected @@ -41,8 +42,8 @@ class MockEmail(common.BaseCase): build_email_origin = IrMailServer.build_email mail_create_origin = MailMail.create mail_unlink_origin = MailMail.unlink - self._init_mail_mock() self.mail_unlink_sent = mail_unlink_sent + self._init_mail_mock() def _ir_mail_server_connect(model, *args, **kwargs): if sim_error and sim_error == 'connect_smtp_notfound': @@ -151,8 +152,6 @@ class MockEmail(common.BaseCase): return email.message_from_string(pycompat.to_text(text), policy=email.policy.SMTP) def assertHtmlEqual(self, value, expected, message=None): - from lxml import html - tree = html.fragment_fromstring(value, parser=html.HTMLParser(encoding='utf-8'), create_parent='body') # mass mailing: add base tag we have to remove @@ -170,19 +169,38 @@ class MockEmail(common.BaseCase): self.assertEqual(tree, expected_node) # ------------------------------------------------------------ - # GATEWAY ASSERTS + # GATEWAY GETTERS # ------------------------------------------------------------ def _find_sent_mail_wemail(self, email_to): - for email in self._mails: - if set(email['email_to']) == set([email_to]): + """ Find a sent email with a given list of recipients. Email should match + exactly the recipients. + + :return email: a dictionary mapping values given to ``build_email``; + """ + for sent_email in self._mails: + if set(sent_email['email_to']) == set([email_to]): break else: raise AssertionError('sent mail not found for email_to %s' % (email_to)) - return email - + return sent_email def _find_mail_mail_wpartners(self, recipients, status, mail_message=None, author=None): + """ Find a mail.mail record based on various parameters, notably a list + of recipients (partners). + + :param recipients: a ``res.partner`` recordset Check all of them are in mail + recipients to find the right mail.mail record; + :param status: state of mail.mail. If not void use it to filter mail.mail + record; + :param mail_message: optional check/filter on mail_message_id field aka + a ``mail.message`` record; + :param author: optional check/filter on author_id field aka a ``res.partner`` + record; + + :return mail: a ``mail.mail`` record generated during the mock and matching + given parameters and filters; + """ for mail in self._new_mails: if author is not None and mail.author_id != author: continue @@ -197,6 +215,21 @@ class MockEmail(common.BaseCase): return mail def _find_mail_mail_wemail(self, email_to, status, mail_message=None, author=None): + """ Find a mail.mail record based on various parameters, notably a list + of email to (string emails). + + :param email_to: either matching mail.email_to value, either a mail sent + to a single recipient whose email is email_to; + :param status: state of mail.mail. If not void use it to filter mail.mail + record; + :param mail_message: optional check/filter on mail_message_id field aka + a ``mail.message`` record; + :param author: optional check/filter on author_id field aka a ``res.partner`` + record; + + :return mail: a ``mail.mail`` record generated during the mock and matching + given parameters and filters; + """ for mail in self._new_mails: if author is not None and mail.author_id != author: continue @@ -204,13 +237,17 @@ class MockEmail(common.BaseCase): continue if status and mail.state != status: continue - if (mail.email_to == email_to and not mail.recipient_ids) or (not mail.email_to and mail.recipient_ids.email) == email_to: + if (mail.email_to == email_to and not mail.recipient_ids) or (not mail.email_to and mail.recipient_ids.email == email_to): break else: raise AssertionError('mail.mail not found for email_to %s / status %s in %s' % (email_to, status, repr([m.email_to for m in self._new_mails]))) return mail def _find_mail_mail_wrecord(self, record): + """ Find a mail.mail record based on model / res_id of a record. + + :return mail: a ``mail.mail`` record generated during the mock; + """ for mail in self._new_mails: if mail.model == record._name and mail.res_id == record.id: break @@ -218,45 +255,78 @@ class MockEmail(common.BaseCase): raise AssertionError('mail.mail not found for record %s in %s' % (record, repr([m.email_to for m in self._new_mails]))) return mail - def assertMailFailed(self, author, recipients, mail_message): - mail = self._find_mail(author, recipients, mail_message) - self.assertEqual(mail.state, 'exception') + # ------------------------------------------------------------ + # GATEWAY ASSERTS + # ------------------------------------------------------------ def assertMailMail(self, recipients, status, check_mail_mail=True, mail_message=None, author=None, content=None, fields_values=None, email_values=None): - if check_mail_mail: - found_mail = self._find_mail_mail_wpartners(recipients, status, mail_message=mail_message, author=author) - self.assertTrue(bool(found_mail)) + """ Assert mail.mail records are created and maybe sent as emails. Allow + asserting their content. Records to check are the one generated when + using mock (mail.mail and outgoing emails). This method takes partners + as source of record fetch and assert. + + :param recipients: a ``res.partner`` recordset. See ``_find_mail_mail_wpartners``; + :param status: mail.mail state used to filter mails. If ``sent`` this method + also check that emails have been sent trough gateway; + :param mail_message: see ``_find_mail_mail_wpartners``; + :param author: see ``_find_mail_mail_wpartners``;; + :param content: if given, check it is contained within mail html body; + :param fields_values: if given, should be a dictionary of field names / + values allowing to check ``mail.mail`` additional values (subject, + reply_to, ...); + :param email_values: if given, should be a dictionary of keys / values + allowing to check sent email additional values (if any). + See ``assertSentEmail``; + + :param check_mail_mail: deprecated, use ``assertSentEmail`` if False + """ + found_mail = self._find_mail_mail_wpartners(recipients, status, mail_message=mail_message, author=author) + self.assertTrue(bool(found_mail)) + if content: + self.assertIn(content, found_mail.body_html) + for fname, fvalue in (fields_values or {}).items(): + self.assertEqual( + found_mail[fname], fvalue, + 'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname])) + if status == 'sent': + for recipient in recipients: + self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [recipient], **(email_values or {})) + + def assertMailMailWEmails(self, emails, status, + mail_message=None, author=None, + content=None, fields_values=None, email_values=None): + """ Assert mail.mail records are created and maybe sent as emails. Allow + asserting their content. Records to check are the one generated when + using mock (mail.mail and outgoing emails). This method takes emails + as source of record fetch and assert. + + :param emails: a list of emails. See ``_find_mail_mail_wemail``; + :param status: mail.mail state used to filter mails. If ``sent`` this method + also check that emails have been sent trough gateway; + :param mail_message: see ``_find_mail_mail_wemail``; + :param author: see ``_find_mail_mail_wemail``;; + :param content: if given, check it is contained within mail html body; + :param fields_values: if given, should be a dictionary of field names / + values allowing to check ``mail.mail`` additional values (subject, + reply_to, ...); + :param email_values: if given, should be a dictionary of keys / values + allowing to check sent email additional values (if any). + See ``assertSentEmail``; + + :param check_mail_mail: deprecated, use ``assertSentEmail`` if False + """ + for email_to in emails: + found_mail = self._find_mail_mail_wemail(email_to, status, mail_message=mail_message, author=author) if content: self.assertIn(content, found_mail.body_html) for fname, fvalue in (fields_values or {}).items(): self.assertEqual( found_mail[fname], fvalue, 'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname])) - if status == 'sent': - for recipient in recipients: - self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [recipient], **(email_values or {})) - def assertMailMailWEmails(self, emails, status, content, fields_values=None): - """ Will check in self._new_mails to find a sent mail.mail. To use with - mail gateway mock. - - :param emails: list of emails; - :param status: status of mail.mail; - :param content: content to check for each email; - :param fields_values: specific value to check on the mail.mail record; - """ - for email_to in emails: - sent_mail = self._find_mail_mail_wemail(email_to, status) - if content: - self.assertIn(content, sent_mail.body_html) - for fname, fvalue in (fields_values or {}).items(): - self.assertEqual( - sent_mail[fname], fvalue, - 'Mail: expected %s for %s, got %s' % (fvalue, fname, sent_mail[fname])) - - def assertNoMail(self, author, recipients, mail_message=None): + def assertNoMail(self, recipients, mail_message=None, author=None): try: self._find_mail_mail_wpartners(recipients, False, mail_message=mail_message, author=author) except AssertionError: @@ -606,18 +676,27 @@ class MailCase(MockEmail): for recipients in email_groups.values(): partners = self.env['res.partner'].sudo().concat(*recipients) if all(p in mail_groups['failure'] for p in partners): - self.assertMailMail(partners, 'exception', author=message.author_id, mail_message=message) + if not self.mail_unlink_sent: + self.assertMailMail(partners, 'exception', + author=message.author_id, + mail_message=message) + else: + for recipient in partners: + self.assertSentEmail(message.author_id, [recipient]) else: - check_mail_mail = not self.mail_unlink_sent - self.assertMailMail( - partners, 'sent', - author=message.author_id if message.author_id else message.email_from, - mail_message=message, - check_mail_mail=check_mail_mail, - email_values={'body_content': mbody} - ) + if not self.mail_unlink_sent: + self.assertMailMail( + partners, 'sent', + author=message.author_id if message.author_id else message.email_from, + mail_message=message, + email_values={'body_content': mbody} + ) + else: + for recipient in partners: + self.assertSentEmail(message.author_id if message.author_id else message.email_from, [recipient], + email_values={'body_content': mbody}) if not any(p for recipients in email_groups.values() for p in recipients): - self.assertNoMail(message.author_id, partners, message) + self.assertNoMail(partners, mail_message=message, author=message.author_id) return done_msgs, done_notifs diff --git a/addons/mail/tests/test_mail_channel.py b/addons/mail/tests/test_mail_channel.py index 99f4f3ac9e2..f1157f077a5 100644 --- a/addons/mail/tests/test_mail_channel.py +++ b/addons/mail/tests/test_mail_channel.py @@ -508,8 +508,11 @@ class TestChannelModeration(MailCommon): (msg_c1_admin1 | msg_c1_emplo2).with_user(self.user_employee)._moderate('reject', title='RejectTitle', comment='RejectComment') self.assertEqual(len(self._new_mails), 2) - self.assertMailMailWEmails([self.partner_admin.email_formatted, self.partner_employee_2.email_formatted], 'outgoing', 'RejectComment', - fields_values={'subject': 'RejectTitle', 'author_id': self.partner_employee}) + self.assertMailMailWEmails( + [self.partner_admin.email_formatted, self.partner_employee_2.email_formatted], 'outgoing', + content='RejectComment', + fields_values={'subject': 'RejectTitle', 'author_id': self.partner_employee} + ) self.assertTrue(all('RejectComment' in body for body in self._new_mails.mapped('body_html'))) self.assertTrue(all('Body' in body for body in self._new_mails.mapped('body_html'))) @@ -674,7 +677,7 @@ class TestChannelModeration(MailCommon): }) # admin being banned, should not received any email - self.assertNoMail(self.partner_employee, self.partner_admin) + self.assertNoMail(self.partner_employee, author=self.partner_admin) # only moderators can send guidelines channel.write({ diff --git a/addons/mass_mailing/tests/common.py b/addons/mass_mailing/tests/common.py index 8c3925d7527..72acbb05c8b 100644 --- a/addons/mass_mailing/tests/common.py +++ b/addons/mass_mailing/tests/common.py @@ -52,15 +52,15 @@ class MassMailCase(MailCase, MockLinkTracker): fields_values['failure_type'] = recipient_info['failure_type'] if state == 'sent': - self.assertMailMailWEmails([email], 'sent', content, fields_values=fields_values) + self.assertMailMailWEmails([email], 'sent', content=content, fields_values=fields_values) elif state == 'replied': # replied imply something has been sent - self.assertMailMailWEmails([email], 'sent', content, fields_values=fields_values) + self.assertMailMailWEmails([email], 'sent', content=content, fields_values=fields_values) elif state == 'ignored': - self.assertMailMailWEmails([email], 'cancel', content, fields_values=fields_values) + self.assertMailMailWEmails([email], 'cancel', content=content, fields_values=fields_values) elif state == 'exception': - self.assertMailMailWEmails([email], 'exception', content, fields_values=fields_values) + self.assertMailMailWEmails([email], 'exception', content=content, fields_values=fields_values) elif state == 'canceled': - self.assertMailMailWEmails([email], 'canceled', content, fields_values=fields_values) + self.assertMailMailWEmails([email], 'canceled', content=content, fields_values=fields_values) else: raise NotImplementedError() diff --git a/addons/sms/tests/test_sms_template.py b/addons/sms/tests/test_sms_template.py index b5a67655180..461b060f33a 100644 --- a/addons/sms/tests/test_sms_template.py +++ b/addons/sms/tests/test_sms_template.py @@ -3,8 +3,9 @@ from odoo.tests.common import TransactionCase, users from odoo.addons.mail.tests.common import mail_new_test_user -from odoo.tests import tagged from odoo.exceptions import AccessError +from odoo.tests import tagged +from odoo.tools import mute_logger @tagged('post_install') @@ -26,7 +27,8 @@ class TestSmsTemplateAccessRights(TransactionCase): cls.sms_templates = cls.env['sms.template'].create(vals) @users('user_employee') - def test_access_rights_user_sms_template(self): + @mute_logger('odoo.models.unlink') + def test_access_rights_user(self): # Check if a member of group_user can only read on sms.template for sms_template in self.env['sms.template'].browse(self.sms_templates.ids): self.assertTrue(bool(sms_template.name)) @@ -42,7 +44,8 @@ class TestSmsTemplateAccessRights(TransactionCase): sms_template.unlink() @users('user_system') - def test_access_rights_manager_sms_template(self): + @mute_logger('odoo.models.unlink', 'odoo.addons.base.models.ir_model') + def test_access_rights_system(self): admin = self.env.ref('base.user_admin') for sms_template in self.env['sms.template'].browse(self.sms_templates.ids): self.assertTrue(bool(sms_template.name)) diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index e22a875a5a6..0547caca98d 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -520,7 +520,6 @@ class TestComposerResultsComment(TestMailComposer): # template is sent only to partners (email_to are transformed) message = self.test_record.message_ids[0] self.assertMailMail(self.partner_employee_2, 'sent', - check_mail_mail=True, mail_message=message, author=self.partner_employee, # author != email_from (template sets only email_from) email_values={ @@ -536,7 +535,6 @@ class TestComposerResultsComment(TestMailComposer): fields_values={}, ) self.assertMailMail(self.test_record.customer_id + new_partners, 'sent', - check_mail_mail=True, mail_message=message, author=self.partner_employee, # author != email_from (template sets only email_from) email_values={ @@ -597,7 +595,7 @@ class TestComposerResultsMass(TestMailComposer): message = record.message_ids[0] # template is sent directly using customer field, meaning we have recipients - self.assertMailMail(record.customer_id, 'sent', check_mail_mail=True, + self.assertMailMail(record.customer_id, 'sent', mail_message=message, author=self.partner_employee) @@ -668,7 +666,6 @@ class TestComposerResultsMass(TestMailComposer): self._mails = _mails self.assertMailMail(record.customer_id + new_partners + self.partner_admin, 'sent', - check_mail_mail=True, mail_message=message, author=self.partner_employee, email_values={ diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index db0c27812dd..c696fb0a263 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -113,10 +113,12 @@ class TestMessagePost(TestMailCommon, TestRecipients): # subscribe second employee to the group to test notifications self.test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id]) - with self.assertSinglePostNotifications([ - {'partner': self.partner_1, 'type': 'email'}, - {'partner': self.partner_2, 'type': 'email'}, - {'partner': self.partner_admin, 'type': 'email'}], {'content': _body}, mail_unlink_sent=True): + with self.assertSinglePostNotifications( + [{'partner': self.partner_1, 'type': 'email'}, + {'partner': self.partner_2, 'type': 'email'}, + {'partner': self.partner_admin, 'type': 'email'}], + {'content': _body}, + mail_unlink_sent=True): msg = self.test_record.with_user(self.user_employee).message_post( body=_body, subject=_subject, message_type='comment', subtype_xmlid='mail.mt_comment', From 344c5ef072af29a0bd885f3d18de819750ef49e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Nov 2020 15:38:45 +0000 Subject: [PATCH 3/4] [IMP] mail: add tests and docstrings for rendering mixin tools (jinja / qweb) PURPOSE Add some tests for ``mail.render.mixin`` in order to assert its base behavior. SPECIFICATIONS Add tests for * jinja rendering tools; * QWeb rendering tools; * translation support; Also add tests for jinja markers: block, variable and line statement markers should be tested. Also improve docstrings of tool methods. Add notably explanation of parameters and better explain methods purpose. LINKS Task ID-2500615 Prepares Task ID-2484296 (improve markers use in Jinja) Prepares Task ID-27033 (support QWeb in templates) COM PR odoo/odoo#68874 X-original-commit: f3dba8e020a0be72321427ed839b80ad77386946 --- addons/mail/models/mail_render_mixin.py | 43 ++++-- addons/mail/tests/__init__.py | 1 + addons/mail/tests/test_mail_render.py | 191 ++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 addons/mail/tests/test_mail_render.py diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py index 6211cde0046..68728726b11 100644 --- a/addons/mail/models/mail_render_mixin.py +++ b/addons/mail/models/mail_render_mixin.py @@ -258,6 +258,19 @@ class MailRenderMixin(models.AbstractModel): @api.model def _render_template_qweb(self, template_src, model, res_ids, add_context=None): + """ Render a QWeb template. + + :param str template_src: source QWeb template. It should be a string + XmlID allowing to fetch an ir.ui.view; + :param str model: see ``MailRenderMixin._render_field)``; + :param list res_ids: see ``MailRenderMixin._render_field)``; + + :param dict add_context: additional context to give to renderer. It + allows to add values to base rendering context generated by + ``MailRenderMixin._render_qweb_eval_context()``; + + :return dict: {res_id: string of rendered template based on record} + """ view = self.env.ref(template_src, raise_if_not_found=False) or self.env['ir.ui.view'] results = dict.fromkeys(res_ids, u"") if not view: @@ -370,15 +383,14 @@ class MailRenderMixin(models.AbstractModel): @api.model def _render_template(self, template_src, model, res_ids, engine='jinja', add_context=None, post_process=False): """ Render the given string on records designed by model / res_ids using - the given rendering engine. Currently only jinja is supported. + the given rendering engine. Currently only jinja or qweb are supported. :param str template_src: template text to render (jinja) or xml id of view (qweb) this could be cleaned but hey, we are in a rush :param str model: model name of records on which we want to perform rendering :param list res_ids: list of ids of records (all belonging to same model) :param string engine: jinja - :param post_process: perform rendered str / html post processing (see - ``_render_template_postprocess``) + :param post_process: see ``MailRenderMixin._render_field``; :return dict: {res_id: string of rendered template based on record} """ @@ -437,16 +449,25 @@ class MailRenderMixin(models.AbstractModel): for lang, lang_res_ids in lang_to_res_ids.items() ) - def _render_field(self, field, res_ids, compute_lang=False, set_lang=False, post_process=False): - """ Given some record ids, render a given field of template rendered on - all records. + def _render_field(self, field, res_ids, + compute_lang=False, set_lang=False, + post_process=False): + """ Given some record ids, render a template located on field on all + records. ``field`` should be a field of self (i.e. ``body_html`` on + ``mail.template``). res_ids are record IDs linked to ``model`` field + on self. :param list res_ids: list of ids of records (all belonging to same model - defined by self.model) - :param compute_lang: compute rendering language based on template.lang - :param set_lang: force language - :param post_process: perform rendered str / html post processing (see - ``_render_template_postprocess``) + defined by ``self.model``) + + :param boolean compute_lang: compute language to render on translated + version of the template instead of default (probably english) one. + Language will be computed based on ``self.lang``; + :param string set_lang: force language for rendering. It should be a + valid lang code matching an activate res.lang. Checked only if + ``compute_lang`` is False; + :param boolean post_process: perform a post processing on rendered result + (notably html links management). See``_render_template_postprocess``); :return dict: {res_id: string of rendered template based on record} """ diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py index f275a83b1b0..4a99ea63e4a 100644 --- a/addons/mail/tests/__init__.py +++ b/addons/mail/tests/__init__.py @@ -4,5 +4,6 @@ from . import test_mail_channel from . import test_mail_channel_partner from . import test_mail_full_composer +from . import test_mail_render from . import test_res_partner from . import test_update_notification diff --git a/addons/mail/tests/test_mail_render.py b/addons/mail/tests/test_mail_render.py new file mode 100644 index 00000000000..2f14cbca97e --- /dev/null +++ b/addons/mail/tests/test_mail_render.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mail.tests import common +from odoo.tests import tagged, users + + +@tagged('mail_render') +class TestMailRender(common.MailCommon): + + @classmethod + def setUpClass(cls): + super(TestMailRender, cls).setUpClass() + + # activate multi language support + cls.env['res.lang']._activate_lang('fr_FR') + cls.user_admin.write({'lang': 'en_US'}) + + # test records + cls.render_object = cls.env['res.partner'].create({ + 'name': 'TestRecord', + 'lang': 'en_US', + }) + cls.render_object_fr = cls.env['res.partner'].create({ + 'name': 'Element de Test', + 'lang': 'fr_FR', + }) + + # some jinja templates + cls.base_jinja_bits = [ + '

Hello

', + '

Hello ${object.name}

', + """

+% set english = object.lang == 'en_US' +% if english + English Speaker +% else + Other Speaker +% endif +

""" + ] + cls.base_jinja_bits_fr = [ + '

Bonjour

', + '

Bonjour ${object.name}

', + """

+% set english = object.lang == 'en_US' +% if english + Narrateur Anglais +% else + Autre Narrateur +% endif +

""" + ] + + # some qweb templates and their xml ids + cls.base_qweb_templates = cls.env['ir.ui.view'].create([ + {'name': 'TestRender', 'type': 'qweb', + 'arch': '

Hello

', + }, + {'name': 'TestRender2', 'type': 'qweb', + 'arch': '

Hello

', + }, + {'name': 'TestRender3', 'type': 'qweb', + 'arch': """

+ English Speaker + Other Speaker +

""", + }, + ]) + cls.base_qweb_templates_data = cls.env['ir.model.data'].create([ + {'name': template.name, 'module': 'mail', + 'model': template._name, 'res_id': template.id, + } for template in cls.base_qweb_templates + ]) + cls.base_qweb_templates_xmlids = [ + model_data.complete_name + for model_data in cls.base_qweb_templates_data + ] + + # render result + cls.base_rendered = [ + '

Hello

', + '

Hello %s

' % cls.render_object.name, + """

+ English Speaker +

""" + ] + cls.base_rendered_fr = [ + '

Bonjour

', + '

Bonjour %s

' % cls.render_object_fr.name, + """

+ Autre Narrateur +

""" + ] + + # link to mail template + cls.test_template_jinja = cls.env['mail.template'].create({ + 'name': 'Test Template', + 'subject': cls.base_jinja_bits[0], + 'body_html': cls.base_jinja_bits[1], + 'model_id': cls.env['ir.model']._get('res.partner').id, + 'lang': '${object.lang}' + }) + + # some translations + cls.env['ir.translation'].create({ + 'type': 'model', + 'name': 'mail.template,subject', + 'lang': 'fr_FR', + 'res_id': cls.test_template_jinja.id, + 'src': cls.test_template_jinja.subject, + 'value': cls.base_jinja_bits_fr[0], + }) + cls.env['ir.translation'].create({ + 'type': 'model', + 'name': 'mail.template,body_html', + 'lang': 'fr_FR', + 'res_id': cls.test_template_jinja.id, + 'src': cls.test_template_jinja.body_html, + 'value': cls.base_jinja_bits_fr[1], + }) + cls.env['ir.model.data'].create({ + 'name': 'test_template_xmlid', + 'module': 'mail', + 'model': cls.test_template_jinja._name, + 'res_id': cls.test_template_jinja.id, + }) + + @users('employee') + def test_render_jinja(self): + source = """

+% set line_statement_variable = 3 +We have ${line_statement_variable} cookies in stock +We have <% set block_variable = 4 %>${block_variable} cookies in stock +

""" + partner = self.env['res.partner'].browse(self.render_object.ids) + result = self.env['mail.render.mixin']._render_template( + source, + partner._name, + partner.ids, + engine='jinja', + )[partner.id] + self.assertEqual(result, """

+We have 3 cookies in stock +We have 4 cookies in stock +

""") + + @users('employee') + def test_render_mail_template_jinja(self): + template = self.env['mail.template'].browse(self.test_template_jinja.ids) + partner = self.env['res.partner'].browse(self.render_object.ids) + for fname, expected in zip(['subject', 'body_html'], self.base_rendered): + rendered = template._render_field( + fname, + partner.ids, + compute_lang=True + )[partner.id] + self.assertEqual(rendered, expected) + + partner = self.env['res.partner'].browse(self.render_object_fr.ids) + for fname, expected in zip(['subject', 'body_html'], self.base_rendered_fr): + rendered = template._render_field( + fname, + partner.ids, + compute_lang=True + )[partner.id] + self.assertEqual(rendered, expected) + + @users('employee') + def test_render_template_jinja(self): + partner = self.env['res.partner'].browse(self.render_object.ids) + for source, expected in zip(self.base_jinja_bits, self.base_rendered): + rendered = self.env['mail.render.mixin']._render_template( + source, + partner._name, + partner.ids, + engine='jinja', + )[partner.id] + self.assertEqual(rendered, expected) + + @users('employee') + def test_render_template_qweb(self): + partner = self.env['res.partner'].browse(self.render_object.ids) + for source, expected in zip(self.base_qweb_templates_xmlids, self.base_rendered): + rendered = self.env['mail.render.mixin']._render_template( + source, + partner._name, + partner.ids, + engine='qweb', + )[partner.id].decode() + self.assertEqual(rendered, expected) From a4a5bb45b5a7a3c5d44a46aaf627e2cca562962b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 8 Apr 2021 14:41:56 +0000 Subject: [PATCH 4/4] [IMP] mail, (test_)mass_mailing: also check sent emails if possible When using assertMailMailWEmails custom assert we now check that a sent mail.mail matches a sent email. It allows to have a complete check from mailing traces to sent emails. Task ID-2500615 COM PR odoo/odoo#68874 ENT PR odoo/enterprise#17558 X-original-commit: 59da8627e0d732b12fb98faa6e66e43b8c75dc9c --- addons/mail/tests/common.py | 3 ++ addons/mass_mailing/tests/common.py | 29 +++++++++++-------- .../tests/test_mailing_internals.py | 4 +-- .../test_mail_full/tests/test_mass_mailing.py | 2 +- addons/test_mass_mailing/tests/common.py | 4 ++- .../tests/test_blacklist_behavior.py | 3 +- 6 files changed, 28 insertions(+), 17 deletions(-) diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index b47bb185bd5..bf3f3707008 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -325,6 +325,9 @@ class MockEmail(common.BaseCase): self.assertEqual( found_mail[fname], fvalue, 'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname])) + if status == 'sent': + for email_to in emails: + self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [email_to], **(email_values or {})) def assertNoMail(self, recipients, mail_message=None, author=None): try: diff --git a/addons/mass_mailing/tests/common.py b/addons/mass_mailing/tests/common.py index 72acbb05c8b..512e1c73a2f 100644 --- a/addons/mass_mailing/tests/common.py +++ b/addons/mass_mailing/tests/common.py @@ -7,7 +7,8 @@ from odoo.addons.mail.tests.common import MailCase, MailCommon, mail_new_test_us class MassMailCase(MailCase, MockLinkTracker): - def assertMailTraces(self, recipients_info, mailing, records, check_mail=True): + def assertMailTraces(self, recipients_info, mailing, records, + check_mail=True, author=None): """ Check content of traces. :param recipients_info: list[{ @@ -19,6 +20,15 @@ class MassMailCase(MailCase, MockLinkTracker): 'failure_type': optional: UPDATE ME }, { ... }] """ + # map trace state to email state + state_mapping = { + 'sent': 'sent', + 'replied': 'sent', # replied imply something has been sent + 'ignored': 'cancel', + 'exception': 'exception', + 'canceled': 'canceled', + } + traces = self.env['mailing.trace'].search([ ('mass_mailing_id', 'in', mailing.ids), ('res_id', 'in', records.ids) @@ -47,22 +57,17 @@ class MassMailCase(MailCase, MockLinkTracker): ) if check_mail: + if author is None: + author = self.env.user.partner_id + fields_values = {'mailing_id': mailing} if 'failure_type' in recipient_info: fields_values['failure_type'] = recipient_info['failure_type'] - if state == 'sent': - self.assertMailMailWEmails([email], 'sent', content=content, fields_values=fields_values) - elif state == 'replied': # replied imply something has been sent - self.assertMailMailWEmails([email], 'sent', content=content, fields_values=fields_values) - elif state == 'ignored': - self.assertMailMailWEmails([email], 'cancel', content=content, fields_values=fields_values) - elif state == 'exception': - self.assertMailMailWEmails([email], 'exception', content=content, fields_values=fields_values) - elif state == 'canceled': - self.assertMailMailWEmails([email], 'canceled', content=content, fields_values=fields_values) + if partner: + self.assertMailMail(partner, state_mapping[state], author=author, content=content, fields_values=fields_values) else: - raise NotImplementedError() + self.assertMailMailWEmails([email], state_mapping[state], author=author, content=content, fields_values=fields_values) class MassMailCommon(MailCommon, MassMailCase): diff --git a/addons/mass_mailing/tests/test_mailing_internals.py b/addons/mass_mailing/tests/test_mailing_internals.py index 4639815bbb7..872161e16c3 100644 --- a/addons/mass_mailing/tests/test_mailing_internals.py +++ b/addons/mass_mailing/tests/test_mailing_internals.py @@ -240,8 +240,8 @@ class TestMassMailFeatures(MassMailCommon, CronMixinCase): mailing._process_mass_mailing_queue() self.assertMailTraces( - [{'email': 'test1@example.com'}, - {'email': 'test2@example.com', 'state': 'ignored'}], + [{'partner': partner_a}, + {'partner': partner_b, 'state': 'ignored'}], mailing, partner_a | partner_b, check_mail=True ) diff --git a/addons/test_mail_full/tests/test_mass_mailing.py b/addons/test_mail_full/tests/test_mass_mailing.py index 2745855cea8..693022572fa 100644 --- a/addons/test_mail_full/tests/test_mass_mailing.py +++ b/addons/test_mail_full/tests/test_mass_mailing.py @@ -75,6 +75,6 @@ class TestMassMailing(TestMailFullCommon): 'http://localhost:%s/mailing/%s/view' % (config['http_port'], mailing.id), email['body']) - self.assertMailTraces([recipient_info], mailing, recipient, check_mail=True) + self.assertMailTraces([recipient_info], mailing, recipient, check_mail=True, author=self.env.user.partner_id) self.assertEqual(mailing.ignored, 4) diff --git a/addons/test_mass_mailing/tests/common.py b/addons/test_mass_mailing/tests/common.py index 84f04bb29b1..2a550878b12 100644 --- a/addons/test_mass_mailing/tests/common.py +++ b/addons/test_mass_mailing/tests/common.py @@ -14,7 +14,9 @@ class TestMassMailCommon(MassMailCommon, TestMailCommon): def setUpClass(cls): super(TestMassMailCommon, cls).setUpClass() - cls.mailing_bl = cls.env['mailing.mailing'].create({ + # enforce last update by user_marketing to match _process_mass_mailing_queue + # taking last writer as user running a batch + cls.mailing_bl = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({ 'name': 'SourceName', 'subject': 'MailingSubject', 'body_html': '

Hello ${object.name}

', diff --git a/addons/test_mass_mailing/tests/test_blacklist_behavior.py b/addons/test_mass_mailing/tests/test_blacklist_behavior.py index aa513d2a389..e07bfff760c 100644 --- a/addons/test_mass_mailing/tests/test_blacklist_behavior.py +++ b/addons/test_mass_mailing/tests/test_blacklist_behavior.py @@ -35,7 +35,8 @@ class TestAutoBlacklist(common.TestMassMailCommon): self.assertMailTraces( [{'email': 'test.record.00@test.example.com'}], - mailing, target, check_mail=True + mailing, target, + check_mail=True ) # call bounced