From c2fd9a496570445862fbc99aedb1e2b0dbd256b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 4 Nov 2021 16:22:29 +0000 Subject: [PATCH 1/9] [IMP] (test_)mail: allow attachment propagation in all activity feedback methods Activity-level feedback method accepts attachment_ids but not its mixin-level counterpart. This commit fixes that by adding the parameter. The activity-level action_feedback_schedule_next method now also accepts attachments in addition to the feedback, making the API coherent through all entry points. At activity level ``action_done`` now goes through ``action_feedback``. This means it now has two main methods * ``action_feedback``; * ``action_feedback_schedule_next``; All methods finally end up calling ``_action_done`` and correctly propagate feedback and attachments. We have a bit less entry points with possible API changes. Test mail models are updated to be able to use this small addition. Task-2710804 (Mail: Clean MailThread API) Part-of: odoo/odoo#86393 --- addons/mail/models/mail_activity.py | 14 +++++++------- addons/mail/models/mail_activity_mixin.py | 4 ++-- addons/test_mail/models/test_mail_models.py | 6 ++++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 6667447cd39..b8887ec2c73 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -472,20 +472,20 @@ class MailActivity(models.Model): def action_done(self): """ Wrapper without feedback because web button add context as parameter, therefore setting context to feedback """ - messages, next_activities = self._action_done() - return messages.ids and messages.ids[0] or False + return self.action_feedback() def action_feedback(self, feedback=False, attachment_ids=None): - self = self.with_context(clean_context(self.env.context)) - messages, next_activities = self._action_done(feedback=feedback, attachment_ids=attachment_ids) - return messages.ids and messages.ids[0] or False + messages, _next_activities = self.with_context( + clean_context(self.env.context) + )._action_done(feedback=feedback, attachment_ids=attachment_ids) + return messages[0].id if messages else False def action_done_schedule_next(self): """ Wrapper without feedback because web button add context as parameter, therefore setting context to feedback """ return self.action_feedback_schedule_next() - def action_feedback_schedule_next(self, feedback=False): + def action_feedback_schedule_next(self, feedback=False, attachment_ids=None): ctx = dict( clean_context(self.env.context), default_previous_activity_type_id=self.activity_type_id.id, @@ -493,7 +493,7 @@ class MailActivity(models.Model): default_res_id=self.res_id, default_res_model=self.res_model, ) - messages, next_activities = self._action_done(feedback=feedback) # will unlink activity, dont access self after that + _messages, next_activities = self._action_done(feedback=feedback, attachment_ids=attachment_ids) # will unlink activity, dont access self after that if next_activities: return False return { diff --git a/addons/mail/models/mail_activity_mixin.py b/addons/mail/models/mail_activity_mixin.py index d718aa21c8c..7d656bc96b6 100644 --- a/addons/mail/models/mail_activity_mixin.py +++ b/addons/mail/models/mail_activity_mixin.py @@ -475,7 +475,7 @@ class MailActivityMixin(models.AbstractModel): activities.write(write_vals) return activities - def activity_feedback(self, act_type_xmlids, user_id=None, feedback=None): + def activity_feedback(self, act_type_xmlids, user_id=None, feedback=None, attachment_ids=None): """ Set activities as done, limiting to some activity types and optionally to a given user. """ if self.env.context.get('mail_activity_automation_skip'): @@ -488,7 +488,7 @@ class MailActivityMixin(models.AbstractModel): return False activities = self.activity_search(act_type_xmlids, user_id=user_id) if activities: - activities.action_feedback(feedback=feedback) + activities.action_feedback(feedback=feedback, attachment_ids=attachment_ids) return True def activity_unlink(self, act_type_xmlids, user_id=None): diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py index 6520ad3adb8..f04c3876cab 100644 --- a/addons/test_mail/models/test_mail_models.py +++ b/addons/test_mail/models/test_mail_models.py @@ -60,8 +60,10 @@ class MailTestActivity(models.Model): summary=action_summary ) - def action_close(self, action_feedback): - self.activity_feedback(['test_mail.mail_act_test_todo'], feedback=action_feedback) + def action_close(self, action_feedback, attachment_ids=None): + self.activity_feedback(['test_mail.mail_act_test_todo'], + feedback=action_feedback, + attachment_ids=attachment_ids) class MailTestTicket(models.Model): From 6d166d60880837c27560e559b6ef2cac41d16aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 2 Nov 2021 12:39:40 +0000 Subject: [PATCH 2/9] [IMP] base, mail_*: use MockSmtplibCase and improve tests tooling mail_* = mail, test_mail, test_mail_full, test_mass_mailing PURPOSE Improve tooling of mail tests. Use SMTP mockup class available in base tests in mail tests to have a better and simplified mock tool when dealing with mail tests. Remove old fashioned code. SPECIFICATIONS Include ``MockSmtplibCase`` directly into ``MockEmail`` so that we re-use existing components for mocking outgoing emails. Also make parameters for gateway coherent between those two classes (use same test values). Remove 'sim_error' weird parameter of Mail ``mock_mail_gateway`` and use standard mock 'side_effect' instead in some specific tests, notably on SMTP.connect() mock. Add some assert / tools method and improve existing tools, used for creating or improving test data. This will be used in future commits when adding new tests. Add outgoing mail servers data in mail tests, allowing to make tests closer to real life use cases. Remove unnecessary calls to ``_init_mail_gateway`` as it is automatically done in ``MailCommon.setUpClass()``. Try to consolidate starting test data, notably with countries, phone numbers or email in mind to be sure test mail are deterministic. Task-2673913 (TestMail: Cleanup and improve test coverage) Part-of: odoo/odoo#86393 --- addons/mail/tests/common.py | 165 +++++++++++------- .../mail/tests/test_mail_channel_partner.py | 14 +- addons/test_mail/tests/common.py | 34 ++-- addons/test_mail/tests/test_mail_composer.py | 11 +- .../tests/test_mail_composer_mixin.py | 1 - addons/test_mail/tests/test_mail_followers.py | 1 - addons/test_mail/tests/test_mail_gateway.py | 4 - addons/test_mail/tests/test_mail_mail.py | 5 +- addons/test_mail/tests/test_mail_message.py | 1 - .../test_mail/tests/test_mail_multicompany.py | 2 - .../tests/test_mail_thread_internals.py | 8 +- .../tests/test_message_management.py | 6 +- addons/test_mail/tests/test_performance.py | 13 +- .../tests/test_sms_performance.py | 2 - .../tests/test_mailing_server.py | 5 +- odoo/addons/base/tests/common.py | 26 +-- odoo/addons/base/tests/test_ir_mail_server.py | 9 +- 17 files changed, 174 insertions(+), 133 deletions(-) diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index 2fe5f1bcb1b..4277c53892b 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -11,10 +11,9 @@ from contextlib import contextmanager from functools import partial from lxml import html from unittest.mock import patch -from smtplib import SMTPServerDisconnected -from odoo import exceptions -from odoo.addons.base.models.ir_mail_server import IrMailServer, MailDeliveryException +from odoo.addons.base.models.ir_mail_server import IrMailServer +from odoo.addons.base.tests.common import MockSmtplibCase from odoo.addons.bus.models.bus import ImBus, json_dump from odoo.addons.mail.models.mail_mail import MailMail from odoo.addons.mail.models.mail_message import Message @@ -22,10 +21,14 @@ from odoo.addons.mail.models.mail_notification import MailNotification from odoo.tests import common, new_test_user from odoo.tools import formataddr, pycompat -mail_new_test_user = partial(new_test_user, context={'mail_create_nolog': True, 'mail_create_nosubscribe': True, 'mail_notrack': True, 'no_reset_password': True}) +mail_new_test_user = partial(new_test_user, context={'mail_create_nolog': True, + 'mail_create_nosubscribe': True, + 'mail_notrack': True, + 'no_reset_password': True, + 'mail_channel_nosubscribe': True}) -class MockEmail(common.BaseCase): +class MockEmail(common.BaseCase, MockSmtplibCase): """ Tools, helpers and asserts for mailgateway-related tests Useful reminders @@ -39,37 +42,19 @@ class MockEmail(common.BaseCase): # ------------------------------------------------------------ @contextmanager - def mock_mail_gateway(self, mail_unlink_sent=False, sim_error=None): + def mock_mail_gateway(self, mail_unlink_sent=False): build_email_origin = IrMailServer.build_email + send_email_origin = IrMailServer.send_email mail_create_origin = MailMail.create mail_unlink_origin = MailMail.unlink 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': - raise exceptions.UserError( - "Missing SMTP Server\nPlease define at least one SMTP server, or provide the SMTP parameters explicitly.") - if sim_error and sim_error == 'connect_failure': - raise Exception("Some exception") - return None - def _ir_mail_server_build_email(model, *args, **kwargs): self._mails.append(kwargs) self._mails_args.append(args) return build_email_origin(model, *args, **kwargs) - def _ir_mail_server_send_email(model, message, *args, **kwargs): - if '@' not in message['To']: - raise AssertionError(model.NO_VALID_RECIPIENT) - if sim_error and sim_error == 'send_assert': - raise AssertionError('AssertionError') - elif sim_error and sim_error == 'send_disconnect': - raise SMTPServerDisconnected('SMTPServerDisconnected') - elif sim_error and sim_error == 'send_delivery': - raise MailDeliveryException('MailDeliveryException') - return message['Message-Id'] - def _mail_mail_create(model, *args, **kwargs): res = mail_create_origin(model, *args, **kwargs) self._new_mails += res.sudo() @@ -80,11 +65,13 @@ class MockEmail(common.BaseCase): return mail_unlink_origin(model, *args, **kwargs) return True - with patch.object(IrMailServer, 'connect', autospec=True, wraps=IrMailServer, side_effect=_ir_mail_server_connect) as ir_mail_server_connect_mock, \ - patch.object(IrMailServer, 'build_email', autospec=True, wraps=IrMailServer, side_effect=_ir_mail_server_build_email) as ir_mail_server_build_email_mock, \ - patch.object(IrMailServer, 'send_email', autospec=True, wraps=IrMailServer, side_effect=_ir_mail_server_send_email) as ir_mail_server_send_email_mock, \ - patch.object(MailMail, 'create', autospec=True, wraps=MailMail, side_effect=_mail_mail_create) as _mail_mail_create_mock, \ - patch.object(MailMail, 'unlink', autospec=True, wraps=MailMail, side_effect=_mail_mail_unlink) as mail_mail_unlink_mock: + with self.mock_smtplib_connection(), \ + patch.object(IrMailServer, 'build_email', autospec=True, wraps=IrMailServer, side_effect=_ir_mail_server_build_email) as build_email_mocked, \ + patch.object(IrMailServer, 'send_email', autospec=True, wraps=IrMailServer, side_effect=send_email_origin) as send_email_mocked, \ + patch.object(MailMail, 'create', autospec=True, wraps=MailMail, side_effect=_mail_mail_create), \ + patch.object(MailMail, 'unlink', autospec=True, wraps=MailMail, side_effect=_mail_mail_unlink): + self.build_email_mocked = build_email_mocked + self.send_email_mocked = send_email_mocked yield def _init_mail_mock(self): @@ -92,10 +79,6 @@ class MockEmail(common.BaseCase): self._mails_args = [] self._new_mails = self.env['mail.mail'].sudo() - # ------------------------------------------------------------ - # GATEWAY TOOLS - # ------------------------------------------------------------ - @classmethod def _init_mail_gateway(cls): cls.alias_domain = 'test.com' @@ -108,6 +91,26 @@ class MockEmail(common.BaseCase): cls.env['ir.config_parameter'].set_param('mail.default.from', cls.default_from) cls.mailer_daemon_email = formataddr(('MAILER-DAEMON', '%s@%s' % (cls.alias_bounce, cls.alias_domain))) + @classmethod + def _init_outgoing_gateway(cls): + cls.env['ir.mail_server'].search([]).unlink() + cls.mail_server_domain, cls.mail_server_global = cls.env['ir.mail_server'].create([ + {'from_filter': 'test.com', + 'name': 'Domain Based Server', + 'smtp_encryption': 'none', + 'smtp_host': 'smtp_host', + }, + {'from_filter': False, + 'name': 'No FromFilter Server', + 'smtp_encryption': 'none', + 'smtp_host': 'smtp_host', + } + ]) + + # ------------------------------------------------------------ + # GATEWAY TOOLS + # ------------------------------------------------------------ + def format(self, template, to='groups@example.com, other@gmail.com', subject='Frogs', email_from='Sylvie Lelitre ', return_path='', cc='', extra='', msg_id='<1198923581.41972151344608186760.JavaMail@agrolait.com>', @@ -316,7 +319,7 @@ class MockEmail(common.BaseCase): # ------------------------------------------------------------ def assertMailMail(self, recipients, status, - check_mail_mail=True, mail_message=None, author=None, + 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 @@ -335,8 +338,6 @@ class MockEmail(common.BaseCase): :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)) @@ -370,8 +371,6 @@ class MockEmail(common.BaseCase): :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) @@ -410,6 +409,16 @@ class MockEmail(common.BaseCase): found_mail[fname], fvalue, 'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname])) + def assertMessageFields(self, message, fields_values): + """ Just a quick helper to check a mail.message content by giving directly + a dict for fields. Allows to hide a lot of assertEqual under a simple + call with a dictionary of expected values. """ + for fname, fvalue in fields_values.items(): + self.assertEqual( + message[fname], fvalue, + 'Message: expected %s for %s, got %s' % (fvalue, fname, message[fname]) + ) + def assertNoMail(self, recipients, mail_message=None, author=None): """ Check no mail.mail and email was generated during gateway mock. """ try: @@ -435,8 +444,10 @@ class MockEmail(common.BaseCase): :param values: dictionary of additional values to check email content; """ base_expected = {} - for fname in ['reply_to', 'subject', 'attachments', 'body', 'references', - 'body_content', 'body_alternative_content', 'references_content']: + for fname in ['author_id', 'reply_to', + 'subject', 'body', 'body_content', 'body_alternative_content', + 'references', 'references_content', + 'attachments']: if fname in values: base_expected[fname] = values[fname] @@ -461,9 +472,14 @@ class MockEmail(common.BaseCase): debug_info = '-'.join('From: %s-To: %s' % (mail['email_from'], mail['email_to']) for mail in self._mails) if not bool(sent_mail) else '' self.assertTrue(bool(sent_mail), 'Expected mail from %s to %s not found in %s' % (expected['email_from'], expected['email_to'], debug_info)) - for val in ['reply_to', 'subject', 'references', 'attachments']: + for val in ['reply_to', 'subject', 'references']: 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' in expected: + self.assertEqual( + sorted(expected['attachments']), sorted(sent_mail['attachments']), + 'Value for %s: expected %s, received %s' % ('attachments', expected['attachments'], sent_mail['attachments']) + ) if 'attachments_info' in values: attachments = sent_mail['attachments'] for attachment_info in values['attachments_info']: @@ -473,12 +489,18 @@ class MockEmail(common.BaseCase): if attachment_info.get('type'): self.assertEqual(attachment[2], attachment_info['type']) self.assertEqual(len(values['attachments_info']), len(attachments)) - for val in ['body']: + for val in ['body', 'body_alternative']: if val in expected: - self.assertHtmlEqual(expected[val], sent_mail[val], 'Value for %s: expected %s, received %s' % (val, expected[val], sent_mail[val])) - for val in ['body_content', 'body_alternative', 'references_content']: + self.assertHtmlEqual( + expected[val], sent_mail[val], + 'Value for %s: expected %s, received %s' % (val, expected[val], sent_mail[val]) + ) + for val in ['body_content', 'body_alternative_content', 'references_content']: if val in expected: - self.assertIn(expected[val], sent_mail[val[:-8]], 'Value for %s: %s does not contain %s' % (val, sent_mail[val[:-8]], expected[val])) + self.assertIn( + expected[val], sent_mail[val[:-8]], + 'Value for %s: %s does not contain %s' % (val, sent_mail[val[:-8]], expected[val]) + ) class MailCase(MockEmail): @@ -496,10 +518,11 @@ class MailCase(MockEmail): ("UNKNOWN", "Unknown error") """ _test_context = { + 'mail_channel_nosubscribe': True, 'mail_create_nolog': True, 'mail_create_nosubscribe': True, 'mail_notrack': True, - 'no_reset_password': True + 'no_reset_password': True, } @classmethod @@ -507,7 +530,7 @@ class MailCase(MockEmail): return record.with_context( mail_create_nolog=False, mail_create_nosubscribe=False, - mail_notrack=False + mail_notrack=False, ) def flush_tracking(self): @@ -627,19 +650,19 @@ class MailCase(MockEmail): # ------------------------------------------------------------ @contextmanager - def assertSinglePostNotifications(self, recipients_info, message_info=None, mail_unlink_sent=False, sim_error=None): + def assertSinglePostNotifications(self, recipients_info, message_info=None, mail_unlink_sent=False): """ Shortcut to assertMsgNotifications when having a single message to check. """ r_info = dict(message_info if message_info else {}) r_info.setdefault('content', '') r_info['notif'] = recipients_info - with self.assertPostNotifications([r_info], mail_unlink_sent=mail_unlink_sent, sim_error=sim_error): + with self.assertPostNotifications([r_info], mail_unlink_sent=mail_unlink_sent): yield @contextmanager - def assertPostNotifications(self, recipients_info, mail_unlink_sent=False, sim_error=None): + def assertPostNotifications(self, recipients_info, mail_unlink_sent=False): """ Check content of notifications. """ try: - with self.mock_mail_gateway(mail_unlink_sent=mail_unlink_sent, sim_error=sim_error), self.mock_bus(), self.mock_mail_app(): + with self.mock_mail_gateway(mail_unlink_sent=mail_unlink_sent), self.mock_bus(), self.mock_mail_app(): yield finally: done_msgs, done_notifs = self.assertMailNotifications(self._new_msgs, recipients_info) @@ -659,7 +682,7 @@ class MailCase(MockEmail): @contextmanager def assertNoNotifications(self): try: - with self.mock_mail_gateway(mail_unlink_sent=False, sim_error=None), self.mock_bus(), self.mock_mail_app(): + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_bus(), self.mock_mail_app(): yield finally: self.assertFalse(bool(self._new_msgs)) @@ -891,9 +914,14 @@ class MailCommon(common.TransactionCase, MailCase): super(MailCommon, cls).setUpClass() # give default values for all email aliases and domain cls._init_mail_gateway() + cls._init_outgoing_gateway() # ensure admin configuration cls.user_admin = cls.env.ref('base.user_admin') - cls.user_admin.write({'notification_type': 'inbox'}) + cls.user_admin.write({ + 'country_id': cls.env.ref('base.be').id, + 'email': 'test.admin@test.example.com', + 'notification_type': 'inbox', + }) cls.partner_admin = cls.env.ref('base.partner_admin') cls.company_admin = cls.user_admin.company_id cls.company_admin.write({'email': 'company@example.com'}) @@ -905,9 +933,11 @@ class MailCommon(common.TransactionCase, MailCase): # test standard employee cls.user_employee = mail_new_test_user( - cls.env, login='employee', - groups='base.group_user,mail.group_mail_template_editor', + cls.env, company_id=cls.company_admin.id, + country_id=cls.env.ref('base.be').id, + groups='base.group_user,mail.group_mail_template_editor', + login='employee', name='Ernest Employee', notification_type='inbox', signature='--\nErnest' @@ -939,12 +969,26 @@ class MailCommon(common.TransactionCase, MailCase): groups='base.group_user', company_id=cls.company_2.id, company_ids=[(4, cls.company_2.id)], + email='enguerrand@example.com', name='Enguerrand Employee C2', notification_type='inbox', signature='--\nEnguerrand' ) cls.partner_employee_c2 = cls.user_employee_c2.partner_id + # test erp manager employee + cls.user_erp_manager = mail_new_test_user( + cls.env, + company_id=cls.company_2.id, + company_ids=[(6, 0, (cls.company_admin + cls.company_2).ids)], + email='etchenne@example.com', + groups='base.group_user,base.group_erp_manager,mail.group_mail_template_editor', + login='erp_manager', + name='Etchenne Tchagada', + notification_type='inbox', + signature='--\nEtchenne', + ) + @classmethod def _activate_multi_lang(cls, lang_code='es_ES', layout_arch_db=None, test_record=False, test_template=False): """ Summary of es_ES matching done here (a bit hardcoded to ease tests) @@ -1097,15 +1141,18 @@ class MailCommon(common.TransactionCase, MailCase): }) cls.env['ir.translation'].create(translations_tocreate) - def _generate_attachments_data(self, count, res_model=None, res_id=None): + def _generate_attachments_data(self, count, res_model=None, res_id=None, attach_values=None): # attachment visibility depends on what they are attached to + attach_values = attach_values or {} if res_model is None: res_model = self.template._name if res_id is None: res_id = self.template.id return [{ - 'name': '%02d.txt' % x, - 'datas': base64.b64encode(b'Att%02d' % x), + 'datas': base64.b64encode(b'AttContent_%02d' % x), + 'name': 'AttFileName_%02d.txt' % x, + 'mimetype': 'text/plain', 'res_model': res_model, 'res_id': res_id, + **attach_values, } for x in range(count)] diff --git a/addons/mail/tests/test_mail_channel_partner.py b/addons/mail/tests/test_mail_channel_partner.py index 8f6e8a66424..f7ba426648f 100644 --- a/addons/mail/tests/test_mail_channel_partner.py +++ b/addons/mail/tests/test_mail_channel_partner.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from functools import partial + from odoo.addons.mail.tests.common import mail_new_test_user from odoo.addons.mail.tests.common import MailCommon from odoo.exceptions import AccessError, UserError +mail_channel_new_test_user = partial(mail_new_test_user, context={'mail_channel_nosubscribe': False}) + class TestMailChannelMembers(MailCommon): @@ -22,23 +26,23 @@ class TestMailChannelMembers(MailCommon): 'res_id': cls.secret_group.id, }) - cls.user_1 = mail_new_test_user( + cls.user_1 = mail_channel_new_test_user( cls.env, login='user_1', name='User 1', groups='base.group_user,mail.secret_group') - cls.user_2 = mail_new_test_user( + cls.user_2 = mail_channel_new_test_user( cls.env, login='user_2', name='User 2', groups='base.group_user,mail.secret_group') - cls.user_3 = mail_new_test_user( + cls.user_3 = mail_channel_new_test_user( cls.env, login='user_3', name='User 3', groups='base.group_user,mail.secret_group') - cls.user_portal = mail_new_test_user( + cls.user_portal = mail_channel_new_test_user( cls.env, login='user_portal', name='User Portal', groups='base.group_portal') - cls.user_public = mail_new_test_user( + cls.user_public = mail_channel_new_test_user( cls.env, login='user_ublic', name='User Public', groups='base.group_public') diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py index ae43c3ae437..88759108d48 100644 --- a/addons/test_mail/tests/common.py +++ b/addons/test_mail/tests/common.py @@ -9,27 +9,29 @@ class TestMailCommon(MailCommon): """ Main entry point for functional tests. """ @classmethod - def _create_channel_listener(cls): - cls.channel_listen = cls.env['mail.channel'].with_context(cls._test_context).create({'name': 'Listener'}) - - @classmethod - def _create_records_for_batch(cls, model, count): - # TDE note: to be cleaned in master + def _create_records_for_batch(cls, model, count, additional_values=None): + additional_values = additional_values or {} records = cls.env[model] partners = cls.env['res.partner'] country_id = cls.env.ref('base.be').id - partners = cls.env['res.partner'].with_context(**cls._test_context).create([{ - 'name': 'Partner_%s' % (x), - 'email': '_test_partner_%s@example.com' % (x), - 'country_id': country_id, - 'mobile': '047500%02d%02d' % (x, x) - } for x in range(count)]) + base_values = [ + {'name': 'Test_%s' % idx, + **additional_values, + } for idx in range(count) + ] - records = cls.env[model].with_context(**cls._test_context).create([{ - 'name': 'Test_%s' % (x), - 'customer_id': partners[x].id, - } for x in range(count)]) + if 'customer_id' in cls.env[model]: + partners = cls.env['res.partner'].with_context(**cls._test_context).create([{ + 'name': 'Partner_%s' % idx, + 'email': '_test_partner_%s@example.com' % idx, + 'country_id': country_id, + 'mobile': '047500%02d%02d' % (idx, idx) + } for idx in range(count)]) + for values, partner in zip(base_values, partners): + values['customer_id'] = partner.id + + records = cls.env[model].with_context(**cls._test_context).create(base_values) cls.records = cls._reset_mail_context(records) cls.partners = partners diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 01c6587ae17..57f4adfbadd 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -18,7 +18,6 @@ class TestMailComposer(TestMailCommon, TestRecipients): @classmethod def setUpClass(cls): super(TestMailComposer, cls).setUpClass() - cls._init_mail_gateway() # ensure employee can create partners, necessary for templates cls.user_employee.write({ @@ -609,8 +608,8 @@ class TestComposerResultsComment(TestMailComposer): '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': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, + {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, ] }, @@ -624,8 +623,8 @@ class TestComposerResultsComment(TestMailComposer): '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': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, + {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, ] }, @@ -638,7 +637,7 @@ class TestComposerResultsComment(TestMailComposer): # 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]) + set(['AttFileName_00.txt', 'AttFileName_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)) diff --git a/addons/test_mail/tests/test_mail_composer_mixin.py b/addons/test_mail/tests/test_mail_composer_mixin.py index d13473f7801..5b85c34ed5c 100644 --- a/addons/test_mail/tests/test_mail_composer_mixin.py +++ b/addons/test_mail/tests/test_mail_composer_mixin.py @@ -12,7 +12,6 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients): @classmethod def setUpClass(cls): super(TestMailComposerMixin, cls).setUpClass() - cls._init_mail_gateway() cls.mail_template = cls.env['mail.template'].create({ 'subject': 'Subject for {{ object.name }}', diff --git a/addons/test_mail/tests/test_mail_followers.py b/addons/test_mail/tests/test_mail_followers.py index 8e6a54c0501..4a1e0d94258 100644 --- a/addons/test_mail/tests/test_mail_followers.py +++ b/addons/test_mail/tests/test_mail_followers.py @@ -14,7 +14,6 @@ class BaseFollowersTest(TestMailCommon): super(BaseFollowersTest, cls).setUpClass() cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) cls._create_portal_user() - cls._create_channel_listener() # allow employee to update partners cls.user_employee.write({'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)]}) diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py index aff9f63bf71..d5e1e669325 100644 --- a/addons/test_mail/tests/test_mail_gateway.py +++ b/addons/test_mail/tests/test_mail_gateway.py @@ -247,8 +247,6 @@ class TestMailgateway(TestMailCommon): 'message_id': '<123456-openerp-%s-mail.test.gateway@%s>' % (cls.test_record.id, socket.gethostname()), }) - cls._init_mail_gateway() - # -------------------------------------------------- # Base low-level tests # -------------------------------------------------- @@ -1277,8 +1275,6 @@ class TestMailThreadCC(TestMailCommon): 'alias_model_id': cls.env['ir.model']._get('mail.test.cc').id, 'alias_contact': 'everyone'}) - cls._init_mail_gateway() - @mute_logger('odoo.addons.mail.models.mail_thread') def test_message_cc_new(self): record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'cc_record@test.com', diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py index 04f48e1a992..0f587e54c02 100644 --- a/addons/test_mail/tests/test_mail_mail.py +++ b/addons/test_mail/tests/test_mail_mail.py @@ -5,19 +5,17 @@ import psycopg2 from unittest.mock import call from odoo import api -from odoo.addons.base.tests.common import MockSmtplibCase from odoo.addons.test_mail.tests.common import TestMailCommon from odoo.tests import common, tagged from odoo.tools import mute_logger @tagged('mail_mail') -class TestMailMail(TestMailCommon, MockSmtplibCase): +class TestMailMail(TestMailCommon): @classmethod def setUpClass(cls): super(TestMailMail, cls).setUpClass() - cls._init_mail_gateway() cls._init_mail_servers() cls.server_domain_2 = cls.env['ir.mail_server'].create({ @@ -133,6 +131,7 @@ class TestMailMail(TestMailCommon, MockSmtplibCase): self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain.from_filter) +@tagged('mail_mail') class TestMailMailRace(common.TransactionCase): @mute_logger('odoo.addons.mail.models.mail_mail') diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py index f4bfe292a68..0ebbc5e9f05 100644 --- a/addons/test_mail/tests/test_mail_message.py +++ b/addons/test_mail/tests/test_mail_message.py @@ -18,7 +18,6 @@ class TestMessageValues(TestMailCommon): def setUpClass(cls): super(TestMessageValues, cls).setUpClass() - cls._init_mail_gateway() cls.alias_record = cls.env['mail.test.container'].with_context(cls._test_context).create({ 'name': 'Pigs', 'alias_name': 'pigs', diff --git a/addons/test_mail/tests/test_mail_multicompany.py b/addons/test_mail/tests/test_mail_multicompany.py index 18082651e69..58309a7482f 100644 --- a/addons/test_mail/tests/test_mail_multicompany.py +++ b/addons/test_mail/tests/test_mail_multicompany.py @@ -47,8 +47,6 @@ class TestMultiCompanySetup(TestMailCommon, TestRecipients): 'message_id': '<123456-openerp-%s-mail.test.gateway@%s>' % (cls.test_record.id, socket.gethostname()), }) - cls._init_mail_gateway() - @users('employee') def test_notify_reply_to_computation(self): test_record = self.env['mail.test.gateway'].browse(self.test_record.ids) diff --git a/addons/test_mail/tests/test_mail_thread_internals.py b/addons/test_mail/tests/test_mail_thread_internals.py index 58c7a0b2538..56b9258070a 100644 --- a/addons/test_mail/tests/test_mail_thread_internals.py +++ b/addons/test_mail/tests/test_mail_thread_internals.py @@ -103,6 +103,7 @@ class TestChatterTweaks(TestMailCommon, TestRecipients): self.assertTrue(record.name) +@tagged('mail_thread') class TestDiscuss(TestMailCommon, TestRecipients): @classmethod @@ -239,7 +240,12 @@ class TestDiscuss(TestMailCommon, TestRecipients): partner_ids=[self.user_employee.partner_id.id] ) self.assertFalse(message.has_error) - with self.mock_mail_gateway(sim_error='connect_smtp_notfound'): + + with self.mock_mail_gateway(): + def _connect(*args, **kwargs): + raise Exception("Some exception") + self.connect_mocked.side_effect = _connect + self.user_admin.notification_type = 'email' message2 = self.test_record.with_user(self.user_employee).message_post( body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', diff --git a/addons/test_mail/tests/test_message_management.py b/addons/test_mail/tests/test_message_management.py index 57d24e47f92..e1e5ab203e9 100644 --- a/addons/test_mail/tests/test_message_management.py +++ b/addons/test_mail/tests/test_message_management.py @@ -34,8 +34,10 @@ class TestMailResend(TestMailCommon): def test_mail_resend_workflow(self): with self.assertSinglePostNotifications( [{'partner': partner, 'type': 'email', 'status': 'exception'} for partner in self.partners], - message_info={'message_type': 'notification'}, - sim_error='connect_failure'): + message_info={'message_type': 'notification'}): + def _connect(*args, **kwargs): + raise Exception("Some exception") + self.connect_mocked.side_effect = _connect message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification') wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}) diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 29a014a18a2..a6076f63606 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -48,6 +48,7 @@ class BaseMailPerformance(TransactionCaseWithUserDemo): # patch registry to simulate a ready environment self.patch(self.env.registry, 'ready', True) self._flush_tracking() + self._init_mail_gateway() def _init_mail_gateway(self): # setup mail gateway @@ -113,8 +114,6 @@ class TestBaseMailPerformance(BaseMailPerformance): } ]) - self._init_mail_gateway() - @users('__system__', 'demo') @warmup def test_read_mail(self): @@ -223,8 +222,6 @@ class TestMailAPIPerformance(BaseMailPerformance): 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])], }) - self._init_mail_gateway() - # automatically follow activities, for backward compatibility concerning query count self.env.ref('mail.mt_activities').write({'default': True}) @@ -531,10 +528,8 @@ class TestMailComplexPerformance(BaseMailPerformance): self.partners |= Partners.create({'name': 'Test %s' % x, 'email': 'test%s@example.com' % x}) self.container.message_subscribe(self.partners.ids, subtype_ids=[ self.env.ref('mail.mt_comment').id, - self.env.ref('test_mail.st_mail_test_container_child_full').id] - ) - - self._init_mail_gateway() + self.env.ref('test_mail.st_mail_test_container_child_full').id + ]) # `test_complex_mail_mail_send` self.container.flush() @@ -991,8 +986,6 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): 'res_id': 0, } for i in range(3)] - self._init_mail_gateway() - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') @users('employee') @warmup diff --git a/addons/test_mail_full/tests/test_sms_performance.py b/addons/test_mail_full/tests/test_sms_performance.py index 24108442039..1e63a832f0c 100644 --- a/addons/test_mail_full/tests/test_sms_performance.py +++ b/addons/test_mail_full/tests/test_sms_performance.py @@ -29,8 +29,6 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase): } for x in range(0, 10) ]) - self._init_mail_gateway() - @mute_logger('odoo.addons.sms.models.sms_sms') @users('employee') @warmup diff --git a/addons/test_mass_mailing/tests/test_mailing_server.py b/addons/test_mass_mailing/tests/test_mailing_server.py index 0bc1d35c222..53986f491a5 100644 --- a/addons/test_mass_mailing/tests/test_mailing_server.py +++ b/addons/test_mass_mailing/tests/test_mailing_server.py @@ -2,20 +2,17 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon -from odoo.addons.base.tests.common import MockSmtplibCase from odoo.tests import tagged from odoo.tests.common import users from odoo.tools import mute_logger @tagged('mass_mailing') -class TestMassMailingServer(TestMassMailCommon, MockSmtplibCase): +class TestMassMailingServer(TestMassMailCommon): @classmethod def setUpClass(cls): super(TestMassMailingServer, cls).setUpClass() - cls._init_mail_gateway() - cls.env['ir.mail_server'].search([]).unlink() cls._init_mail_servers() cls.recipients = cls._create_mailing_test_records(model='mailing.test.optout', count=8) diff --git a/odoo/addons/base/tests/common.py b/odoo/addons/base/tests/common.py index 0fd845209db..28ddff19b06 100644 --- a/odoo/addons/base/tests/common.py +++ b/odoo/addons/base/tests/common.py @@ -252,14 +252,14 @@ class MockSmtplibCase: self.testing_smtp_session = TestingSMTPSession() IrMailServer = self.env['ir.mail_server'] - connect = IrMailServer.connect - find_mail_server = IrMailServer._find_mail_server + connect_origin = IrMailServer.connect + find_mail_server_origin = IrMailServer._find_mail_server - with patch.object(type(IrMailServer), '_is_test_mode', lambda self: False), \ - patch('smtplib.SMTP_SSL', side_effect=lambda *args, **kwargs: self.testing_smtp_session), \ + with patch('smtplib.SMTP_SSL', side_effect=lambda *args, **kwargs: self.testing_smtp_session), \ patch('smtplib.SMTP', side_effect=lambda *args, **kwargs: self.testing_smtp_session), \ - patch.object(type(IrMailServer), 'connect', side_effect=connect) as connect_mocked, \ - patch.object(type(IrMailServer), '_find_mail_server', side_effect=find_mail_server) as find_mail_server_mocked: + patch.object(type(IrMailServer), '_is_test_mode', lambda self: False), \ + patch.object(type(IrMailServer), 'connect', wraps=IrMailServer, side_effect=connect_origin) as connect_mocked, \ + patch.object(type(IrMailServer), '_find_mail_server', side_effect=find_mail_server_origin) as find_mail_server_mocked: self.connect_mocked = connect_mocked self.find_mail_server_mocked = find_mail_server_mocked yield @@ -298,14 +298,16 @@ class MockSmtplibCase: ) @classmethod - def _init_mail_servers(cls): - cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test.com') - cls.env['ir.config_parameter'].sudo().set_param('mail.default.from', 'notifications') - cls.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', 'bounce') - - cls.alias_bounce = 'bounce' + def _init_mail_config(cls): + cls.alias_bounce = 'bounce.test' cls.alias_domain = 'test.com' + cls.default_from = 'notifications' + cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', cls.alias_domain) + cls.env['ir.config_parameter'].sudo().set_param('mail.default.from', cls.default_from) + cls.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', cls.alias_bounce) + @classmethod + def _init_mail_servers(cls): cls.env['ir.mail_server'].search([]).unlink() ir_mail_server_values = { diff --git a/odoo/addons/base/tests/test_ir_mail_server.py b/odoo/addons/base/tests/test_ir_mail_server.py index 448f04c2d06..c2a330067f9 100644 --- a/odoo/addons/base/tests/test_ir_mail_server.py +++ b/odoo/addons/base/tests/test_ir_mail_server.py @@ -15,6 +15,7 @@ from odoo.tools import mute_logger class TestIrMailServer(TransactionCase, MockSmtplibCase): def setUp(self): + self._init_mail_config() self._init_mail_servers() def _build_email(self, mail_from, return_path=None): @@ -231,7 +232,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): self.assertEqual(len(self.emails), 1) self.assert_email_sent_smtp( - smtp_from='bounce@xn--9caaaaaaa.com', + smtp_from='bounce.test@xn--9caaaaaaa.com', smtp_to_list=['dest@xn--example--i1a.com'], message_from='test@=?utf-8?b?w6nDqcOpw6nDqcOpw6k=?=.com', from_filter=False, @@ -322,7 +323,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ) @mute_logger('odoo.models.unlink') - @patch.dict("odoo.tools.config.options", {"from_filter": "test.com"}) + @patch.dict("odoo.tools.config.options", {"from_filter": "test.com", "smtp_server": "example.com"}) def test_mail_server_binary_arguments_domain(self): """Test the configuration provided in the odoo-bin arguments. @@ -374,7 +375,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ) @mute_logger('odoo.models.unlink') - @patch.dict("odoo.tools.config.options", {"from_filter": "test.com"}) + @patch.dict("odoo.tools.config.options", {"from_filter": "test.com", "smtp_server": "example.com"}) def test_mail_server_binary_arguments_domain_smtp_session(self): """Test the configuration provided in the odoo-bin arguments. @@ -417,7 +418,7 @@ class TestIrMailServer(TransactionCase, MockSmtplibCase): ) @mute_logger('odoo.models.unlink') - @patch.dict('odoo.tools.config.options', {'from_filter': 'test.com'}) + @patch.dict('odoo.tools.config.options', {'from_filter': 'test.com', 'smtp_server': 'example.com'}) def test_mail_server_mail_default_from_filter(self): """Test that the config parameter "mail.default.from_filter" overwrite the odoo-bin argument "--from-filter" From e31504f3a3dc77072a23f61e585dd970fc17d2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 23 Mar 2022 09:00:27 +0000 Subject: [PATCH 3/9] [IMP] test_mail: make performance test use standard mail test class PURPOSE Improve tooling of mail tests. Lessen custom code, try to use same tools in all tests. SPECIFICATIONS Use MailCommon as a base class for performance test. It now uses the same boilerplate as other tests, leading to easier to reproduce and understand testing setup. This notably now adds outgoing mail servers which means adding one query to some tests involving emails. Task-2673913 (TestMail: Cleanup and improve test coverage) Part-of: odoo/odoo#86393 --- addons/test_mail/tests/test_performance.py | 102 ++++++------------ .../tests/test_sms_performance.py | 8 +- .../tests/test_performance.py | 46 +++----- 3 files changed, 55 insertions(+), 101 deletions(-) diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index a6076f63606..d02a2ef0006 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -1,42 +1,26 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + import base64 from odoo.addons.base.tests.common import TransactionCaseWithUserDemo +from odoo.addons.mail.tests.common import MailCommon from odoo.tests.common import users, warmup from odoo.tests import tagged from odoo.tools import mute_logger, formataddr @tagged('mail_performance') -class BaseMailPerformance(TransactionCaseWithUserDemo): +class BaseMailPerformance(MailCommon, TransactionCaseWithUserDemo): @classmethod def setUpClass(cls): super(BaseMailPerformance, cls).setUpClass() - cls._quick_create_ctx = { - 'no_reset_password': True, - 'mail_create_nolog': True, - 'mail_create_nosubscribe': True, - 'mail_notrack': True, - 'mail_channel_nosubscribe': True, - } - cls.user_employee = cls.env['res.users'].with_context(cls._quick_create_ctx).create({ - 'country_id': cls.env.ref('base.be').id, - 'email': 'e.e@example.com', - 'groups_id': [(6, 0, [cls.env.ref('base.group_user').id, cls.env.ref('base.group_partner_manager').id])], - 'login': 'employee', - 'name': 'Ernest Employee', - 'notification_type': 'inbox', - 'signature': '--\nErnest', - }) - cls.admin = cls.env.user - cls.admin.write({ - 'country_id': cls.env.ref('base.be').id, - }) + # creating partners is required notably witn template usage + cls.user_employee.write({'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)]}) - cls.customer = cls.env['res.partner'].with_context(cls._quick_create_ctx).create({ + cls.customer = cls.env['res.partner'].with_context(cls._test_context).create({ 'country_id': cls.env.ref('base.be').id, 'email': 'customer.test@example.com', 'name': 'Test Customer', @@ -47,25 +31,7 @@ class BaseMailPerformance(TransactionCaseWithUserDemo): super(BaseMailPerformance, self).setUp() # patch registry to simulate a ready environment self.patch(self.env.registry, 'ready', True) - self._flush_tracking() - self._init_mail_gateway() - - def _init_mail_gateway(self): - # setup mail gateway - self.alias_domain = 'example.com' - self.alias_catchall = 'catchall.test' - self.alias_bounce = 'bounce.test' - self.default_from = 'notifications' - self.env['ir.config_parameter'].set_param('mail.bounce.alias', self.alias_bounce) - self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain) - self.env['ir.config_parameter'].set_param('mail.catchall.alias', self.alias_catchall) - self.env['ir.config_parameter'].set_param('mail.default.from', self.default_from) - - def _flush_tracking(self): - """ Force the creation of tracking values notably, and ensure tests are - reproducible. """ - self.env['base'].flush() - self.cr.flush() + self.flush_tracking() @tagged('mail_performance') @@ -214,7 +180,7 @@ class TestMailAPIPerformance(BaseMailPerformance): def setUp(self): super(TestMailAPIPerformance, self).setUp() - self.user_test = self.env['res.users'].with_context(self._quick_create_ctx).create({ + self.user_test = self.env['res.users'].with_context(self._test_context).create({ 'name': 'Paulette Testouille', 'login': 'paul', 'email': 'user.test.paulette@example.com', @@ -226,7 +192,7 @@ class TestMailAPIPerformance(BaseMailPerformance): self.env.ref('mail.mt_activities').write({'default': True}) def _create_test_records(self): - self.test_record_full = self.env['mail.test.ticket'].with_context(self._quick_create_ctx).create({ + self.test_record_full = self.env['mail.test.ticket'].with_context(self._test_context).create({ 'name': 'TestRecord', 'customer_id': self.customer.id, 'user_id': self.user_test.id, @@ -293,7 +259,7 @@ class TestMailAPIPerformance(BaseMailPerformance): @users('__system__', 'employee') @warmup - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') def test_mail_composer(self): self._create_test_records() test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) @@ -308,7 +274,7 @@ class TestMailAPIPerformance(BaseMailPerformance): 'partner_ids': [(4, customer_id)], }) - with self.assertQueryCount(__system__=32, employee=38): + with self.assertQueryCount(__system__=33, employee=39): composer._action_send_mail() @users('__system__', 'employee') @@ -329,7 +295,7 @@ class TestMailAPIPerformance(BaseMailPerformance): 'partner_ids': [(4, customer_id)], }) - with self.assertQueryCount(__system__=25, employee=31): + with self.assertQueryCount(__system__=26, employee=32): composer._action_send_mail() @users('__system__', 'employee') @@ -348,7 +314,7 @@ class TestMailAPIPerformance(BaseMailPerformance): }).create({}) composer._onchange_template_id_wrapper() - with self.assertQueryCount(__system__=32, employee=38): + with self.assertQueryCount(__system__=33, employee=39): composer._action_send_mail() # remove created partner to ensure tests are the same each run @@ -360,7 +326,7 @@ class TestMailAPIPerformance(BaseMailPerformance): def test_message_assignation_email(self): self.user_test.write({'notification_type': 'email'}) record = self.env['mail.test.track'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=38, employee=39): + with self.assertQueryCount(__system__=39, employee=40): record.write({ 'user_id': self.user_test.id, }) @@ -413,7 +379,7 @@ class TestMailAPIPerformance(BaseMailPerformance): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=29, employee=32): + with self.assertQueryCount(__system__=30, employee=33): record.message_post( body='

Test Post Performances with an email ping

', partner_ids=self.customer.ids, @@ -508,7 +474,7 @@ class TestMailComplexPerformance(BaseMailPerformance): def setUp(self): super(TestMailComplexPerformance, self).setUp() - self.user_portal = self.env['res.users'].with_context(self._quick_create_ctx).create({ + self.user_portal = self.env['res.users'].with_context(self._test_context).create({ 'name': 'Olivia Portal', 'login': 'port', 'email': 'p.p@example.com', @@ -522,7 +488,7 @@ class TestMailComplexPerformance(BaseMailPerformance): 'customer_id': self.customer.id, 'alias_name': 'test-alias', }) - Partners = self.env['res.partner'].with_context(self._quick_create_ctx) + Partners = self.env['res.partner'].with_context(self._test_context) self.partners = self.env['res.partner'] for x in range(0, 10): self.partners |= Partners.create({'name': 'Test %s' % x, 'email': 'test%s@example.com' % x}) @@ -552,11 +518,13 @@ class TestMailComplexPerformance(BaseMailPerformance): 'recipient_ids': [(4, pid) for pid in self.partners.ids], }) mail_ids = mail.ids - with self.assertQueryCount(__system__=8, employee=8): + with self.assertQueryCount(__system__=9, employee=9): self.env['mail.mail'].sudo().browse(mail_ids).send() self.assertEqual(mail.body_html, '

Test

') - self.assertEqual(mail.reply_to, formataddr(('%s %s' % (self.env.company.name, self.container.name), 'test-alias@example.com'))) + self.assertEqual(mail.reply_to, formataddr( + ('%s %s' % (self.env.company.name, self.container.name), 'test-alias@%s' % self.alias_domain) + )) @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') @users('__system__', 'employee') @@ -565,7 +533,7 @@ class TestMailComplexPerformance(BaseMailPerformance): self.container.message_subscribe(self.user_portal.partner_id.ids) record = self.container.with_user(self.env.user) - with self.assertQueryCount(__system__=53, employee=54): # about 20 (19?) queries per additional customer group + with self.assertQueryCount(__system__=54, employee=55): # about 20 (19?) queries per additional customer group record.message_post( body='

Test Post Performances

', message_type='comment', @@ -582,7 +550,7 @@ class TestMailComplexPerformance(BaseMailPerformance): record = self.container.with_user(self.env.user) template_id = self.env.ref('test_mail.mail_test_container_tpl').id - with self.assertQueryCount(__system__=61, employee=62): # about 20 (19 ?) queries per additional customer group + with self.assertQueryCount(__system__=62, employee=63): # about 20 (19 ?) queries per additional customer group record.message_post_with_template(template_id, message_type='comment', composition_mode='comment') self.assertEqual(record.message_ids[0].body, '

Adding stuff on %s

' % record.name) @@ -645,7 +613,7 @@ class TestMailComplexPerformance(BaseMailPerformance): }) rec1 = rec.with_context(active_test=False) # to see inactive records self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id) - with self.assertQueryCount(__system__=38, employee=39): + with self.assertQueryCount(__system__=39, employee=40): rec.write({'user_id': self.user_portal.id}) self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) # write tracking message @@ -665,7 +633,7 @@ class TestMailComplexPerformance(BaseMailPerformance): customer_id = self.customer.id user_id = self.user_portal.id - with self.assertQueryCount(__system__=89, employee=90): + with self.assertQueryCount(__system__=91, employee=92): rec = self.env['mail.test.ticket'].create({ 'name': 'Test', 'container_id': container_id, @@ -694,7 +662,7 @@ class TestMailComplexPerformance(BaseMailPerformance): rec1 = rec.with_context(active_test=False) # to see inactive records self.assertEqual(rec1.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) self.assertEqual(len(rec1.message_ids), 1) - with self.assertQueryCount(__system__=58, employee=58): + with self.assertQueryCount(__system__=59, employee=59): rec.write({ 'name': 'Test2', 'container_id': self.container.id, @@ -731,7 +699,7 @@ class TestMailComplexPerformance(BaseMailPerformance): rec1 = rec.with_context(active_test=False) # to see inactive records self.assertEqual(rec1.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id) - with self.assertQueryCount(__system__=65, employee=65): + with self.assertQueryCount(__system__=66, employee=66): rec.write({ 'name': 'Test2', 'container_id': container_id, @@ -764,7 +732,7 @@ class TestMailComplexPerformance(BaseMailPerformance): rec1 = rec.with_context(active_test=False) # to see inactive records self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) - with self.assertQueryCount(__system__=28, employee=29): + with self.assertQueryCount(__system__=29, employee=30): rec.write({ 'name': 'Test2', 'customer_id': customer_id, @@ -935,21 +903,21 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): 'alias_name': 'test-alias', }) # followers - self.user_follower_email = self.env['res.users'].with_context(self._quick_create_ctx).create({ + self.user_follower_email = self.env['res.users'].with_context(self._test_context).create({ 'name': 'user_follower_email', 'login': 'user_follower_email', 'email': 'user_follower_email@example.com', 'notification_type': 'email', 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])], }) - self.user_follower_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({ + self.user_follower_inbox = self.env['res.users'].with_context(self._test_context).create({ 'name': 'user_follower_inbox', 'login': 'user_follower_inbox', 'email': 'user_follower_inbox@example.com', 'notification_type': 'inbox', 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])], }) - self.partner_follower = self.env['res.partner'].with_context(self._quick_create_ctx).create({ + self.partner_follower = self.env['res.partner'].with_context(self._test_context).create({ 'name': 'partner_follower', 'email': 'partner_follower@example.com', }) @@ -960,21 +928,21 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): ]) # partner_ids - self.user_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({ + self.user_inbox = self.env['res.users'].with_context(self._test_context).create({ 'name': 'user_inbox', 'login': 'user_inbox', 'email': 'user_inbox@example.com', 'notification_type': 'inbox', 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])], }) - self.user_email = self.env['res.users'].with_context(self._quick_create_ctx).create({ + self.user_email = self.env['res.users'].with_context(self._test_context).create({ 'name': 'user_email', 'login': 'user_email', 'email': 'user_email@example.com', 'notification_type': 'email', 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])], }) - self.partner = self.env['res.partner'].with_context(self._quick_create_ctx).create({ + self.partner = self.env['res.partner'].with_context(self._test_context).create({ 'name': 'partner', 'email': 'partner@example.com', }) @@ -1000,7 +968,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): ] self.attachements = self.env['ir.attachment'].with_user(self.env.user).create(self.vals) attachement_ids = self.attachements.ids - with self.assertQueryCount(employee=67): + with self.assertQueryCount(employee=68): self.cr.sql_log = self.warm and self.cr.sql_log_count record.with_context({}).message_post( body='

Test body

', diff --git a/addons/test_mail_full/tests/test_sms_performance.py b/addons/test_mail_full/tests/test_sms_performance.py index 1e63a832f0c..95439be6c76 100644 --- a/addons/test_mail_full/tests/test_sms_performance.py +++ b/addons/test_mail_full/tests/test_sms_performance.py @@ -14,14 +14,14 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.SMSCase): def setUp(self): super(TestSMSPerformance, self).setUp() - self.test_record = self.env['mail.test.sms'].with_context(self._quick_create_ctx).create({ + self.test_record = self.env['mail.test.sms'].with_context(self._test_context).create({ 'name': 'Test', 'customer_id': self.customer.id, 'phone_nbr': '0456999999', }) # prepare recipients to test for more realistic workload - self.partners = self.env['res.partner'].with_context(self._quick_create_ctx).create([ + self.partners = self.env['res.partner'].with_context(self._test_context).create([ {'name': 'Test %s' % x, 'email': 'test%s@example.com' % x, 'mobile': '0456%s%s0000' % (x, x), @@ -85,13 +85,13 @@ class TestSMSMassPerformance(BaseMailPerformance, sms_common.MockSMS): records = self.env['mail.test.sms'] partners = self.env['res.partner'] for x in range(50): - partners += self.env['res.partner'].with_context(**self._quick_create_ctx).create({ + partners += self.env['res.partner'].with_context(**self._test_context).create({ 'name': 'Partner_%s' % (x), 'email': '_test_partner_%s@example.com' % (x), 'country_id': be_country_id, 'mobile': '047500%02d%02d' % (x, x) }) - records += self.env['mail.test.sms'].with_context(**self._quick_create_ctx).create({ + records += self.env['mail.test.sms'].with_context(**self._test_context).create({ 'name': 'Test_%s' % (x), 'customer_id': partners[x].id, }) diff --git a/addons/test_mass_mailing/tests/test_performance.py b/addons/test_mass_mailing/tests/test_performance.py index d5f2d2ca6e4..57c1e674b43 100644 --- a/addons/test_mass_mailing/tests/test_performance.py +++ b/addons/test_mass_mailing/tests/test_performance.py @@ -2,39 +2,25 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.mail.tests.common import mail_new_test_user -from odoo.tests.common import TransactionCase, users, warmup +from odoo.addons.test_mail.tests.test_performance import BaseMailPerformance +from odoo.tests.common import users, warmup from odoo.tests import tagged from odoo.tools import mute_logger -class TestMassMailPerformanceBase(TransactionCase): +class TestMassMailPerformanceBase(BaseMailPerformance): - def setUp(self): - super(TestMassMailPerformanceBase, self).setUp() + @classmethod + def setUpClass(cls): + super(TestMassMailPerformanceBase, cls).setUpClass() - self.user_employee = mail_new_test_user( - self.env, login='emp', - groups='base.group_user', - name='Ernest Employee', notification_type='inbox') - - self.user_marketing = mail_new_test_user( - self.env, login='marketing', + cls.user_marketing = mail_new_test_user( + cls.env, groups='base.group_user,mass_mailing.group_mass_mailing_user', - name='Martial Marketing', signature='--\nMartial') - - # setup mail gateway - self.alias_domain = 'example.com' - self.alias_catchall = 'catchall.test' - self.alias_bounce = 'bounce.test' - self.default_from = 'notifications' - self.env['ir.config_parameter'].set_param('mail.bounce.alias', self.alias_bounce) - self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain) - self.env['ir.config_parameter'].set_param('mail.catchall.alias', self.alias_catchall) - self.env['ir.config_parameter'].set_param('mail.default.from', self.default_from) - - # patch registry to simulate a ready environment - self.patch(self.env.registry, 'ready', True) - + login='marketing', + name='Martial Marketing', + signature='--\nMartial' + ) @tagged('mail_performance') class TestMassMailPerformance(TestMassMailPerformanceBase): @@ -60,8 +46,8 @@ class TestMassMailPerformance(TestMassMailPerformanceBase): 'mailing_domain': [('id', 'in', self.mm_recs.ids)], }) - # runbot needs +101 compared to local (1568) - with self.assertQueryCount(__system__=1670, marketing=1671): + # runbot needs +101 compared to local (1570) + with self.assertQueryCount(__system__=1672, marketing=1673): mailing.action_send_mail() self.assertEqual(mailing.sent, 50) @@ -100,8 +86,8 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase): 'mailing_domain': [('id', 'in', self.mm_recs.ids)], }) - # runbot needs +125 compared to local (1834) - with self.assertQueryCount(__system__=1960, marketing=1961): + # runbot needs +125 compared to local (1836) + with self.assertQueryCount(__system__=1962, marketing=1963): mailing.action_send_mail() self.assertEqual(mailing.sent, 50) From d895f3514e0022737c685eee24e2c8213f1f278a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 8 Feb 2022 18:01:16 +0000 Subject: [PATCH 4/9] [IMP] test_mail: add tests for mail failure management PURPOSE Add tests related to mail failure management and its effect on mail.mail and related mail.notification records. SPECIFICATIONS Test wrong configurations when sending emails (email_from, email_to) and/or use mocks to enable sending issues (SMTP issues, Memory errors, assertion errors, ...) that may happen when sending emails. Add unit tests for as much use cases as possible to assert current behavior even if some tests show that it is not always accurate. Notably * invalid / missing email failure types that are not correctly distinguished even if it should; * email_from invalid value goes a bit incognito; * sometimes mail is marked as sent even if it was not sent; Future commits will (probably) fix or improve behavior while impacting as less as possible the global behavior of the outgoing gateway that is quite tricky to modify. Task-2673913 (TestMail: Cleanup and improve test coverage) Part-of: odoo/odoo#86393 --- addons/test_mail/tests/test_mail_mail.py | 396 ++++++++++++++++++++++- 1 file changed, 394 insertions(+), 2 deletions(-) diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py index 0f587e54c02..90688b004b8 100644 --- a/addons/test_mail/tests/test_mail_mail.py +++ b/addons/test_mail/tests/test_mail_mail.py @@ -2,9 +2,14 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import psycopg2 +import smtplib + +from OpenSSL.SSL import Error as SSLError +from socket import gaierror, timeout from unittest.mock import call from odoo import api +from odoo.addons.base.models.ir_mail_server import MailDeliveryException from odoo.addons.test_mail.tests.common import TestMailCommon from odoo.tests import common, tagged from odoo.tools import mute_logger @@ -29,9 +34,36 @@ class TestMailMail(TestMailCommon): 'email_from': 'ignasse@example.com', }).with_context({}) + cls.test_message = cls.test_record.message_post(body='

Message

', subject='Subject') + cls.test_mail = cls.env['mail.mail'].create([{ + 'body': '

Body

', + 'email_from': False, + 'email_to': 'test@example.com', + 'is_notification': True, + 'subject': 'Subject', + }]) + cls.test_notification = cls.env['mail.notification'].create({ + 'is_read': False, + 'mail_mail_id': cls.test_mail.id, + 'mail_message_id': cls.test_message.id, + 'notification_type': 'email', + 'res_partner_id': cls.partner_employee.id, # not really used for matching except multi-recipients + }) + + cls.emails_falsy = [False, '', ' '] + cls.emails_invalid = ['buggy', 'buggy, wrong'] + cls.emails_invalid_ascii = ['raoul@example¢¡.com'] + cls.emails_valid = ['raoul¢¡@example.com', 'raoul@example.com'] + + def _reset_data(self): + self._init_mail_mock() + self.test_mail.write({'failure_reason': False, 'failure_type': False, 'state': 'outgoing'}) + self.test_notification.write({'failure_type': False, 'notification_status': 'ready'}) + + @mute_logger('odoo.addons.mail.models.mail_mail') - def test_mail_message_notify_from_mail_mail(self): - # Due ot post-commit hooks, store send emails in every step + def test_mail_mail_recipients(self): + """ Partner_ids is a field used from mail_message, but not from mail_mail. """ mail = self.env['mail.mail'].sudo().create({ 'body_html': '

Test

', 'email_to': 'test@example.com', @@ -42,6 +74,18 @@ class TestMailMail(TestMailCommon): self.assertSentEmail(mail.env.user.partner_id, ['test@example.com']) self.assertEqual(len(self._mails), 1) + mail = self.env['mail.mail'].sudo().create({ + 'body_html': '

Test

', + 'email_to': 'test@example.com', + 'recipient_ids': [(4, self.user_employee.partner_id.id)], + }) + with self.mock_mail_gateway(): + mail.send() + self.assertSentEmail(mail.env.user.partner_id, ['test@example.com']) + self.assertSentEmail(mail.env.user.partner_id, [self.user_employee.email_formatted]) + self.assertEqual(len(self._mails), 2) + + @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_mail_return_path(self): # mail without thread-enabled record base_values = { @@ -63,6 +107,354 @@ class TestMailMail(TestMailCommon): mail.send() self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain)) + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_send_exceptions_origin(self): + """ Test various use case with exceptions and errors and see how they are + managed and stored at mail and notification level. """ + mail, notification = self.test_mail, self.test_notification + + # MailServer.build_email(): invalid from + self.env['ir.config_parameter'].set_param('mail.default.from', '') + self._reset_data() + with self.mock_mail_gateway(), mute_logger('odoo.addons.mail.models.mail_mail'): + mail.send(raise_exception=False) + self.assertFalse(self._mails[0]['email_from']) + self.assertIn( + 'You must either provide a sender address explicitly or configure using the combination of `mail.catchall.domain` and `mail.default.from` ICPs', + mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: void from: no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'unknown', 'Mail: void from: unknown failure type, should be updated') + self.assertEqual(notification.notification_status, 'exception') + + # MailServer.send_email(): _prepare_email_message: unexpected ASCII + # Force catchall domain to void otherwise bounce is set to postmaster-odoo@domain + self.env['ir.config_parameter'].set_param('mail.catchall.domain', '') + self._reset_data() + mail.write({'email_from': 'strange@example¢¡.com'}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertEqual(self._mails[0]['email_from'], 'strange@example¢¡.com') + self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: bugged from (ascii): no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged from (ascii): unknown failure type, should be updated') + self.assertEqual(notification.notification_status, 'exception') + + # MailServer.send_email(): _prepare_email_message: unexpected ASCII based on catchall domain + self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'domain¢¡.com') + self._reset_data() + mail.write({'email_from': 'test.user@example.com'}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertEqual(self._mails[0]['email_from'], 'test.user@example.com') + self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: bugged catchall domain (ascii): no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged catchall domain (ascii): unknown failure type, should be updated') + self.assertEqual(notification.notification_status, 'exception') + + # MailServer.send_email(): _prepare_email_message: Malformed 'Return-Path' or 'From' address + self.env['ir.config_parameter'].set_param('mail.catchall.domain', '') + self._reset_data() + mail.write({'email_from': 'robert'}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertEqual(self._mails[0]['email_from'], 'robert') + self.assertIn("Malformed 'Return-Path' or 'From' address: robert", mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: bugged from (ascii): no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged from (ascii): unknown failure type, should be updated') + self.assertEqual(notification.notification_status, 'exception') + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_send_exceptions_recipients_emails(self): + """ Test various use case with exceptions and errors and see how they are + managed and stored at mail and notification level. """ + mail, notification = self.test_mail, self.test_notification + + self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain) + self.env['ir.config_parameter'].set_param('mail.default.from', self.default_from) + + # MailServer.send_email(): _prepare_email_message: missing To + for email_to in self.emails_falsy: + self._reset_data() + mail.write({'email_to': email_to}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertIn('Error without exception. Probably due do sending an email without computed recipients.', mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: missing email_to: no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + if email_to == ' ': + self.assertEqual(notification.failure_type, 'mail_email_missing') + self.assertEqual(notification.notification_status, 'exception') + else: + self.assertEqual(notification.failure_type, False, 'Mail: missing email_to: notification is wrongly set as sent') + self.assertEqual(notification.notification_status, 'sent', 'Mail: missing email_to: notification is wrongly set as sent') + + # MailServer.send_email(): _prepare_email_message: invalid To + for email_to, failure_type in zip(self.emails_invalid, + ['mail_email_missing', 'mail_email_missing']): + self._reset_data() + mail.write({'email_to': email_to}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertIn('Error without exception. Probably due do sending an email without computed recipients.', mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: invalid email_to: no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, failure_type, 'Mail: invalid email_to: missing instead of invalid') + self.assertEqual(notification.notification_status, 'exception') + + # MailServer.send_email(): _prepare_email_message: invalid To (ascii) + for email_to in self.emails_invalid_ascii: + self._reset_data() + mail.write({'email_to': email_to}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: invalid (ascii) recipient partner: no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'unknown', 'Mail: invalid (ascii) recipient partner: unknown failure type, should be updated') + self.assertEqual(notification.notification_status, 'exception') + + # MailServer.send_email(): _prepare_email_message: ok To (ascii or just ok) + for email_to in self.emails_valid: + self._reset_data() + mail.write({'email_to': email_to}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertFalse(mail.failure_reason) + self.assertFalse(mail.failure_type) + self.assertEqual(mail.state, 'sent') + self.assertFalse(notification.failure_type) + self.assertEqual(notification.notification_status, 'sent') + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_send_exceptions_recipients_partners(self): + """ Test various use case with exceptions and errors and see how they are + managed and stored at mail and notification level. """ + mail, notification = self.test_mail, self.test_notification + + mail.write({'email_from': 'test.user@test.example.com', 'email_to': False}) + partners_falsy = self.env['res.partner'].create([ + {'name': 'Name %s' % email, 'email': email} + for email in self.emails_falsy + ]) + partners_invalid = self.env['res.partner'].create([ + {'name': 'Name %s' % email, 'email': email} + for email in self.emails_invalid + ]) + partners_invalid_ascii = self.env['res.partner'].create([ + {'name': 'Name %s' % email, 'email': email} + for email in self.emails_invalid_ascii + ]) + partners_valid = self.env['res.partner'].create([ + {'name': 'Name %s' % email, 'email': email} + for email in self.emails_valid + ]) + + # void values + for partner in partners_falsy: + self._reset_data() + mail.write({'recipient_ids': [(5, 0), (4, partner.id)]}) + notification.write({'res_partner_id': partner.id}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertEqual(mail.failure_reason, 'Error without exception. Probably due do sending an email without computed recipients.') + self.assertFalse(mail.failure_type, 'Mail: void recipient partner: no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'mail_email_invalid', 'Mail: void recipient partner: should be missing, not invalid') + self.assertEqual(notification.notification_status, 'exception') + + # wrong values + for partner in partners_invalid: + self._reset_data() + mail.write({'recipient_ids': [(5, 0), (4, partner.id)]}) + notification.write({'res_partner_id': partner.id}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertEqual(mail.failure_reason, 'Error without exception. Probably due do sending an email without computed recipients.') + self.assertFalse(mail.failure_type, 'Mail: invalid recipient partner: no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'mail_email_invalid') + self.assertEqual(notification.notification_status, 'exception') + + # ascii ko + for partner in partners_invalid_ascii: + self._reset_data() + mail.write({'recipient_ids': [(5, 0), (4, partner.id)]}) + notification.write({'res_partner_id': partner.id}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: invalid (ascii) recipient partner: no failure type, should be updated') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'unknown', 'Mail: invalid (ascii) recipient partner: unknown failure type, should be updated') + self.assertEqual(notification.notification_status, 'exception') + + # ascii ok or just ok + for partner in partners_valid: + self._reset_data() + mail.write({'recipient_ids': [(5, 0), (4, partner.id)]}) + notification.write({'res_partner_id': partner.id}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertFalse(mail.failure_reason) + self.assertFalse(mail.failure_type) + self.assertEqual(mail.state, 'sent') + self.assertFalse(notification.failure_type) + self.assertEqual(notification.notification_status, 'sent') + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_send_exceptions_recipients_partners_mixed(self): + """ Test various use case with exceptions and errors and see how they are + managed and stored at mail and notification level. """ + mail, notification = self.test_mail, self.test_notification + + mail.write({'email_to': 'test@example.com'}) + partners_falsy = self.env['res.partner'].create([ + {'name': 'Name %s' % email, 'email': email} + for email in self.emails_falsy + ]) + partners_invalid = self.env['res.partner'].create([ + {'name': 'Name %s' % email, 'email': email} + for email in self.emails_invalid + ]) + partners_valid = self.env['res.partner'].create([ + {'name': 'Name %s' % email, 'email': email} + for email in self.emails_valid + ]) + + # valid to, missing email for recipient or wrong email for recipient + for partner in partners_falsy + partners_invalid: + self._reset_data() + mail.write({'recipient_ids': [(5, 0), (4, partner.id)]}) + notification.write({'res_partner_id': partner.id}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertFalse(mail.failure_reason, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertEqual(mail.state, 'sent', 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertEqual(notification.failure_type, 'mail_email_invalid', 'Mail: void email considered as invalid') + self.assertEqual(notification.notification_status, 'exception') + + # update to have valid partner and invalid partner + mail.write({'recipient_ids': [(5, 0), (4, partners_valid[1].id), (4, partners_falsy[0].id)]}) + notification.write({'res_partner_id': partners_valid[1].id}) + notification2 = notification.create({ + 'is_read': False, + 'mail_mail_id': mail.id, + 'mail_message_id': self.test_message.id, + 'notification_type': 'email', + 'res_partner_id': partners_falsy[0].id, + }) + + # missing to / invalid to + for email_to in self.emails_falsy + self.emails_invalid: + self._reset_data() + notification2.write({'failure_type': False, 'notification_status': 'ready'}) + mail.write({'email_to': email_to}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertFalse(mail.failure_reason, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertEqual(mail.state, 'sent', 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertFalse(notification.failure_type) + self.assertEqual(notification.notification_status, 'sent') + self.assertEqual(notification2.failure_type, 'mail_email_invalid') + self.assertEqual(notification2.notification_status, 'exception') + + # buggy to (ascii) + for email_to in self.emails_invalid_ascii: + self._reset_data() + notification2.write({'failure_type': False, 'notification_status': 'ready'}) + mail.write({'email_to': email_to}) + with self.mock_mail_gateway(): + mail.send(raise_exception=False) + self.assertIn('Codepoint U+00A2 at position', mail.failure_reason) + self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'unknown') + self.assertEqual(notification.notification_status, 'exception') + self.assertEqual(notification2.failure_type, 'unknown') + self.assertEqual(notification2.notification_status, 'exception') + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_mail_send_exceptions_raise_management(self): + """ Test various use case with exceptions and errors and see how they are + managed and stored at mail and notification level. """ + mail, notification = self.test_mail, self.test_notification + mail.write({'email_from': 'test.user@test.example.com', 'email_to': 'test@example.com'}) + + # SMTP connecting issues + with self.mock_mail_gateway(): + _connect_current = self.connect_mocked.side_effect + + # classic errors that may be raised during sending, just to test their current support + for error, msg in [ + (smtplib.SMTPServerDisconnected('SMTPServerDisconnected'), 'SMTPServerDisconnected'), + (smtplib.SMTPResponseException('code', 'SMTPResponseException'), 'code\nSMTPResponseException'), + (smtplib.SMTPNotSupportedError('SMTPNotSupportedError'), 'SMTPNotSupportedError'), + (smtplib.SMTPException('SMTPException'), 'SMTPException'), + (SSLError('SSLError'), 'SSLError'), + (gaierror('gaierror'), 'gaierror'), + (timeout('timeout'), 'timeout')]: + + def _connect(*args, **kwargs): + raise error + self.connect_mocked.side_effect = _connect + + mail.send(raise_exception=False) + self.assertFalse(mail.failure_type) + self.assertEqual(mail.failure_reason, msg) + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'mail_smtp') + self.assertEqual(notification.notification_status, 'exception') + self._reset_data() + + self.connect_mocked.side_effect = _connect_current + + # SMTP sending issues + with self.mock_mail_gateway(): + _send_current = self.send_email_mocked.side_effect + self._reset_data() + mail.write({'email_to': 'test@example.com'}) + + # should always raise for those errors, even with raise_exception=False + for error, error_class in [ + (smtplib.SMTPServerDisconnected("Some exception"), smtplib.SMTPServerDisconnected), + (MemoryError("Some exception"), MemoryError)]: + def _send_email(*args, **kwargs): + raise error + self.send_email_mocked.side_effect = _send_email + + with self.assertRaises(error_class): + mail.send(raise_exception=False) + self.assertFalse(mail.failure_reason, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') + self.assertFalse(mail.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') + self.assertEqual(mail.state, 'outgoing', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') + self.assertFalse(notification.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') + self.assertEqual(notification.notification_status, 'ready', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback') + + # MailDeliveryException: should be catched; other issues are sub-catched under + # a MailDeliveryException and are catched + for error, msg in [ + (MailDeliveryException("Some exception"), 'Some exception'), + (ValueError("Unexpected issue"), 'Unexpected issue')]: + def _send_email(*args, **kwargs): + raise error + self.send_email_mocked.side_effect = _send_email + + self._reset_data() + mail.send(raise_exception=False) + self.assertEqual(mail.failure_reason, msg) + self.assertFalse(mail.failure_type, 'Mail: unlogged failure type to fix') + self.assertEqual(mail.state, 'exception') + self.assertEqual(notification.failure_type, 'unknown', 'Mail: generic failure type') + self.assertEqual(notification.notification_status, 'exception') + + self.send_email_mocked.side_effect = _send_current + @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_mail_send_server(self): """Test that the mails are send in batch. From bf508a808e766fe19b70278b3edd3b0ac3023328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 8 Dec 2021 14:28:19 +0000 Subject: [PATCH 5/9] [IMP] test_mail: add multicompany test models PURPOSE Purpose of this commit is to add a company field on test mail models used to replicate ticket / project behavior (``mail.test.ticket`` and ``mail.test. container``). SPECIFICATIONS To avoid messing with existing tests and ease comparison through all Odoo versions (notably performance) this is done by adding new models inheriting from current mail models. A company_id field is added as well as MC rules. Those models will be used to add tests for mail thread behavior in a multi company environment (aliases management, performance, emails and notification layouts, ...). In this commit we also update ACLs for ticket-like models to add rules for portal users, based on followers. This mimics classic rules from Odoo used notably in a light project-like environment. This allows to test a bit more in details recipients, access tokens, links in emails, ... Task-2673913 (TestMail: Cleanup and improve test coverage) Part-of: odoo/odoo#86393 --- addons/test_mail/models/test_mail_models.py | 24 ++++++++ addons/test_mail/security/ir.model.access.csv | 8 ++- .../test_mail/security/test_mail_security.xml | 56 +++++++++++++++++-- addons/test_mail/tests/test_mail_composer.py | 3 + 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py index f04c3876cab..16e7c0afd09 100644 --- a/addons/test_mail/models/test_mail_models.py +++ b/addons/test_mail/models/test_mail_models.py @@ -113,6 +113,18 @@ class MailTestTicket(models.Model): return super(MailTestTicket, self)._track_subtype(init_values) +class MailTestTicketMC(models.Model): + """ Just mail.test.ticket, but multi company. Kept as different model to + avoid messing with existing tests, notably performance, and ease backward + comparison. """ + _description = 'Ticket-like model' + _name = 'mail.test.ticket.mc' + _inherit = ['mail.test.ticket'] + + company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company) + container_id = fields.Many2one('mail.test.container.mc', tracking=True) + + class MailTestContainer(models.Model): """ This model can be used in tests when container records like projects or teams are required. """ @@ -146,6 +158,18 @@ class MailTestContainer(models.Model): values['alias_parent_thread_id'] = self.id return values +class MailTestContainerMC(models.Model): + """ Just mail.test.container, but multi company. Kept as different model to + avoid messing with existing tests, notably performance, and ease backward + comparison. """ + _description = 'Project-like model with alias (MC)' + _name = 'mail.test.container.mc' + _mail_post_access = 'read' + _inherit = ['mail.test.container'] + + company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company) + + class MailTestComposerMixin(models.Model): """ A simple invite-like wizard using the composer mixin, rendering on itself. """ diff --git a/addons/test_mail/security/ir.model.access.csv b/addons/test_mail/security/ir.model.access.csv index e283c89b36f..c212f163d61 100644 --- a/addons/test_mail/security/ir.model.access.csv +++ b/addons/test_mail/security/ir.model.access.csv @@ -11,12 +11,16 @@ access_mail_test_activity_portal,mail.test.activity.portal,model_mail_test_activ access_mail_test_activity_user,mail.test.activity.user,model_mail_test_activity,base.group_user,1,1,1,1 access_mail_test_field_type_portal,mail.test.field.type.portal,model_mail_test_field_type,base.group_portal,0,0,0,0 access_mail_test_field_type_user,mail.test.field.type.user,model_mail_test_field_type,base.group_user,1,1,1,1 -access_mail_test_ticket_portal,mail.test.ticket.portal,model_mail_test_ticket,base.group_portal,0,0,0,0 +access_mail_test_ticket_portal,mail.test.ticket.portal,model_mail_test_ticket,base.group_portal,1,0,0,0 access_mail_test_ticket_user,mail.test.ticket.user,model_mail_test_ticket,base.group_user,1,1,1,1 +access_mail_test_ticket_mc_portal,mail.test.ticket.mc.portal,model_mail_test_ticket_mc,base.group_portal,1,0,0,0 +access_mail_test_ticket_mc_user,mail.test.ticket.mc.user,model_mail_test_ticket_mc,base.group_user,1,1,1,1 access_mail_test_composer_mixin_all,mail.test.composer.mixin.all,model_mail_test_composer_mixin,,0,0,0,0 access_mail_test_composer_mixin_user,mail.test.composer.mixin.user,model_mail_test_composer_mixin,base.group_user,1,1,1,1 -access_mail_test_container_portal,mail.test.container_portal,model_mail_test_container,base.group_portal,1,1,0,0 +access_mail_test_container_portal,mail.test.container_portal,model_mail_test_container,base.group_portal,1,0,0,0 access_mail_test_container_user,mail.test.container.user,model_mail_test_container,base.group_user,1,1,1,1 +access_mail_test_container_mc_portal,mail.test.container.mc.portal,model_mail_test_container_mc,base.group_portal,1,0,0,0 +access_mail_test_container_mc_user,mail.test.container.mc.user,model_mail_test_container_mc,base.group_user,1,1,1,1 access_mail_test_cc_portal,mail.test.cc.portal,model_mail_test_cc,base.group_portal,1,0,0,0 access_mail_test_cc_user,mail.test.cc.user,model_mail_test_cc,base.group_user,1,1,1,1 access_mail_test_lang_portal,mail.test.lang.portal,model_mail_test_lang,base.group_portal,1,0,0,0 diff --git a/addons/test_mail/security/test_mail_security.xml b/addons/test_mail/security/test_mail_security.xml index 296cbdc02da..5873a5d09ae 100644 --- a/addons/test_mail/security/test_mail_security.xml +++ b/addons/test_mail/security/test_mail_security.xml @@ -1,11 +1,55 @@ - - Mail Test Multi Company - - - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] - + + Mail Test Multi Company + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + + Portal Mail Test Ticket + + [('message_partner_ids', 'in', [user.partner_id.id])] + + + + + + Mail Test Ticket Multi Company + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + Portal Mail Test Ticket Multi Company + + [('message_partner_ids', 'in', [user.partner_id.id])] + + + + + + Portal Mail Test Container + + [('message_partner_ids', 'in', [user.partner_id.id])] + + + + + + Mail Test Container Multi Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + + Portal Mail Test Container Multi Company + + [('message_partner_ids', 'in', [user.partner_id.id])] + + diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 57f4adfbadd..8037497ca0d 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -438,7 +438,10 @@ class TestComposerInternals(TestMailComposer): def test_mail_composer_rights_portal(self): portal_user = self._create_portal_user() + # give read access to the record to portal (for check access rule) + self.test_record.message_subscribe(partner_ids=portal_user.partner_id.ids) + # patch check access rights for write access, required to post a message by default 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) From 94ab6963cf00bbd788bc11d261deae1d9fb96791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 2 Nov 2021 11:22:51 +0000 Subject: [PATCH 6/9] [IMP] test_mail: add and improve tests for composer PURPOSE Cleanup and improve composer-related tests. As composer will soon be improved better ensure we don't break flows. SPECIFCIATIONS Cleanup existing tests: rename, make it use tools, reoder them. Add some tests about composer in order to better spot changes and differences as well as to help debugging mail composer code update. Notably add tests with Form tool, add tests for reply-to computation, auto-delete, mail server. Performance tests about composer are added, notably using the Form tool. This helps determining differences between creating / manipulating a composer in code and in UI. Indeed notably onchange and some side effects may lead to different query counters, which is the case as highlighted here. Tests involving attachments are also added to see their impact on posting process. Task-2673913 (TestMail: Cleanup and improve test coverage) Part-of: odoo/odoo#86393 --- addons/mail/tests/test_mail_composer.py | 16 +- addons/mail/wizard/mail_compose_message.py | 2 +- addons/test_mail/tests/test_mail_composer.py | 459 ++++++++++++++----- addons/test_mail/tests/test_performance.py | 255 +++++++++-- 4 files changed, 587 insertions(+), 145 deletions(-) diff --git a/addons/mail/tests/test_mail_composer.py b/addons/mail/tests/test_mail_composer.py index 11bde11084c..9b573efb5be 100644 --- a/addons/mail/tests/test_mail_composer.py +++ b/addons/mail/tests/test_mail_composer.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests import users from odoo.addons.mail.tests.common import MailCommon +from odoo.tests import tagged, users +@tagged('mail_composer') class TestMailComposer(MailCommon): + @classmethod def setUpClass(cls): super(TestMailComposer, cls).setUpClass() @@ -47,9 +49,11 @@ class TestMailComposer(MailCommon): values = mail_compose_message.get_mail_values(self.partner_employee.ids) - self.assertIn(self.body_html, + self.assertIn( + self.body_html, values[self.partner_employee.id]['body_html'], - 'We must preserve (mso) comments in email html') + 'We must preserve (mso) comments in email html' + ) @users('employee') def test_mail_mass_mode_compose_with_mso(self): @@ -69,6 +73,8 @@ class TestMailComposer(MailCommon): values = composer.get_mail_values(self.partner_employee.ids) - self.assertIn(self.body_html, + self.assertIn( + self.body_html, values[self.partner_employee.id]['body_html'], - 'We must preserve (mso) comments in email html') + 'We must preserve (mso) comments in email html' + ) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index ce079a23cae..3fca30f5caf 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -545,7 +545,7 @@ class MailComposer(models.TransientModel): elif template_id: values = self.generate_email_for_composer( template_id, [res_id], - ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id'] + ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id'] )[res_id] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 8037497ca0d..c9ad9faf0e4 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -11,6 +11,7 @@ from odoo.tests import tagged from odoo.tests.common import users, Form from odoo.tools import mute_logger + @tagged('mail_composer') class TestMailComposer(TestMailCommon, TestRecipients): """ Test Composer internals """ @@ -46,7 +47,10 @@ class TestMailComposer(TestMailCommon, TestRecipients): 'customer_id': cls.partner_1.id, 'user_id': cls.user_employee_2.id, }) - cls.test_records, cls.test_partners = cls._create_records_for_batch('mail.test.ticket', 2) + cls.test_records, cls.test_partners = cls._create_records_for_batch( + 'mail.test.ticket', 2, + additional_values={'user_id': cls.user_employee_2.id} + ) cls.test_report = cls.env['ir.actions.report'].create({ 'name': 'Test Report on mail test ticket', @@ -58,24 +62,17 @@ class TestMailComposer(TestMailCommon, TestRecipients): cls.test_from = '"John Doe" ' - cls.mail_server = cls.env['ir.mail_server'].create({ - 'name': 'Dummy Test Server', - 'smtp_host': 'smtp.pizza.moc', - 'smtp_port': 17, - 'smtp_encryption': 'ssl', - 'sequence': 666, - }) - cls.template = cls.env['mail.template'].create({ + 'auto_delete': True, 'name': 'TestTemplate', 'subject': 'TemplateSubject {{ object.name }}', 'body_html': '

TemplateBody

', 'partner_to': '{{ object.customer_id.id if object.customer_id else "" }}', 'email_to': '{{ (object.email_from if not object.customer_id else "") }}', 'email_from': '{{ (object.user_id.email_formatted or user.email_formatted) }}', + 'mail_server_id': cls.mail_server_domain.id, 'model_id': cls.env['ir.model']._get('mail.test.ticket').id, - 'mail_server_id': cls.mail_server.id, - 'auto_delete': True, + 'reply_to': '{{ ctx.get("custom_reply_to") or "info@test.example.com" }}', }) def _get_web_context(self, records, add_web=True, **values): @@ -107,15 +104,28 @@ class TestComposerForm(TestMailComposer): @users('employee') def test_mail_composer_comment(self): - composer_form = Form(self.env['mail.compose.message'].with_context(self._get_web_context(self.test_record, add_web=True))) + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=True) + )) + # auto_delete not displayed currently in view + # self.assertFalse(composer_form.auto_delete) + # self.assertFalse(composer_form.auto_delete_message) + # author_id not displayed currently in view + # self.assertEqual(composer_form.author_id, self.env.user.partner_id) + self.assertFalse(composer_form.body) + self.assertEqual(composer_form.composition_mode, 'comment') + self.assertEqual(composer_form.email_from, self.env.user.email_formatted) + self.assertFalse(composer_form.mail_server_id) + self.assertEqual(composer_form.model, self.test_record._name) + self.assertFalse(composer_form.partner_ids) + # record name not displayed currently in view + # self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name') + self.assertFalse(composer_form.reply_to) + self.assertFalse(composer_form.reply_to_force_new) + self.assertEqual(composer_form.res_id, self.test_record.id) self.assertEqual( composer_form.subject, 'Re: %s' % self.test_record.name, 'MailComposer: comment mode should have default subject Re: record_name') - # record name not displayed currently in view - # self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name') - self.assertFalse(composer_form.reply_to_force_new) - self.assertEqual(composer_form.composition_mode, 'comment') - self.assertEqual(composer_form.model, self.test_record._name) @users('employee') def test_mail_composer_comment_attachments(self): @@ -155,7 +165,7 @@ class TestComposerForm(TestMailComposer): 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']) + self.assertFalse(tpl_attachments) # change back to template 1 composer_form.template_id = template_1 @@ -169,29 +179,77 @@ class TestComposerForm(TestMailComposer): composer_form.template_id = self.env['mail.template'] self.assertEqual(len(composer_form.attachment_ids), 0) + @users('employee') + def test_mail_composer_comment_wtpl(self): + 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) + )) + # auto_delete not displayed currently in view + # self.assertTrue(composer_form.auto_delete) + # self.assertFalse(composer_form.auto_delete_message) + # author_id not displayed currently in view + # self.assertEqual(composer_form.author_id, self.env.user.partner_id) + self.assertEqual(composer_form.body, '

TemplateBody %s

' % self.test_record.name) + self.assertEqual(composer_form.composition_mode, 'comment') + self.assertEqual(composer_form.email_from, self.user_employee_2.email_formatted) + self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) + self.assertEqual(composer_form.model, self.test_record._name) + self.assertEqual(composer_form.partner_ids[:], self.partner_1) + # record name not displayed currently in view + # self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name') + self.assertEqual(composer_form.reply_to, 'info@test.example.com') + self.assertFalse(composer_form.reply_to_force_new) + self.assertEqual(composer_form.res_id, self.test_record.id) + self.assertEqual(composer_form.subject, 'TemplateSubject %s' % self.test_record.name) + @users('employee') def test_mail_composer_mass(self): - composer_form = Form(self.env['mail.compose.message'].with_context(self._get_web_context(self.test_records, add_web=True))) - self.assertFalse(composer_form.subject, 'MailComposer: mass mode should have void default subject if no template') + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True) + )) + # auto_delete not displayed currently in view + # self.assertFalse(composer_form.auto_delete) + # self.assertFalse(composer_form.auto_delete_message) + # author_id not displayed currently in view + # self.assertEqual(composer_form.author_id, self.env.user.partner_id) + self.assertFalse(composer_form.body) + self.assertEqual(composer_form.composition_mode, 'mass_mail') + self.assertEqual(composer_form.email_from, self.env.user.email_formatted) + self.assertFalse(composer_form.mail_server_id) + self.assertEqual(composer_form.model, self.test_records._name) # record name not displayed currently in view # self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name') + self.assertFalse(composer_form.reply_to) self.assertFalse(composer_form.reply_to_force_new) - self.assertEqual(composer_form.composition_mode, 'mass_mail') - self.assertEqual(composer_form.model, self.test_records._name) + self.assertEqual(composer_form.res_id, self.test_records[0].id, + 'MailComposer: even in mass mode web active_id presence may add a res_id') + self.assertFalse(composer_form.subject, 'MailComposer: mass mode should have void default subject if no template') @users('employee') def test_mail_composer_mass_wtpl(self): - ctx = self._get_web_context(self.test_records, add_web=True, default_template_id=self.template.id) - composer_form = Form(self.env['mail.compose.message'].with_context(ctx)) - self.assertEqual(composer_form.subject, self.template.subject, - 'MailComposer: mass mode should have template raw subject if template') + 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) + )) + # auto_delete not displayed currently in view + # self.assertTrue(composer_form.auto_delete) + # self.assertFalse(composer_form.auto_delete_message) + # author_id not displayed currently in view + # self.assertEqual(composer_form.author_id, self.env.user.partner_id) self.assertEqual(composer_form.body, self.template.body_html, 'MailComposer: mass mode should have template raw body if template') + self.assertEqual(composer_form.composition_mode, 'mass_mail') + self.assertEqual(composer_form.email_from, self.template.email_from, + 'MailComposer: mass mode should have template raw email_from if template') + self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) + self.assertEqual(composer_form.model, self.test_records._name) # record name not displayed currently in view # self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name') + self.assertEqual(composer_form.reply_to, self.template.reply_to) self.assertFalse(composer_form.reply_to_force_new) - self.assertEqual(composer_form.composition_mode, 'mass_mail') - self.assertEqual(composer_form.model, self.test_records._name) + self.assertEqual(composer_form.res_id, self.test_records[0].id, + 'MailComposer: even in mass mode web active_id presence may add a res_id') + self.assertEqual(composer_form.subject, self.template.subject, + 'MailComposer: mass mode should have template raw subject if template') @tagged('mail_composer') @@ -293,6 +351,10 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.subject, 'My amazing subject') self.assertEqual(composer.body, '

Test Body

') self.assertEqual(composer.mail_server_id.id, False) + if composition_mode == 'comment': + self.assertEqual(composer.record_name, self.test_record.name) + else: + self.assertFalse(composer.record_name) # changing template should update its content composer.write({'template_id': self.template.id}) @@ -304,10 +366,12 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.subject, 'TemplateSubject %s' % self.test_record.name) self.assertEqual(composer.body, '

TemplateBody %s

' % self.test_record.name) self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + self.assertEqual(composer.record_name, self.test_record.name) else: self.assertEqual(composer.subject, self.template.subject) self.assertEqual(composer.body, self.template.body_html) self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + self.assertFalse(composer.record_name) # manual values is kept over template composer.write({'subject': 'Back to my amazing subject'}) @@ -321,17 +385,19 @@ class TestComposerInternals(TestMailComposer): # values are reset if composition_mode == 'comment': self.assertEqual(composer.subject, 'Re: %s' % self.test_record.name) - self.assertEqual(composer.body, '') + self.assertFalse(composer.body) # TDE FIXME: server id is kept, not sure why # self.assertFalse(composer.mail_server_id.id) self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + self.assertEqual(composer.record_name, self.test_record.name) else: # values are reset TDE FIXME: strange for subject self.assertEqual(composer.subject, 'Back to my amazing subject') - self.assertEqual(composer.body, '') + self.assertFalse(composer.body) # TDE FIXME: server id is kept, not sure why # self.assertFalse(composer.mail_server_id.id) self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + self.assertFalse(composer.record_name) # 2. check with default ctx['default_template_id'] = self.template.id @@ -346,10 +412,12 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.subject, 'TemplateSubject %s' % self.test_record.name) self.assertEqual(composer.body, '

TemplateBody %s

' % self.test_record.name) self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + self.assertEqual(composer.record_name, self.test_record.name) else: self.assertEqual(composer.subject, self.template.subject) self.assertEqual(composer.body, self.template.body_html) self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + self.assertFalse(composer.record_name) # 3. check at create ctx.pop('default_template_id') @@ -364,10 +432,12 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.subject, 'TemplateSubject %s' % self.test_record.name) self.assertEqual(composer.body, '

TemplateBody %s

' % self.test_record.name) self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + self.assertEqual(composer.record_name, self.test_record.name) else: self.assertEqual(composer.subject, self.template.subject) self.assertEqual(composer.body, self.template.body_html) self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + self.assertFalse(composer.record_name) # 4. template + user input ctx['default_template_id'] = self.template.id @@ -375,15 +445,155 @@ class TestComposerInternals(TestMailComposer): 'subject': 'My amazing subject', 'body': '

Test Body

', 'mail_server_id': False, + 'record_name': 'CustomName', }) # creation values are taken self.assertEqual(composer.subject, 'My amazing subject') self.assertEqual(composer.body, '

Test Body

') self.assertEqual(composer.mail_server_id.id, False) + self.assertEqual(composer.record_name, 'CustomName') @users('employee') - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.models.unlink') + def test_mail_composer_recipients(self): + """ Test content management (partner_ids, reply_to) in both comment and + mass mailing mode. Template update is also tested. Add some tests for + partner creation based on unknown emails as this is part of the process. """ + base_recipients = self.partner_1 + self.partner_2 + self.template.write({ + 'email_cc': 'test.cc.1@test.example.com, test.cc.2@test.example.com' + }) + + for composition_mode in ['comment', 'mass_mail']: + self.assertFalse( + self.env['res.partner'].search([ + ('email_normalized', 'in', ['test.cc.1@test.example.com', + 'test.cc.2@test.example.com']) + ]) + ) + if composition_mode == 'comment': + ctx = self._get_web_context(self.test_record, add_web=False) + else: + ctx = self._get_web_context(self.test_records, add_web=False) + + # 1. check without template + template update + composer = self.env['mail.compose.message'].with_context(ctx).create({ + 'body': '

Test Body

', + 'partner_ids': base_recipients.ids, + 'reply_to': False, + 'subject': 'My amazing subject', + }) + + # creation values are taken + self.assertEqual(composer.partner_ids, base_recipients) + self.assertFalse(composer.reply_to) + self.assertFalse(composer.reply_to_force_new) + self.assertEqual(composer.reply_to_mode, 'update') + + # changing template should update its content + composer.write({'template_id': self.template.id}) + # currently onchange necessary + composer._onchange_template_id_wrapper() + new_partners = self.env['res.partner'].search( + [('email_normalized', 'in', ['test.cc.1@test.example.com', + 'test.cc.2@test.example.com']) + ] + ) + + # values come from template + if composition_mode == 'comment': + self.assertEqual(len(new_partners), 2) + self.assertEqual(composer.partner_ids, self.partner_1 + new_partners, 'Template took customer_id as set on record') + self.assertEqual(composer.reply_to, 'info@test.example.com', 'Template was rendered') + self.assertFalse(composer.reply_to_force_new) # should not change in comment mode + else: + self.assertEqual(len(new_partners), 0) + self.assertEqual(composer.partner_ids, base_recipients, 'Mass mode: kept original values') + self.assertEqual(composer.reply_to, self.template.reply_to, 'Mass mode: raw template value') + self.assertFalse(composer.reply_to_force_new) # should probably become True, not supported currently + + # manual values is kept over template + composer.write({'partner_ids': [(5, 0), (4, self.partner_admin.id)]}) + self.assertEqual(composer.partner_ids, self.partner_admin) + + # reset template should reset values + composer.write({'template_id': False}) + # currently onchange necessary + composer._onchange_template_id_wrapper() + + # values are kepts, not sure why + if composition_mode == 'comment': + self.assertEqual(composer.partner_ids, self.partner_admin, 'Values are kept, not sure why') + self.assertEqual(composer.reply_to, 'info@test.example.com', 'Values are kept') + self.assertFalse(composer.reply_to_force_new) + else: + self.assertEqual(composer.partner_ids, self.partner_admin, 'Mass mode: kept current value') + self.assertEqual(composer.reply_to, '{{ ctx.get("custom_reply_to") or "info@test.example.com" }}', 'Mass mode: kept current value') + self.assertFalse(composer.reply_to_force_new) + + # 2. check with default + ctx['default_template_id'] = self.template.id + composer = self.env['mail.compose.message'].with_context(ctx).create({ + 'template_id': self.template.id, + }) + # currently onchange necessary + composer._onchange_template_id_wrapper() + + # values come from template + if composition_mode == 'comment': + self.assertEqual(composer.partner_ids, self.partner_1 + new_partners) + else: + self.assertFalse(composer.partner_ids) + if composition_mode == 'comment': + self.assertEqual(composer.reply_to, "info@test.example.com") + else: + self.assertEqual(composer.reply_to, self.template.reply_to) + self.assertFalse(composer.reply_to_force_new) # note: this should be updated with reply-to + self.assertEqual(composer.reply_to_mode, 'update') # note: this should be updated with reply-to + + # 3. check at create + ctx.pop('default_template_id') + composer = self.env['mail.compose.message'].with_context(ctx).create({ + 'template_id': self.template.id, + }) + # currently onchange necessary + composer._onchange_template_id_wrapper() + + # values come from template + if composition_mode == 'comment': + self.assertEqual(composer.partner_ids, self.partner_1 + new_partners) + else: + self.assertFalse(composer.partner_ids) + if composition_mode == 'comment': + self.assertEqual(composer.reply_to, "info@test.example.com") + else: + self.assertEqual(composer.reply_to, self.template.reply_to) + self.assertFalse(composer.reply_to_force_new) + self.assertEqual(composer.reply_to_mode, 'update') + + # 4. template + user input + ctx['default_template_id'] = self.template.id + composer = self.env['mail.compose.message'].with_context(ctx).create({ + 'body': '

Test Body

', + 'partner_ids': base_recipients.ids, + 'subject': 'My amazing subject', + 'reply_to': False, + }) + + # creation values are taken + self.assertEqual(composer.partner_ids, base_recipients) + self.assertFalse(composer.reply_to) + self.assertFalse(composer.reply_to_force_new) + self.assertEqual(composer.reply_to_mode, 'update') + + self.env['res.partner'].search([ + ('email_normalized', 'in', ['test.cc.1@test.example.com', + 'test.cc.2@test.example.com']) + ]).unlink() + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') def test_mail_composer_parent(self): """ Test specific management in comment mode when having parent_id set: record_name, subject, parent's partners. """ @@ -396,11 +606,13 @@ class TestComposerInternals(TestMailComposer): }) # 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) + self.assertEqual(composer.record_name, self.test_record.name) + self.assertEqual(composer.subject, 'Re: %s' % self.test_record.name) @users('user_rendering_restricted') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') def test_mail_composer_rights_attachments(self): """ Ensure a user without write access to a template can send an email""" template_1 = self.template.copy({ @@ -436,6 +648,7 @@ class TestComposerInternals(TestMailComposer): sorted(self.test_record.message_ids[0].attachment_ids.mapped('name')), sorted(template_1_attachment_name)) + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') def test_mail_composer_rights_portal(self): portal_user = self._create_portal_user() # give read access to the record to portal (for check access rule) @@ -489,7 +702,7 @@ class TestComposerResultsComment(TestMailComposer): notification and emails generated during this process. """ @users('employee') - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') def test_mail_composer_notifications_delete(self): """ Notifications are correctly deleted once sent """ composer = self.env['mail.compose.message'].with_context( @@ -506,9 +719,29 @@ class TestComposerResultsComment(TestMailComposer): self.assertEqual(message.notified_partner_ids, self.partner_employee_2 + self.partner_1 + self.partner_2) # global outgoing - self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail (1 for users, 1 for customers)') self.assertEqual(len(self._mails), 3, 'Should have sent an email each recipient') - self.assertEqual(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records') + self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail (1 for users, 1 for customers)') + self.assertFalse(self._new_mails.exists(), 'Should have deleted mail.mail records') + + # Check ``auto_delete`` field usage (note: currently not correctly managed) + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record), + ).create({ + 'auto_delete': False, + 'body': '

Test Body

', + 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] + }) + with self.mock_mail_gateway(mail_unlink_sent=True): + composer._action_send_mail() + + # notifications + message = self.test_record.message_ids[0] + self.assertEqual(message.notified_partner_ids, self.partner_employee_2 + self.partner_1 + self.partner_2) + + # global outgoing + self.assertEqual(len(self._mails), 3, 'Should have sent an email each recipient') + self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail (1 for users, 1 for customers)') + self.assertEqual(len(self._new_mails.exists()), 0, 'To fix: does not respect auto_delete') # ensure ``mail_auto_delete`` context key allow to override this behavior composer = self.env['mail.compose.message'].with_context( @@ -526,12 +759,12 @@ class TestComposerResultsComment(TestMailComposer): self.assertEqual(message.notified_partner_ids, self.partner_employee_2 + self.partner_1 + self.partner_2) # global outgoing - self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail (1 for users, 1 for customers)') self.assertEqual(len(self._mails), 3, 'Should have sent an email each recipient') + self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail (1 for users, 1 for customers)') self.assertEqual(len(self._new_mails.exists()), 2, 'Should not have deleted mail.mail records') @users('employee') - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') 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( @@ -543,8 +776,8 @@ class TestComposerResultsComment(TestMailComposer): composer._action_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.body, '

Test Body

') 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) @@ -609,6 +842,7 @@ class TestComposerResultsComment(TestMailComposer): email_values={ 'body_content': 'TemplateBody %s' % self.test_record.name, 'email_from': self.test_record.user_id.email_formatted, # set by template + 'mail_server_id': self.mail_server_domain, 'subject': 'TemplateSubject %s' % self.test_record.name, 'attachments_info': [ {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, @@ -624,6 +858,7 @@ class TestComposerResultsComment(TestMailComposer): email_values={ 'body_content': 'TemplateBody %s' % self.test_record.name, 'email_from': self.test_record.user_id.email_formatted, # set by template + 'mail_server_id': self.mail_server_domain, 'subject': 'TemplateSubject %s' % self.test_record.name, 'attachments_info': [ {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, @@ -658,10 +893,73 @@ class TestComposerResultsMass(TestMailComposer): 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], }) + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_mail_composer_delete(self): + """ Check mail / msg delete support """ + # ensure initial data + self.assertTrue(self.template.auto_delete) + self.assertEqual(self.test_records.user_id, self.user_employee_2) + self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2) + + 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), self.mock_mail_app(): + composer._action_send_mail() + + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertFalse(self._new_mails.exists(), 'Should have deleted mail.mail records') + self.assertEqual(len(self._new_msgs), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(self._new_msgs.exists(), self._new_msgs, 'Should not have deleted mail.message records') + + # force composer auto_delete field + 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() + composer.auto_delete = False + with self.mock_mail_gateway(mail_unlink_sent=True), self.mock_mail_app(): + composer._action_send_mail() + + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + # self.assertEqual(self._new_mails.exists(), self._new_mails, 'Should not have deleted mail.mail records') + self.assertFalse(self._new_mails.exists(), 'Template is forced over composer value, which is not correct') + self.assertEqual(len(self._new_msgs), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(self._new_msgs.exists(), self._new_msgs, 'Should not have deleted mail.message records') + + # check composer auto_delete_message + 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() + composer.auto_delete_message = True + with self.mock_mail_gateway(mail_unlink_sent=True), self.mock_mail_app(): + composer._action_send_mail() + + # global outgoing + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertFalse(self._new_mails.exists(), 'Should have deleted mail.mail records') + self.assertEqual(len(self._new_msgs), 2, 'Should have created 1 mail.mail per record') + self.assertFalse(self._new_msgs.exists(), 'Should have deleted mail.message records') + @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 + + # ensure initial data + self.assertEqual(self.test_records.user_id, self.user_employee_2) + self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2) + 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) @@ -681,15 +979,18 @@ class TestComposerResultsMass(TestMailComposer): # template is sent directly using customer field, meaning we have recipients self.assertMailMail(record.customer_id, 'sent', mail_message=message, - author=self.partner_employee) + author=self.partner_employee, + email_values={ + 'email_from': self.partner_employee_2.email_formatted, + }) # 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']) + self.assertFalse(message.subtype_id) + self.assertFalse(message.partner_ids) @users('employee') @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') @@ -714,8 +1015,8 @@ class TestComposerResultsMass(TestMailComposer): 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']) + self.assertEqual(self.test_records.user_id, self.user_employee_2) + self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2) # launch composer in mass mode composer_form = Form(self.env['mail.compose.message'].with_context( @@ -754,72 +1055,17 @@ class TestComposerResultsMass(TestMailComposer): author=self.partner_employee, email_values={ 'body_content': 'TemplateBody %s' % record.name, + 'email_from': self.partner_employee_2.email_formatted, + 'mail_server_id': self.mail_server_global.id, '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') - # ] + 'attachments_info': [ + {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, + {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, + {'name': 'TestReport for %s.html' % record.name, 'type': '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._action_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._action_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): @@ -843,7 +1089,8 @@ class TestComposerResultsMass(TestMailComposer): for record in self.test_records: # template is sent directly using customer field - self.assertSentEmail(self.partner_employee, record.customer_id) + self.assertSentEmail(self.partner_employee_2.email_formatted, record.customer_id, + author_id=self.partner_employee) # 2: active_domain not taken into account if use_active_domain is False composer_form = Form(self.env['mail.compose.message'].with_context( diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index d02a2ef0006..43721eef50e 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -5,7 +5,7 @@ import base64 from odoo.addons.base.tests.common import TransactionCaseWithUserDemo from odoo.addons.mail.tests.common import MailCommon -from odoo.tests.common import users, warmup +from odoo.tests.common import users, warmup, Form from odoo.tests import tagged from odoo.tools import mute_logger, formataddr @@ -27,6 +27,10 @@ class BaseMailPerformance(MailCommon, TransactionCaseWithUserDemo): 'mobile': '0456123456', }) + cls.test_attachments_vals = cls._generate_attachments_data( + cls, 3, res_model='mail.compose.message', res_id=0 + ) + def setUp(self): super(BaseMailPerformance, self).setUp() # patch registry to simulate a ready environment @@ -206,6 +210,10 @@ class TestMailAPIPerformance(BaseMailPerformance): 'email_from': '{{ object.user_id.email_formatted }}', 'partner_to': '{{ object.customer_id.id }}', 'email_to': '{{ ("%s Customer <%s>" % (object.name, object.email_from)) }}', + 'attachment_ids': [ + (0, 0, dict(attachment, res_model='mail.template')) + for attachment in self.test_attachments_vals + ], }) @users('__system__', 'employee') @@ -231,9 +239,9 @@ class TestMailAPIPerformance(BaseMailPerformance): 'res_id': record.id, 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, }) - #read activity_type to normalize cache between enterprise and community - #voip module read activity_type during create leading to one less query in enterprise on action_feedback - category = activity.activity_type_id.category + # read activity_type to normalize cache between enterprise and community + # voip module read activity_type during create leading to one less query in enterprise on action_feedback + _category = activity.activity_type_id.category with self.assertQueryCount(__system__=17, employee=19): activity.action_feedback(feedback='Zizisse Done !') @@ -246,9 +254,9 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(__system__=8, employee=8): activity = record.action_start('Test Start') - #read activity_type to normalize cache between enterprise and community - #voip module read activity_type during create leading to one less query in enterprise on action_close - category = activity.activity_type_id.category + # read activity_type to normalize cache between enterprise and community + # voip module read activity_type during create leading to one less query in enterprise on action_close + _category = activity.activity_type_id.category record.write({'name': 'Dupe write'}) @@ -259,7 +267,38 @@ class TestMailAPIPerformance(BaseMailPerformance): @users('__system__', 'employee') @warmup - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @mute_logger('odoo.models.unlink') + def test_adv_activity_mixin_w_attachments(self): + record = self.env['mail.test.activity'].create({'name': 'Test'}) + + attachments = self.env['ir.attachment'].create([ + dict(values, + res_model='mail.activity', + res_id=0) + for values in self.test_attachments_vals + ]) + + with self.assertQueryCount(__system__=8, employee=8): + activity = record.action_start('Test Start') + #read activity_type to normalize cache between enterprise and community + #voip module read activity_type during create leading to one less query in enterprise on action_close + _category = activity.activity_type_id.category + + record.write({'name': 'Dupe write'}) + + with self.assertQueryCount(__system__=20, employee=24): + record.action_close('Dupe feedback', attachment_ids=attachments.ids) + + # notifications + message = record.message_ids[0] + self.assertEqual( + set(message.attachment_ids.mapped('name')), + set(['AttFileName_00.txt', 'AttFileName_01.txt', 'AttFileName_02.txt']) + ) + + @users('__system__', 'employee') + @warmup + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') def test_mail_composer(self): self._create_test_records() test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) @@ -279,7 +318,59 @@ class TestMailAPIPerformance(BaseMailPerformance): @users('__system__', 'employee') @warmup - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') + def test_mail_composer_attachments(self): + self._create_test_records() + test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) + customer = self.env['res.partner'].browse(self.customer.ids) + attachments = self.env['ir.attachment'].with_user(self.env.user).create(self.test_attachments_vals) + with self.assertQueryCount(__system__=3, employee=3): + composer = self.env['mail.compose.message'].with_context({ + 'default_composition_mode': 'comment', + 'default_model': test_record._name, + 'default_res_id': test_record.id, + }).create({ + 'attachment_ids': attachments.ids, + 'body': '

Test Body

', + 'partner_ids': [(4, customer.id)], + }) + + with self.assertQueryCount(__system__=36, employee=44): + composer._action_send_mail() + + @users('__system__', 'employee') + @warmup + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') + def test_mail_composer_form_attachments(self): + self._create_test_records() + test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) + customer = self.env['res.partner'].browse(self.customer.ids) + attachments = self.env['ir.attachment'].with_user(self.env.user).create(self.test_attachments_vals) + with self.assertQueryCount(__system__=15, employee=15): + composer_form = Form( + self.env['mail.compose.message'].with_context({ + 'default_composition_mode': 'comment', + 'default_model': test_record._name, + 'default_res_id': test_record.id, + }) + ) + composer_form.body = '

Test Body

' + composer_form.partner_ids.add(customer) + for attachment in attachments: + composer_form.attachment_ids.add(attachment) + composer = composer_form.save() + + with self.assertQueryCount(__system__=44, employee=52): + composer._action_send_mail() + + # notifications + message = test_record.message_ids[0] + self.assertEqual(message.attachment_ids, attachments) + self.assertEqual(message.notified_partner_ids, customer + self.user_test.partner_id) + + @users('__system__', 'employee') + @warmup + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') def test_mail_composer_nodelete(self): self._create_test_records() test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) @@ -300,11 +391,14 @@ class TestMailAPIPerformance(BaseMailPerformance): @users('__system__', 'employee') @warmup - @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') def test_mail_composer_w_template(self): self._create_test_records() + self.test_template_full.write({'attachment_ids': [(5, 0)]}) + test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) test_template = self.env['mail.template'].browse(self.test_template_full.id) + with self.assertQueryCount(__system__=11, employee=12): composer = self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', @@ -317,9 +411,111 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(__system__=33, employee=39): composer._action_send_mail() + # notifications + message = test_record.message_ids[0] + self.assertFalse(message.attachment_ids) + # remove created partner to ensure tests are the same each run self.env['res.partner'].sudo().search([('email', '=', 'nopartner.test@example.com')]).unlink() + @users('__system__', 'employee') + @warmup + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') + def test_mail_composer_w_template_attachments(self): + self._create_test_records() + + test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) + test_template = self.env['mail.template'].browse(self.test_template_full.id) + with self.assertQueryCount(__system__=12, employee=13): + composer = self.env['mail.compose.message'].with_context({ + 'default_composition_mode': 'comment', + 'default_model': test_record._name, + 'default_res_id': test_record.id, + 'default_template_id': test_template.id, + }).create({}) + composer._onchange_template_id_wrapper() + + with self.assertQueryCount(__system__=41, employee=52): + composer._action_send_mail() + + # notifications + message = test_record.message_ids[0] + self.assertEqual( + set(message.attachment_ids.mapped('name')), + set(['AttFileName_00.txt', 'AttFileName_01.txt', 'AttFileName_02.txt']) + ) + + # remove created partner to ensure tests are the same each run + self.env['res.partner'].sudo().search([('email', '=', 'nopartner.test@example.com')]).unlink() + + @users('__system__', 'employee') + @warmup + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') + def test_mail_composer_w_template_form(self): + self._create_test_records() + self.test_template_full.write({'attachment_ids': [(5, 0)]}) + + test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) + test_template = self.env['mail.template'].browse(self.test_template_full.id) + customer = self.env['res.partner'].browse(self.customer.ids) + with self.assertQueryCount(__system__=33, employee=33): + composer_form = Form( + self.env['mail.compose.message'].with_context({ + 'default_composition_mode': 'comment', + 'default_model': test_record._name, + 'default_res_id': test_record.id, + 'default_template_id': test_template.id, + }) + ) + composer = composer_form.save() + + with self.assertQueryCount(__system__=41, employee=47): + composer._action_send_mail() + + # notifications + new_partner = self.env['res.partner'].sudo().search([('email', '=', 'nopartner.test@example.com')]) + message = test_record.message_ids[0] + self.assertFalse(message.attachment_ids) + self.assertEqual(message.notified_partner_ids, customer + self.user_test.partner_id + new_partner) + + # remove created partner to ensure tests are the same each run + new_partner.unlink() + + @users('__system__', 'employee') + @warmup + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') + def test_mail_composer_w_template_form_attachments(self): + self._create_test_records() + + test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) + test_template = self.env['mail.template'].browse(self.test_template_full.id) + customer = self.env['res.partner'].browse(self.customer.ids) + with self.assertQueryCount(__system__=34, employee=34): + composer_form = Form( + self.env['mail.compose.message'].with_context({ + 'default_composition_mode': 'comment', + 'default_model': test_record._name, + 'default_res_id': test_record.id, + 'default_template_id': test_template.id, + }) + ) + composer = composer_form.save() + + with self.assertQueryCount(__system__=53, employee=70): + composer._action_send_mail() + + # notifications + new_partner = self.env['res.partner'].sudo().search([('email', '=', 'nopartner.test@example.com')]) + message = test_record.message_ids[0] + self.assertEqual( + set(message.attachment_ids.mapped('name')), + set(['AttFileName_00.txt', 'AttFileName_01.txt', 'AttFileName_02.txt']) + ) + self.assertEqual(message.notified_partner_ids, customer + self.user_test.partner_id + new_partner) + + # remove created partner to ensure tests are the same each run + new_partner.unlink() + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') @users('__system__', 'employee') @warmup @@ -885,7 +1081,7 @@ class TestMailComplexPerformance(BaseMailPerformance): messages.flush() messages.invalidate_cache() - with self.assertQueryCount(employee=13): + with self.assertQueryCount(employee=13): # tm only: 12 res = messages.message_format() self.assertEqual(len(res), 6) @@ -897,7 +1093,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): super(TestMailHeavyPerformancePost, self).setUp() # record - self.record = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({ + self.record_container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({ 'name': 'Test record', 'customer_id': self.customer.id, 'alias_name': 'test-alias', @@ -921,7 +1117,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): 'name': 'partner_follower', 'email': 'partner_follower@example.com', }) - self.record.message_subscribe([ + self.record_container.message_subscribe([ self.partner_follower.id, self.user_follower_inbox.partner_id.id, self.user_follower_email.partner_id.id @@ -946,45 +1142,38 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): 'name': 'partner', 'email': 'partner@example.com', }) - self.vals = [{ - 'datas': base64.b64encode(bytes("attachement content %s" % i, 'utf-8')), - 'name': 'fileText_test%s.txt' % i, - 'mimetype': 'text/plain', - 'res_model': 'mail.compose.message', - 'res_id': 0, - } for i in range(3)] @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') @users('employee') @warmup def test_complete_message_post(self): # aims to cover as much features of message_post as possible - partner_ids = [self.user_inbox.partner_id.id, self.user_email.partner_id.id, self.partner.id] - record = self.record.with_user(self.env.user) - attachements = [ # not linear on number of attachements + recipients = self.user_inbox.partner_id + self.user_email.partner_id + self.partner + record_container = self.record_container.with_user(self.env.user) + attachments_vals = [ # not linear on number of attachments_vals ('attach tuple 1', "attachement tupple content 1"), ('attach tuple 2', "attachement tupple content 2", {'cid': 'cid1'}), ('attach tuple 3', "attachement tupple content 3", {'cid': 'cid2'}), ] - self.attachements = self.env['ir.attachment'].with_user(self.env.user).create(self.vals) - attachement_ids = self.attachements.ids + attachments = self.env['ir.attachment'].with_user(self.env.user).create(self.test_attachments_vals) with self.assertQueryCount(employee=68): self.cr.sql_log = self.warm and self.cr.sql_log_count - record.with_context({}).message_post( + record_container.with_context({}).message_post( body='

Test body

', subject='Test Subject', message_type='notification', subtype_xmlid=None, - partner_ids=partner_ids, + partner_ids=recipients.ids, parent_id=False, - attachments=attachements, - attachment_ids=attachement_ids, + attachments=attachments_vals, + attachment_ids=attachments.ids, email_add_signature=True, model_description=False, mail_auto_delete=True ) self.cr.sql_log = False - self.assertTrue(record.message_ids[0].body.startswith('

Test body Date: Wed, 20 Oct 2021 11:55:19 +0000 Subject: [PATCH 7/9] [IMP] test_mail: add and improve posting tests PURPOSE Cleanup and improve post-related tests. As composer and posting methods will soon be improved better ensure we don't break flows. SPECIFICATIONS Cleanup tests added for message_post and the related API (notify, log, with view or template, ...) as well as sub-methods (notify, recipients computation and classification, ...). Use existing tools, reorder and add some details check on content. Add some tests with attachments, notably with main_attachment management. Task-2673913 (TestMail: Cleanup and improve test coverage) Part-of: odoo/odoo#86393 --- addons/test_mail/tests/test_message_post.py | 848 +++++++++++++------- 1 file changed, 543 insertions(+), 305 deletions(-) diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index 71352d2d5da..9659e516d7d 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -6,6 +6,7 @@ import base64 from unittest.mock import patch from odoo import tools +from odoo.addons.mail.tests.common import mail_new_test_user from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE_PLAINTEXT from odoo.addons.test_mail.models.test_mail_models import MailTestSimple from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients @@ -19,12 +20,29 @@ from markupsafe import escape @tagged('mail_post') -class TestMessagePost(TestMailCommon, TestRecipients): +class TestMessagePostCommon(TestMailCommon, TestRecipients): @classmethod def setUpClass(cls): - super(TestMessagePost, cls).setUpClass() - cls._create_portal_user() + super(TestMessagePostCommon, cls).setUpClass() + + # portal user, notably for ACLS / notifications + cls.user_portal = cls._create_portal_user() + cls.partner_portal = cls.user_portal.partner_id + + # another standard employee to test follow and notifications between two + # users (and not admin / user) + cls.user_employee_2 = mail_new_test_user( + cls.env, login='employee2', + groups='base.group_user', + company_id=cls.company_admin.id, + email='eglantine@example.com', # check: use a formatted email + name='Eglantine Employee2', + notification_type='email', + signature='--\nEglantine', + ) + cls.partner_employee_2 = cls.user_employee_2.partner_id + cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({ 'name': 'Test', 'email_from': 'ignasse@example.com' @@ -45,8 +63,22 @@ class TestMessagePost(TestMailCommon, TestRecipients): }) cls.user_admin.write({'notification_type': 'email'}) + def setUp(self): + super(TestMessagePostCommon, self).setUp() + # send tracking and messages + patch registry to simulate a ready environment + # purpose is to avoid nondeterministic tests, notably because tracking is + # accumulated and sent at flush -> we want to test only the result of a + # given test, not setup + test + self.flush_tracking() + # see ``_message_auto_subscribe_notify`` + self.patch(self.env.registry, 'ready', True) + + +@tagged('mail_post') +class TestMailNotifyAPI(TestMessagePostCommon): + @users('employee') - def test_notify_email_layouts(self): + def test_email_notifiction_layouts(self): self.user_employee.write({'notification_type': 'email'}) test_record = self.env['mail.test.simple'].browse(self.test_record.ids) test_message = self.env['mail.message'].browse(self.test_message.ids) @@ -74,12 +106,13 @@ class TestMessagePost(TestMailCommon, TestRecipients): self.assertTrue(user_email) @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') def test_notify_by_mail_add_signature(self): - self.test_track = self.env['mail.test.track'].with_context(self._test_context).with_user(self.user_employee).create({ + test_track = self.env['mail.test.track'].with_context(self._test_context).with_user(self.user_employee).create({ 'name': 'Test', 'email_from': 'ignasse@example.com' }) - self.test_track.user_id = self.env.user + test_track.user_id = self.env.user signature = self.env.user.signature @@ -87,7 +120,7 @@ class TestMessagePost(TestMailCommon, TestRecipients): self.assertIn("record.user_id.sudo().signature", template.arch) with self.mock_mail_gateway(): - self.test_track.message_post( + test_track.message_post( body="Test body", email_add_signature=True, email_layout_xmlid="mail.mail_notification_paynow", @@ -99,12 +132,12 @@ class TestMessagePost(TestMailCommon, TestRecipients): self.assertEqual(found_mail.body_html.count(signature), 1) with self.mock_mail_gateway(): - self.test_track.message_post( + test_track.message_post( body="Test body", email_add_signature=False, email_layout_xmlid="mail.mail_notification_paynow", mail_auto_delete=False, - partner_ids=[self.partner_1.id, self.partner_2.id] + partner_ids=[self.partner_1.id, self.partner_2.id], ) found_mail = self._new_mails self.assertNotIn(signature, found_mail.body_html) @@ -112,18 +145,22 @@ class TestMessagePost(TestMailCommon, TestRecipients): @users('employee') def test_notify_by_email_add_signature_no_author_user_or_no_user(self): - self.test_message.author_id = self.env['res.partner'].sudo().create({ - 'name': 'Steve', - }).id - template_values = self.test_record._notify_by_email_prepare_rendering_context(self.test_message, {}) + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + test_message = self.env['mail.message'].browse(self.test_message.ids) + test_message.write({ + 'author_id': self.env['res.partner'].sudo().create({ + 'name': 'Steve', + }).id + }) + template_values = test_record._notify_by_email_prepare_rendering_context(test_message, {}) self.assertNotEqual(escape(template_values['signature']), escape('

--
Steve

')) self.test_message.author_id = None - template_values = self.test_record._notify_by_email_prepare_rendering_context(self.test_message, {}) + template_values = test_record._notify_by_email_prepare_rendering_context(test_message, {}) self.assertEqual(template_values['signature'], '') @users('employee') - def test_notify_by_email_prepare_rendering_contextt(self): + def test_notify_by_email_prepare_rendering_context(self): """ Verify that the template context company value is right after switching the env company or if a company_id is set on mail record. @@ -225,300 +262,495 @@ class TestMessagePost(TestMailCommon, TestRecipients): self.assertFalse(partner_info['has_button_access']) self.assertFalse(emp_info['has_button_access']) + +@tagged('mail_post', 'mail_notify') +class TestMessageNotify(TestMessagePostCommon): + + @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_needaction(self): - (self.user_employee | self.user_admin).write({'notification_type': 'inbox'}) - with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], {'content': 'Body'}): - self.test_record.message_post( - body='Body', message_type='comment', subtype_xmlid='mail.mt_comment', - partner_ids=[self.user_employee.partner_id.id]) - - self.test_record.message_subscribe([self.partner_1.id]) - with self.assertSinglePostNotifications([ - {'partner': self.partner_employee, 'type': 'inbox'}, - {'partner': self.partner_1, 'type': 'email'}], {'content': 'NewBody'}): - self.test_record.message_post( - body='NewBody', message_type='comment', subtype_xmlid='mail.mt_comment', - partner_ids=[self.user_employee.partner_id.id]) - - with self.assertSinglePostNotifications([ - {'partner': self.partner_1, 'type': 'email'}, - {'partner': self.partner_portal, 'type': 'email'}], {'content': 'ToPortal'}): - self.test_record.message_post( - body='ToPortal', message_type='comment', subtype_xmlid='mail.mt_comment', - partner_ids=[self.partner_portal.id]) - - def test_post_inactive_follower(self): - # In some case odoobot is follower of a record. - # Even if it shouldn't be the case, we want to be sure that odoobot is not notified - (self.user_employee | self.user_admin).write({'notification_type': 'inbox'}) - self.test_record._message_subscribe(self.user_employee.partner_id.ids) - with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], {'content': 'Test'}): - self.test_record.message_post( - body='Test', message_type='comment', subtype_xmlid='mail.mt_comment') - - self.user_employee.active = False - # at this point, partner is still active and would receive an email notification - self.user_employee.partner_id._write({'active': False}) - with self.assertPostNotifications([{'content': 'Test', 'notif': []}]): - self.test_record.message_post( - body='Test', message_type='comment', subtype_xmlid='mail.mt_comment') - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_notifications(self): - _body, _subject = '

Test Body

', 'Test Subject' - - # 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): - msg = self.test_record.with_user(self.user_employee).message_post( - body=_body, subject=_subject, - message_type='comment', subtype_xmlid='mail.mt_comment', - partner_ids=[self.partner_1.id, self.partner_2.id] - ) - - # message content - self.assertEqual(msg.subject, _subject) - self.assertEqual(msg.body, _body) - self.assertEqual(msg.partner_ids, self.partner_1 | self.partner_2) - self.assertEqual(msg.notified_partner_ids, self.user_admin.partner_id | self.partner_1 | self.partner_2) - - # notifications emails should have been deleted - self.assertFalse(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)]), - 'message_post: mail.mail notifications should have been auto-deleted') - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_notifications_keep_emails(self): - self.test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id]) - - msg = self.test_record.with_user(self.user_employee).message_post( - body='Test', subject='Test', - message_type='comment', subtype_xmlid='mail.mt_comment', - partner_ids=[self.partner_1.id, self.partner_2.id], - mail_auto_delete=False - ) - - # notifications emails should not have been deleted: one for customers, one for user - self.assertEqual(len(self.env['mail.mail'].sudo().search([('mail_message_id', '=', msg.id)])), 2) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_notifications_emails_tweak(self): - pass - # we should check _notification_groups behavior, for emails and buttons - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_attachments(self): - _attachments = [ - ('List1', b'My first attachment'), - ('List2', b'My second attachment') - ] - _attach_1 = self.env['ir.attachment'].with_user(self.user_employee).create({ - 'name': 'Attach1', - 'datas': 'bWlncmF0aW9uIHRlc3Q=', - 'res_model': 'mail.compose.message', 'res_id': 0}) - _attach_2 = self.env['ir.attachment'].with_user(self.user_employee).create({ - 'name': 'Attach2', - 'datas': 'bWlncmF0aW9uIHRlc3Q=', - 'res_model': 'mail.compose.message', 'res_id': 0}) + def test_notify(self): + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) with self.mock_mail_gateway(): - msg = self.test_record.with_user(self.user_employee).message_post( - body='Test', subject='Test', - message_type='comment', subtype_xmlid='mail.mt_comment', - attachment_ids=[_attach_1.id, _attach_2.id], - partner_ids=[self.partner_1.id], - attachments=_attachments, - ) - - # message attachments - self.assertEqual(len(msg.attachment_ids), 4) - self.assertEqual(set(msg.attachment_ids.mapped('res_model')), set([self.test_record._name])) - self.assertEqual(set(msg.attachment_ids.mapped('res_id')), set([self.test_record.id])) - self.assertEqual(set([base64.b64decode(x) for x in msg.attachment_ids.mapped('datas')]), - set([b'migration test', _attachments[0][1], _attachments[1][1]])) - self.assertTrue(set([_attach_1.id, _attach_2.id]).issubset(msg.attachment_ids.ids), - 'message_post: mail.message attachments duplicated') - - # notification email attachments - self.assertSentEmail(self.user_employee.partner_id, [self.partner_1]) - # self.assertEqual(len(self._mails), 1) - self.assertEqual(len(self._mails[0]['attachments']), 4) - self.assertIn(('List1', b'My first attachment', 'application/octet-stream'), self._mails[0]['attachments']) - self.assertIn(('List2', b'My second attachment', 'application/octet-stream'), self._mails[0]['attachments']) - self.assertIn(('Attach1', b'migration test', 'application/octet-stream'), self._mails[0]['attachments']) - self.assertIn(('Attach2', b'migration test', 'application/octet-stream'), self._mails[0]['attachments']) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_answer(self): - with self.mock_mail_gateway(): - parent_msg = self.test_record.with_user(self.user_employee).message_post( - body='

Test

', subject='Test Subject', - message_type='comment', subtype_xmlid='mail.mt_comment') - - self.assertEqual(parent_msg.partner_ids, self.env['res.partner']) - self.assertNotSentEmail() - - with self.assertPostNotifications([{'content': '

Test Answer

', 'notif': [{'partner': self.partner_1, 'type': 'email'}]}]): - msg = self.test_record.with_user(self.user_employee).message_post( - body='

Test Answer

', - message_type='comment', subtype_xmlid='mail.mt_comment', - partner_ids=[self.partner_1.id], - parent_id=parent_msg.id) - - self.assertEqual(msg.parent_id.id, parent_msg.id) - self.assertEqual(msg.partner_ids, self.partner_1) - self.assertEqual(parent_msg.partner_ids, self.env['res.partner']) - - # check notification emails: references - self.assertSentEmail(self.user_employee.partner_id, [self.partner_1], ref_content='openerp-%d-mail.test.simple' % self.test_record.id) - # self.assertTrue(all('openerp-%d-mail.test.simple' % self.test_record.id in m['references'] for m in self._mails)) - - new_msg = self.test_record.with_user(self.user_employee).message_post( - body='

Test Answer Bis

', - message_type='comment', subtype_xmlid='mail.mt_comment', - parent_id=msg.id) - - self.assertEqual(new_msg.parent_id.id, parent_msg.id, 'message_post: flatten error') - self.assertEqual(new_msg.partner_ids, self.env['res.partner']) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_email_with_multiline_subject(self): - _body, _body_alt, _subject = '

Test Body

', 'Test Body', '1st line\n2nd line' - msg = self.test_record.with_user(self.user_employee).message_post( - body=_body, subject=_subject, - message_type='comment', subtype_xmlid='mail.mt_comment', - partner_ids=[self.partner_1.id, self.partner_2.id] - ) - self.assertEqual(msg.subject, '1st line 2nd line') - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_portal_ok(self): - self.test_record.message_subscribe((self.partner_1 | self.user_employee.partner_id).ids) - - with self.assertPostNotifications([{'content': '

Test

', 'notif': [ - {'partner': self.partner_employee, 'type': 'inbox'}, - {'partner': self.partner_1, 'type': 'email'}]} - ]), patch.object(MailTestSimple, 'check_access_rights', return_value=True): - new_msg = self.test_record.with_user(self.user_portal).message_post( - body='

Test

', subject='Subject', - message_type='comment', subtype_xmlid='mail.mt_comment') - - self.assertEqual(new_msg.sudo().notified_partner_ids, (self.partner_1 | self.user_employee.partner_id)) - - def test_post_portal_crash(self): - with self.assertRaises(AccessError): - self.test_record.with_user(self.user_portal).message_post( - body='

Test

', subject='Subject', - message_type='comment', subtype_xmlid='mail.mt_comment') - - @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread') - def test_post_internal(self): - self.test_record.message_subscribe([self.user_admin.partner_id.id]) - msg = self.test_record.with_user(self.user_employee).message_post( - body='My Body', subject='My Subject', - message_type='comment', subtype_xmlid='mail.mt_note') - self.assertEqual(msg.partner_ids, self.env['res.partner']) - self.assertEqual(msg.notified_partner_ids, self.env['res.partner']) - - self.format_and_process( - MAIL_TEMPLATE_PLAINTEXT, self.user_admin.email, 'not_my_businesss@example.com', - msg_id='<1198923581.41972151344608186800.JavaMail.diff1@agrolait.com>', - extra='In-Reply-To:\r\n\t%s\n' % msg.message_id, - target_model='mail.test.simple') - reply = self.test_record.message_ids - msg - self.assertTrue(reply) - self.assertEqual(reply.subtype_id, self.env.ref('mail.mt_note')) - self.assertEqual(reply.notified_partner_ids, self.user_employee.partner_id) - self.assertEqual(reply.parent_id, msg) - - def test_post_log(self): - new_note = self.test_record.with_user(self.user_employee)._message_log( - body='

Labrador

', - ) - - self.assertEqual(new_note.subtype_id, self.env.ref('mail.mt_note')) - self.assertEqual(new_note.body, '

Labrador

') - self.assertEqual(new_note.author_id, self.user_employee.partner_id) - self.assertEqual(new_note.email_from, formataddr((self.user_employee.name, self.user_employee.email))) - self.assertEqual(new_note.notified_partner_ids, self.env['res.partner']) - - def test_post_log_with_view(self): - new_note = self.test_record.with_user(self.user_employee)._message_log_with_view( - 'test_mail.mail_template_simple_test', values={'partner': self.user_employee.partner_id} - ) - - self.assertEqual(new_note.subtype_id, self.env.ref('mail.mt_note')) - self.assertTrue('

Hello %s,

' % self.user_employee.name in new_note.body) - self.assertEqual(new_note.author_id, self.user_employee.partner_id) - self.assertEqual(new_note.email_from, formataddr((self.user_employee.name, self.user_employee.email))) - self.assertEqual(new_note.notified_partner_ids, self.env['res.partner']) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_notify(self): - self.user_employee.write({'notification_type': 'inbox'}) - - with self.mock_mail_gateway(): - new_notification = self.test_record.message_notify( - subject='This should be a subject', + new_notification = test_record.message_notify( body='

You have received a notification

', - partner_ids=[self.partner_1.id, self.partner_admin.id, self.user_employee.partner_id.id], + partner_ids=[self.partner_1.id, self.partner_admin.id, self.partner_employee_2.id], + subject='This should be a subject', ) - self.assertEqual(new_notification.subtype_id, self.env.ref('mail.mt_note')) - self.assertEqual(new_notification.message_type, 'user_notification') - self.assertEqual(new_notification.body, '

You have received a notification

') - self.assertEqual(new_notification.author_id, self.env.user.partner_id) - self.assertEqual(new_notification.email_from, formataddr((self.env.user.name, self.env.user.email))) - self.assertEqual(new_notification.notified_partner_ids, self.partner_1 | self.user_employee.partner_id | self.partner_admin) + self.assertMessageFields( + new_notification, + {'author_id': self.partner_employee, + 'body': '

You have received a notification

', + 'email_from': formataddr((self.partner_employee.name, self.partner_employee.email_normalized)), + 'message_type': 'user_notification', + 'model': test_record._name, + 'notified_partner_ids': self.partner_1 | self.partner_employee_2 | self.partner_admin, + 'res_id': test_record.id, + 'subtype_id': self.env.ref('mail.mt_note'), + } + ) self.assertNotIn(new_notification, self.test_record.message_ids) - admin_mails = [x for x in self._mails if self.partner_admin.name in x.get('email_to')[0]] + admin_mails = [mail for mail in self._mails if self.partner_admin.name in mail.get('email_to')[0]] self.assertEqual(len(admin_mails), 1, 'There should be exactly one email sent to admin') - admin_mail = admin_mails[0].get('body') - admin_access_link = admin_mail[admin_mail.index('model='):admin_mail.index('/>') - 1] if 'model=' in admin_mail else None - + admin_mail_body = admin_mails[0].get('body') + admin_access_link = admin_mail_body[admin_mail_body.index('model='):admin_mail_body.index('/>') - 1] if 'model=' in admin_mail_body else None + self.assertIsNotNone(admin_access_link, 'The email sent to admin should contain an access link') self.assertIn('model=%s' % self.test_record._name, admin_access_link, 'The access link should contain a valid model argument') self.assertIn('res_id=%d' % self.test_record.id, admin_access_link, 'The access link should contain a valid res_id argument') partner_mails = [x for x in self._mails if self.partner_1.name in x.get('email_to')[0]] self.assertEqual(len(partner_mails), 1, 'There should be exactly one email sent to partner') - partner_mail = partner_mails[0].get('body') - self.assertNotIn('/mail/view?model=', partner_mail, 'The email sent to admin should not contain an access link') - # todo xdo add test message_notify on thread with followers and stuff + partner_mail_body = partner_mails[0].get('body') + self.assertNotIn('/mail/view?model=', partner_mail_body, 'The email sent to customer should not contain an access link') + + @users('employee') + def test_notify_batch(self): + """ Test notify in batch. Currently not supported. """ + test_records, _partners = self._create_records_for_batch('mail.test.simple', 10) + + with self.assertRaises(ValueError): + test_records.message_notify( + body='

Nice notification content

', + partner_ids=self.partner_employee_2.ids, + subject='Notify Subject', + ) + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_notify_from_user_id(self): + """ Test notify coming from user_id assignment. """ + test_record = self.env['mail.test.track'].create({ + 'company_id': self.env.user.company_id.id, + 'email_from': self.env.user.email_formatted, + 'name': 'Test UserId Track', + 'user_id': False, + }) + self.flush_tracking() + + with self.mock_mail_gateway(), self.mock_mail_app(): + test_record.write({'user_id': self.user_employee_2.id}) + self.flush_tracking() + + self.assertEqual(len(self._new_msgs), 2, 'Should have 2 messages: tracking and assignment') + assign_notif = self._new_msgs.filtered(lambda msg: msg.message_type == 'user_notification') + self.assertTrue(assign_notif) + self.assertMessageFields( + assign_notif, + {'author_id': self.partner_employee, + 'email_from': formataddr((self.partner_employee.name, self.partner_employee.email_normalized)), + 'model': test_record._name, + 'notified_partner_ids': self.partner_employee_2, + 'res_id': test_record.id, + 'subtype_id': self.env.ref('mail.mt_note'), + } + ) + self.assertIn('Dear %s' % self.partner_employee_2.name, assign_notif.body) + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_notify_from_user_id_batch(self): + """ Test notify coming from user_id assignment. """ + test_records, _ = self._create_records_for_batch( + 'mail.test.track', 10, { + 'company_id': self.env.user.company_id.id, + 'email_from': self.env.user.email_formatted, + 'user_id': False, + } + ) + test_records = self.env['mail.test.track'].browse(test_records.ids) + self.flush_tracking() + + with self.mock_mail_gateway(), self.mock_mail_app(): + test_records.write({'user_id': self.user_employee_2.id}) + self.flush_tracking() + + self.assertEqual(len(self._new_msgs), 20, 'Should have 20 messages: 10 tracking and 10 assignments') + for test_record in test_records: + assign_notif = self._new_msgs.filtered(lambda msg: msg.message_type == 'user_notification' and msg.res_id == test_record.id) + self.assertTrue(assign_notif) + self.assertMessageFields( + assign_notif, + {'author_id': self.partner_employee, + 'email_from': formataddr((self.partner_employee.name, self.partner_employee.email_normalized)), + 'model': test_record._name, + 'notified_partner_ids': self.partner_employee_2, + 'res_id': test_record.id, + 'subtype_id': self.env.ref('mail.mt_note'), + } + ) + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_notify_thread(self): + """ Test notify on ``mail.thread`` model, which is pushing a message to + people without having a document. """ + with self.mock_mail_gateway(): + new_notification = self.env['mail.thread'].message_notify( + body='

You have received a notification

', + partner_ids=[self.partner_1.id, self.partner_admin.id, self.partner_employee_2.id], + subject='This should be a subject', + ) + + self.assertMessageFields( + new_notification, + {'author_id': self.partner_employee, + 'body': '

You have received a notification

', + 'email_from': formataddr((self.partner_employee.name, self.partner_employee.email_normalized)), + 'message_type': 'user_notification', + 'model': False, + 'res_id': False, + 'notified_partner_ids': self.partner_1 | self.partner_employee_2 | self.partner_admin, + 'subtype_id': self.env.ref('mail.mt_note'), + } + ) + + +@tagged('mail_post') +class TestMessagePost(TestMessagePostCommon): + + def test_initial_values(self): + self.assertFalse(self.test_record.message_ids) + self.assertFalse(self.test_record.message_follower_ids) + self.assertFalse(self.test_record.message_partner_ids) + + @users('employee') + def test_message_log(self): + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + test_record.message_subscribe(self.partner_employee_2.ids) + + new_note = test_record._message_log( + body='

Labrador

', + ) + self.assertMessageFields( + new_note, + {'author_id': self.partner_employee, + 'body': '

Labrador

', + 'email_from': formataddr((self.partner_employee.name, self.partner_employee.email_normalized)), + 'is_internal': True, + 'message_type': 'notification', + 'model': test_record._name, + 'notified_partner_ids': self.env['res.partner'], + 'reply_to': formataddr((self.company_admin.name, '%s@%s' % (self.alias_catchall, self.alias_domain))), + 'res_id': test_record.id, + 'subtype_id': self.env.ref('mail.mt_note'), + } + ) + + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('employee') + def test_message_post(self): + self.user_employee_2.write({'notification_type': 'inbox'}) + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + + with self.assertSinglePostNotifications( + [{'partner': self.partner_employee_2, 'type': 'inbox'}], + {'content': 'Body'} + ): + new_message = test_record.message_post( + body='Body', + message_type='comment', + subtype_xmlid='mail.mt_comment', + partner_ids=[self.partner_employee_2.id], + ) + + self.assertMessageFields( + new_message, + {'author_id': self.partner_employee, + 'body': '

Body

', + 'email_from': formataddr((self.partner_employee.name, self.partner_employee.email_normalized)), + 'is_internal': False, + 'message_type': 'comment', + 'model': test_record._name, + 'notified_partner_ids': self.partner_employee_2, + 'reply_to': formataddr(("%s %s" % (self.company_admin.name, test_record.name), '%s@%s' % (self.alias_catchall, self.alias_domain))), + 'res_id': test_record.id, + 'subtype_id': self.env.ref('mail.mt_comment'), + } + ) + self.assertEqual(test_record.message_partner_ids, self.partner_employee) + + test_record.message_subscribe(self.partner_1.ids) + with self.assertSinglePostNotifications( + [{'partner': self.partner_employee_2, 'type': 'inbox'}, + {'partner': self.partner_1, 'type': 'email'}], + message_info={'content': 'NewBody'}, + mail_unlink_sent=True + ): + new_message = test_record.message_post( + body='NewBody', + message_type='comment', + subtype_xmlid='mail.mt_comment', + partner_ids=[self.partner_employee_2.id], + ) + + self.assertMessageFields( + new_message, + {'notified_partner_ids': self.partner_1 + self.partner_employee_2} + ) + # notifications emails should have been deleted + self.assertFalse(self.env['mail.mail'].sudo().search_count([('mail_message_id', '=', new_message.id)])) + + with self.assertSinglePostNotifications( + [{'partner': self.partner_1, 'type': 'email'}, + {'partner': self.partner_portal, 'type': 'email'}], + {'content': 'ToPortal'} + ): + test_record.message_post( + body='ToPortal', + message_type='comment', + subtype_xmlid='mail.mt_comment', + partner_ids=self.partner_portal.ids, + ) + + @users('employee') + def test_message_post_inactive_follower(self): + """ Test posting with inactive followers does not notify them (e.g. odoobot) """ + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + test_record._message_subscribe(self.user_employee_2.partner_id.ids) + self.user_employee_2.write({'active': False}) + self.partner_employee_2.write({'active': False}) + + with self.assertPostNotifications([{'content': 'Test', 'notif': []}]): + test_record.message_post( + body='Test', + message_type='comment', + subtype_xmlid='mail.mt_comment', + ) @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_post_w_template(self): - test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) - self.user_employee.write({ - 'groups_id': [(4, self.env.ref('base.group_partner_manager').id)], + @users('employee') + def test_message_post_keep_emails(self): + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + test_record.message_subscribe(partner_ids=self.partner_employee_2.ids) + + with self.mock_mail_gateway(mail_unlink_sent=True): + msg = test_record.message_post( + body='Test', + mail_auto_delete=False, + message_type='comment', + partner_ids=[self.partner_1.id, self.partner_2.id], + subject='Test', + subtype_xmlid='mail.mt_comment', + ) + + # notifications emails should not have been deleted: one for customers, one for user + self.assertEqual(self.env['mail.mail'].sudo().search_count([('mail_message_id', '=', msg.id)]), 2) + + @mute_logger('odoo.addons.mail.models.mail_mail') + @users('employee') + def test_message_post_w_attachments(self): + _attachments = [ + ('List1', b'My first attachment'), + ('List2', b'My second attachment'), + ] + _attachment_records = self.env['ir.attachment'].create( + self._generate_attachments_data(3, res_id=0, res_model='mail.compose.message') + ) + _attachment_records[1].write({'mimetype': 'image/png'}) # to test message_main_attachment heuristic + + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + self.assertFalse(test_record.message_main_attachment_id) + + with self.mock_mail_gateway(): + msg = test_record.message_post( + attachments=_attachments, + attachment_ids=_attachment_records.ids, + body='Test', + message_type='comment', + partner_ids=[self.partner_1.id], + subject='Test', + subtype_xmlid='mail.mt_comment', + ) + + # updated message main attachment + self.assertEqual(test_record.message_main_attachment_id, _attachment_records[1], + 'MailThread: main attachment should be set to image/png') + + # message attachments + self.assertEqual(len(msg.attachment_ids), 5) + self.assertEqual(set(msg.attachment_ids.mapped('res_model')), set([self.test_record._name])) + self.assertEqual(set(msg.attachment_ids.mapped('res_id')), set([self.test_record.id])) + self.assertEqual(set(base64.b64decode(x) for x in msg.attachment_ids.mapped('datas')), + set([b'AttContent_00', b'AttContent_01', b'AttContent_02', _attachments[0][1], _attachments[1][1]])) + self.assertTrue(set(_attachment_records.ids).issubset(msg.attachment_ids.ids), + 'message_post: mail.message attachments duplicated') + + # notification email attachments + self.assertEqual(len(self._mails), 1) + self.assertSentEmail( + self.user_employee.partner_id, [self.partner_1], + attachments=[('List1', b'My first attachment', 'application/octet-stream'), + ('List2', b'My second attachment', 'application/octet-stream'), + ('AttFileName_00.txt', b'AttContent_00', 'text/plain'), + ('AttFileName_01.txt', b'AttContent_01', 'image/png'), + ('AttFileName_02.txt', b'AttContent_02', 'text/plain'), + ] + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_multiline_subject(self): + with self.mock_mail_gateway(): + msg = self.test_record.with_user(self.user_employee).message_post( + body='

Test Body

', + partner_ids=[self.partner_1.id, self.partner_2.id], + subject='1st line\n2nd line', + ) + self.assertEqual(msg.subject, '1st line 2nd line') + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_portal_acls(self): + self.test_record.message_subscribe((self.partner_1 | self.user_employee.partner_id).ids) + + with self.assertPostNotifications( + [{'content': '

Test

', 'notif': [ + {'partner': self.partner_employee, 'type': 'inbox'}, + {'partner': self.partner_1, 'type': 'email'}]} + ] + ), patch.object(MailTestSimple, 'check_access_rights', return_value=True): + new_msg = self.test_record.with_user(self.user_portal).message_post( + body='

Test

', + message_type='comment', + subject='Subject', + subtype_xmlid='mail.mt_comment', + ) + self.assertEqual(new_msg.sudo().notified_partner_ids, (self.partner_1 | self.user_employee.partner_id)) + + with self.assertRaises(AccessError): + self.test_record.with_user(self.user_portal).message_post( + body='

Test

', + message_type='comment', + subject='Subject', + subtype_xmlid='mail.mt_comment', + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + @users('employee') + def test_post_answer(self): + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + + with self.mock_mail_gateway(): + parent_msg = test_record.message_post( + body='

Test

', + message_type='comment', + subject='Test Subject', + subtype_xmlid='mail.mt_comment', + ) + self.assertFalse(parent_msg.partner_ids) + self.assertNotSentEmail() + + with self.assertPostNotifications( + [{'content': '

Test Answer

', 'notif': [{'partner': self.partner_1, 'type': 'email'}]}] + ): + msg = test_record.message_post( + body='

Test Answer

', + message_type='comment', + parent_id=parent_msg.id, + partner_ids=[self.partner_1.id], + subtype_xmlid='mail.mt_comment', + ) + self.assertEqual(msg.parent_id, parent_msg) + self.assertEqual(msg.partner_ids, self.partner_1) + self.assertFalse(parent_msg.partner_ids) + + # check notification emails: references + self.assertSentEmail( + self.user_employee.partner_id, [self.partner_1], + ref_content='openerp-%d-mail.test.simple' % self.test_record.id + ) + + with self.mock_mail_gateway(): + new_msg = test_record.message_post( + body='

Test Answer Bis

', + message_type='comment', + parent_id=msg.id, + subtype_xmlid='mail.mt_comment', + ) + self.assertEqual(new_msg.parent_id, parent_msg, 'message_post: flatten error') + self.assertFalse(new_msg.partner_ids) + + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread') + @users('employee') + def test_post_internal(self): + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + + test_record.message_subscribe([self.user_admin.partner_id.id]) + with self.mock_mail_gateway(): + msg = test_record.message_post( + body='My Body', + message_type='comment', + subject='My Subject', + subtype_xmlid='mail.mt_note', + ) + self.assertFalse(msg.is_internal, + 'Notes are not "internal" but replies will be. Subtype being internal should be sufficient from ACLs point of view.') + self.assertFalse(msg.partner_ids) + self.assertFalse(msg.notified_partner_ids) + + self.format_and_process( + MAIL_TEMPLATE_PLAINTEXT, self.user_admin.email, 'not_my_businesss@example.com', + msg_id='<1198923581.41972151344608186800.JavaMail.diff1@agrolait.example.com>', + extra='In-Reply-To:\r\n\t%s\n' % msg.message_id, + target_model='mail.test.simple') + reply = test_record.message_ids - msg + self.assertTrue(reply) + self.assertTrue(reply.is_internal) + self.assertEqual(reply.notified_partner_ids, self.user_employee.partner_id) + self.assertEqual(reply.parent_id, msg) + self.assertEqual(reply.subtype_id, self.env.ref('mail.mt_note')) + + +@tagged('mail_post') +class TestMessagePostHelpers(TestMessagePostCommon): + + @classmethod + def setUpClass(cls): + super(TestMessagePostHelpers, 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)], }) - _attachments = [{ - 'name': 'first.txt', - 'datas': base64.b64encode(b'My first attachment'), - 'res_model': 'res.partner', - 'res_id': self.user_admin.partner_id.id - }, { - 'name': 'second.txt', - 'datas': base64.b64encode(b'My second attachment'), - 'res_model': 'res.partner', - 'res_id': self.user_admin.partner_id.id - }] + + @users('employee') + def test_message_log_with_view(self): + test_record = self.env['mail.test.simple'].browse(self.test_record.ids) + new_note = test_record._message_log_with_view( + 'test_mail.mail_template_simple_test', + values={'partner': self.user_employee.partner_id} + ) + self.assertMessageFields( + new_note, + {'author_id': self.partner_employee, + 'body': '

Hello %s,

' % self.user_employee.name, + 'email_from': formataddr((self.partner_employee.name, self.partner_employee.email_normalized)), + 'is_internal': True, + 'message_type': 'notification', + 'model': test_record._name, + 'notified_partner_ids': self.env['res.partner'], + 'reply_to': formataddr((self.company_admin.name, '%s@%s' % (self.alias_catchall, self.alias_domain))), + 'res_id': test_record.id, + 'subtype_id': self.env.ref('mail.mt_note'), + } + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_message_post_w_template(self): + test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + + _attachments = self._generate_attachments_data(count=2, res_id=0, res_model='mail.template') email_1 = 'test1@example.com' email_2 = 'test2@example.com' email_3 = self.partner_1.email - self._create_template('mail.test.simple', { - 'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])], + template = self._create_template('mail.test.simple', { + 'attachment_ids': [(0, 0, attach_vals) for attach_vals in _attachments], 'partner_to': '%s,%s' % (self.partner_2.id, self.user_admin.partner_id.id), 'email_to': '%s, %s' % (email_1, email_2), 'email_cc': '%s' % email_3, }) + template.attachment_ids.write({'res_id': template.id}) + # admin should receive emails self.user_admin.write({'notification_type': 'email'}) # Force the attachments of the template to be in the natural order. @@ -534,25 +766,35 @@ class TestMessagePost(TestMailCommon, TestRecipients): [r], subject='About %s' % test_record.name, body_content=test_record.name, - attachments=[('first.txt', b'My first attachment', 'text/plain'), ('second.txt', b'My second attachment', 'text/plain')]) + attachments=[ + ('AttFileName_00.txt', b'AttContent_00', 'text/plain'), + ('AttFileName_01.txt', b'AttContent_01', 'text/plain'), + ] + ) @mute_logger('odoo.addons.mail.models.mail_mail') - def test_post_post_w_template_mass_mode(self): + def test_message_post_w_template_mass_mode(self): test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) self.user_employee.write({ 'groups_id': [(4, self.env.ref('base.group_partner_manager').id)], }) - self._create_template('mail.test.simple', { - 'partner_to': '%s,%s' % (self.partner_2.id, self.user_admin.partner_id.id), - 'email_to': 'test@example.com', - 'email_cc': self.partner_1.email, + _attachments = self._generate_attachments_data(count=2, res_id=0, res_model='mail.template') + template = self._create_template('mail.test.simple', { + 'attachment_ids': [(0, 0, attach_vals) for attach_vals in _attachments], # After the HTML sanitizer, it will become "

Body for: link

" 'body_html': 'Body for: link', + 'email_to': 'test@example.com', + 'email_cc': self.partner_1.email, + 'partner_to': '%s,%s' % (self.partner_2.id, self.user_admin.partner_id.id), }) + template.attachment_ids.write({'res_id': template.id}) with self.mock_mail_gateway(): - test_record.with_user(self.user_employee).message_post_with_template(self.email_template.id, composition_mode='mass_mail') + test_record.with_user(self.user_employee).message_post_with_template( + template.id, + composition_mode='mass_mail' + ) new_partner = self.env['res.partner'].search([('email', '=', 'test@example.com')]) @@ -561,21 +803,15 @@ class TestMessagePost(TestMailCommon, TestRecipients): [new_partner], subject='About %s' % test_record.name, body_content=test_record.name, - attachments=[]) + attachments=[ + ('AttFileName_00.txt', b'AttContent_00', 'text/plain'), + ('AttFileName_01.txt', b'AttContent_01', 'text/plain'), + ] + ) @tagged('mail_post', 'post_install', '-at_install') -class TestMessagePostGlobal(TestMailCommon, TestRecipients): - - @classmethod - def setUpClass(cls): - super(TestMessagePostGlobal, cls).setUpClass() - cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({ - 'email_from': 'ignasse@example.com', - 'name': 'Test', - }) - cls._reset_mail_context(cls.test_record) - cls.user_admin.write({'notification_type': 'email'}) +class TestMessagePostGlobal(TestMessagePostCommon): @users('employee') def test_message_post_return(self): @@ -586,8 +822,7 @@ class TestMessagePostGlobal(TestMailCommon, TestRecipients): message_id = call_kw(self.env['mail.test.simple'], 'message_post', [test_record.id], - {'body': 'test'} - ) + {'body': 'test'}) self.assertTrue(isinstance(message_id, int)) @@ -631,6 +866,7 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): cls.partner_2.write({'lang': 'es_ES'}) @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') def test_composer_lang_template(self): test_records = self.test_records.with_user(self.env.user) test_template = self.test_template.with_user(self.env.user) @@ -659,6 +895,7 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): self.assertEqual('EnglishSubject for %s' % record.name, customer_email['subject'], 'Fixme: this should be translated') @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') def test_layout_email_lang_context(self): test_records = self.test_records.with_user(self.env.user).with_context(lang='es_ES') test_records[1].message_subscribe(self.partner_2.ids) @@ -687,6 +924,7 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): self.assertIn('Hello', body, 'Body of posted message should be present') @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') def test_layout_email_lang_template(self): test_records = self.test_records.with_user(self.env.user) test_template = self.test_template.with_user(self.env.user) From 363f626a1bbaecf86309fbd2388b1c559e0cb165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 2 Dec 2021 12:58:25 +0000 Subject: [PATCH 8/9] [IMP] test_mail_full: add performance tests Purpose is to add some query counters tests in test_mail_full that allows to test more complete flows. Soon portal and rating will be added as dependencies to see their potential impact. This also serves as a basis for adding new tests with all sub mail modules in a near future. Task-2673913 (TestMail: Cleanup and improve test coverage) Part-of: odoo/odoo#86393 --- addons/test_mail_full/models/__init__.py | 2 +- ...mail_models.py => test_mail_models_sms.py} | 0 addons/test_mail_full/tests/__init__.py | 1 + .../tests/test_mail_performance.py | 96 +++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) rename addons/test_mail_full/models/{test_mail_models.py => test_mail_models_sms.py} (100%) create mode 100644 addons/test_mail_full/tests/test_mail_performance.py diff --git a/addons/test_mail_full/models/__init__.py b/addons/test_mail_full/models/__init__.py index 26ed2919633..b72c7ebd3fb 100644 --- a/addons/test_mail_full/models/__init__.py +++ b/addons/test_mail_full/models/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import test_mail_models +from . import test_mail_models_sms diff --git a/addons/test_mail_full/models/test_mail_models.py b/addons/test_mail_full/models/test_mail_models_sms.py similarity index 100% rename from addons/test_mail_full/models/test_mail_models.py rename to addons/test_mail_full/models/test_mail_models_sms.py diff --git a/addons/test_mail_full/tests/__init__.py b/addons/test_mail_full/tests/__init__.py index 9b71f41ab53..d3948f83d4d 100644 --- a/addons/test_mail_full/tests/__init__.py +++ b/addons/test_mail_full/tests/__init__.py @@ -2,6 +2,7 @@ from . import common from . import test_odoobot +from . import test_mail_performance from . import test_phone_blacklist from . import test_mass_mailing from . import test_mass_mailing_statistics diff --git a/addons/test_mail_full/tests/test_mail_performance.py b/addons/test_mail_full/tests/test_mail_performance.py new file mode 100644 index 00000000000..550985a2a33 --- /dev/null +++ b/addons/test_mail_full/tests/test_mail_performance.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.test_mail.tests.test_performance import BaseMailPerformance +from odoo.tests.common import users, warmup +from odoo.tests import tagged +from odoo.tools import mute_logger + + +@tagged('mail_performance') +class TestMailPerformance(BaseMailPerformance): + + @classmethod + def setUpClass(cls): + super(TestMailPerformance, cls).setUpClass() + + # users / followers + cls.user_emp_email = mail_new_test_user( + cls.env, + company_id=cls.user_admin.company_id.id, + company_ids=[(4, cls.user_admin.company_id.id)], + email='user.emp.email@test.example.com', + login='user_emp_email', + groups='base.group_user,base.group_partner_manager', + name='Emmanuel Email', + notification_type='email', + signature='--\nEmmanuel', + ) + cls.user_portal = mail_new_test_user( + cls.env, + company_id=cls.user_admin.company_id.id, + company_ids=[(4, cls.user_admin.company_id.id)], + email='user.portal@test.example.com', + login='user_portal', + groups='base.group_portal', + name='Paul Portal', + ) + cls.customers = cls.env['res.partner'].create([ + {'country_id': cls.env.ref('base.be').id, + 'email': 'customer.full.test.1@example.com', + 'name': 'Test Full Customer 1', + 'mobile': '0456112233', + 'phone': '0456112233', + }, + {'country_id': cls.env.ref('base.be').id, + 'email': 'customer.full.test.2@example.com', + 'name': 'Test Full Customer 2', + 'mobile': '0456223344', + 'phone': '0456112233', + }, + ]) + + # record + cls.record_container = cls.env['mail.test.container.mc'].create({ + 'alias_name': 'test-alias', + 'customer_id': cls.customer.id, + 'name': 'Test Container', + }) + cls.record_ticket = cls.env['mail.test.ticket.mc'].create({ + 'email_from': 'email.from@test.example.com', + 'container_id': cls.record_container.id, + 'customer_id': False, + 'name': 'Test Ticket', + 'user_id': cls.user_emp_email.id, + }) + cls.record_ticket.message_subscribe(cls.customers.ids + cls.user_admin.partner_id.ids + cls.user_portal.partner_id.ids) + + def test_initial_values(self): + """ Simply ensure some values through all tests """ + record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids) + self.assertEqual(record_ticket.message_partner_ids, + self.user_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_portal.partner_id) + self.assertEqual(len(record_ticket.message_ids), 1) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('employee') + @warmup + def test_message_post_w_followers(self): + """ Aims to cover as much features of message_post as possible """ + record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids) + attachments = self.env['ir.attachment'].create(self.test_attachments_vals) + + with self.assertQueryCount(employee=102): # tmf: 93 / com: 100 + new_message = record_ticket.message_post( + attachment_ids=attachments.ids, + body='

Test Content

', + message_type='comment', + subject='Test Subject', + subtype_xmlid='mail.mt_comment', + ) + + self.assertEqual( + new_message.notified_partner_ids, + self.user_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_portal.partner_id + ) From bf1adc2d2c451568cb5655953cc5b0020c0252b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 2 Dec 2021 11:47:27 +0000 Subject: [PATCH 9/9] [IMP] test_mail_full: add portal and rating dependencies and tests Purpose of this commit is to add some first tests about rating and rating mixin. Indeed those are not really tested by standard python unit tests. In this commit we add tests about * rating preparation: token, information from the record to rate; * rating apply: posting message, author; Task-2673913 (TestMail: Cleanup and improve test coverage) Task-2704390 (TestMailFull: add first rating tests) Part-of: odoo/odoo#86393 --- addons/test_mail_full/__manifest__.py | 3 + addons/test_mail_full/models/__init__.py | 1 + .../models/test_mail_models_mail.py | 55 ++++++++++++++ .../security/ir.model.access.csv | 3 + .../test_mail_full/security/ir_rule_data.xml | 17 +++++ addons/test_mail_full/tests/__init__.py | 1 + addons/test_mail_full/tests/test_rating.py | 74 +++++++++++++++++++ 7 files changed, 154 insertions(+) create mode 100644 addons/test_mail_full/models/test_mail_models_mail.py create mode 100644 addons/test_mail_full/security/ir_rule_data.xml create mode 100644 addons/test_mail_full/tests/test_rating.py diff --git a/addons/test_mail_full/__manifest__.py b/addons/test_mail_full/__manifest__.py index 586ee908821..0fdf6b0212d 100644 --- a/addons/test_mail_full/__manifest__.py +++ b/addons/test_mail_full/__manifest__.py @@ -15,6 +15,8 @@ real applications. """, 'test_mass_mailing', 'mail', 'mail_bot', + 'portal', + 'rating', # 'snailmail', 'mass_mailing', 'mass_mailing_sms', @@ -23,6 +25,7 @@ real applications. """, ], 'data': [ 'security/ir.model.access.csv', + 'security/ir_rule_data.xml', ], 'demo': [ ], diff --git a/addons/test_mail_full/models/__init__.py b/addons/test_mail_full/models/__init__.py index b72c7ebd3fb..3948f713541 100644 --- a/addons/test_mail_full/models/__init__.py +++ b/addons/test_mail_full/models/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- +from . import test_mail_models_mail from . import test_mail_models_sms diff --git a/addons/test_mail_full/models/test_mail_models_mail.py b/addons/test_mail_full/models/test_mail_models_mail.py new file mode 100644 index 00000000000..80bdc88dad7 --- /dev/null +++ b/addons/test_mail_full/models/test_mail_models_mail.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class MailTestRating(models.Model): + """ A model inheriting from mail.thread with some fields used for SMS + gateway, like a partner, a specific mobile phone, ... """ + _description = 'Rating Model (ticket-like)' + _name = 'mail.test.rating' + _inherit = [ + 'mail.thread', + 'mail.activity.mixin', + 'rating.mixin', + 'portal.mixin', + ] + _mailing_enabled = True + _order = 'name asc, id asc' + + name = fields.Char() + subject = fields.Char() + company_id = fields.Many2one('res.company', 'Company') + customer_id = fields.Many2one('res.partner', 'Customer') + email_from = fields.Char(compute='_compute_email_from', precompute=True, readonly=False, store=True) + mobile_nbr = fields.Char(compute='_compute_mobile_nbr', precompute=True, readonly=False, store=True) + phone_nbr = fields.Char(compute='_compute_phone_nbr', precompute=True, readonly=False, store=True) + user_id = fields.Many2one('res.users', 'Responsible', tracking=1) + + @api.depends('customer_id') + def _compute_email_from(self): + for rating in self: + if rating.customer_id.email_normalized: + rating.email_from = rating.customer_id.email_normalized + elif not rating.email_from: + rating.email_from = False + + @api.depends('customer_id') + def _compute_mobile_nbr(self): + for rating in self: + if rating.customer_id.mobile: + rating.mobile_nbr = rating.customer_id.mobile + elif not rating.mobile_nbr: + rating.mobile_nbr = False + + @api.depends('customer_id') + def _compute_phone_nbr(self): + for rating in self: + if rating.customer_id.phone: + rating.phone_nbr = rating.customer_id.phone + elif not rating.phone_nbr: + rating.phone_nbr = False + + def rating_get_partner_id(self): + return self.customer_id diff --git a/addons/test_mail_full/security/ir.model.access.csv b/addons/test_mail_full/security/ir.model.access.csv index 657c8e38782..4181b40d397 100644 --- a/addons/test_mail_full/security/ir.model.access.csv +++ b/addons/test_mail_full/security/ir.model.access.csv @@ -1,4 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mail_test_rating_all,mail.test.rating.all,model_mail_test_rating,,0,0,0,0 +access_mail_test_rating_portal,mail.test.rating.portal,model_mail_test_rating,base.group_portal,1,0,0,0 +access_mail_test_rating_user,mail.test.rating.user,model_mail_test_rating,base.group_user,1,1,1,1 access_mail_test_sms_all,mail.test.sms.all,model_mail_test_sms,,0,0,0,0 access_mail_test_sms_user,mail.test.sms.user,model_mail_test_sms,base.group_user,1,1,1,1 access_mail_test_sms_bl_all,mail.test.sms.bl.all,model_mail_test_sms_bl,,0,0,0,0 diff --git a/addons/test_mail_full/security/ir_rule_data.xml b/addons/test_mail_full/security/ir_rule_data.xml new file mode 100644 index 00000000000..e4d32e58ba8 --- /dev/null +++ b/addons/test_mail_full/security/ir_rule_data.xml @@ -0,0 +1,17 @@ + + + + + TestRating: Multi Company + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + TestRating: Portal should follow + + [('message_partner_ids', 'in', [user.partner_id.id])] + + + + diff --git a/addons/test_mail_full/tests/__init__.py b/addons/test_mail_full/tests/__init__.py index d3948f83d4d..3921c4e1343 100644 --- a/addons/test_mail_full/tests/__init__.py +++ b/addons/test_mail_full/tests/__init__.py @@ -7,6 +7,7 @@ from . import test_phone_blacklist from . import test_mass_mailing from . import test_mass_mailing_statistics from . import test_mass_sms +from . import test_rating from . import test_sms_composer from . import test_sms_management from . import test_sms_performance diff --git a/addons/test_mail_full/tests/test_rating.py b/addons/test_mail_full/tests/test_rating.py new file mode 100644 index 00000000000..a0e867e73e2 --- /dev/null +++ b/addons/test_mail_full/tests/test_rating.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.test_mail_full.tests.common import TestMailFullCommon, TestMailFullRecipients +from odoo.tests import tagged +from odoo.tests.common import users +from odoo.tools import mute_logger + + +@tagged('rating') +class TestRatingFlow(TestMailFullCommon, TestMailFullRecipients): + + @classmethod + def setUpClass(cls): + super(TestRatingFlow, cls).setUpClass() + + cls.record_rating = cls.env['mail.test.rating'].create({ + 'customer_id': cls.partner_1.id, + 'name': 'Test Rating', + 'user_id': cls.user_admin.id, + }) + + def test_initial_values(self): + record_rating = self.env['mail.test.rating'].browse(self.record_rating.ids) + self.assertFalse(record_rating.rating_ids) + self.assertEqual(record_rating.message_partner_ids, self.partner_admin) + self.assertEqual(len(record_rating.message_ids), 1) + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_rating_prepare(self): + record_rating = self.env['mail.test.rating'].browse(self.record_rating.ids) + + # prepare rating token + access_token = record_rating.rating_get_access_token() + record_rating.invalidate_cache(fnames=['rating_ids']) + + # check rating creation + rating = record_rating.rating_ids + self.assertEqual(rating.access_token, access_token) + self.assertFalse(rating.consumed) + self.assertFalse(rating.is_internal) + self.assertEqual(rating.partner_id, self.partner_1) + self.assertEqual(rating.rated_partner_id, self.user_admin.partner_id) + self.assertFalse(rating.rating) + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_rating_rating_apply(self): + record_rating = self.env['mail.test.rating'].browse(self.record_rating.ids) + + # prepare rating token + access_token = record_rating.rating_get_access_token() + record_rating.invalidate_cache(fnames=['rating_ids']) + + # apply a rate + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + record_rating.rating_apply(5, token=access_token, feedback='Top Feedback') + message = record_rating.message_ids[0] + rating = record_rating.rating_ids + + # check posted message + self.assertIn('Top Feedback', message.body) + self.assertIn('rating/static/src/img/rating_5.png', message.body) + self.assertEqual(message.author_id, self.partner_1) + self.assertFalse(message.rating_ids) + self.assertEqual(message.notified_partner_ids, self.partner_admin) + self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) + + # check rating update + self.assertTrue(rating.consumed) + self.assertEqual(rating.feedback, 'Top Feedback') + self.assertFalse(rating.message_id) + self.assertEqual(rating.rating, 5)