diff options
Diffstat (limited to 'addons/test_mail/tests')
20 files changed, 7152 insertions, 0 deletions
diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py new file mode 100644 index 00000000..c2bbe943 --- /dev/null +++ b/addons/test_mail/tests/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from . import test_invite +from . import test_ir_actions +from . import test_mail_activity +from . import test_mail_composer +from . import test_mail_followers +from . import test_mail_message +from . import test_mail_mail +from . import test_mail_channel +from . import test_mail_channel_partner +from . import test_mail_gateway +from . import test_mail_thread_internals +from . import test_mail_template +from . import test_mail_tools +from . import test_message_management +from . import test_message_post +from . import test_message_track +from . import test_performance +from . import test_ui diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py new file mode 100644 index 00000000..c556a988 --- /dev/null +++ b/addons/test_mail/tests/common.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mail.tests.common import MailCommon +from odoo.tests.common import SavepointCase + + +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 + 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)]) + + records = cls.env[model].with_context(**cls._test_context).create([{ + 'name': 'Test_%s' % (x), + 'customer_id': partners[x].id, + } for x in range(count)]) + + cls.records = cls._reset_mail_context(records) + cls.partners = partners + return cls.records, cls.partners + + +class TestMailMultiCompanyCommon(MailCommon): + + @classmethod + def setUpClass(cls): + super(TestMailMultiCompanyCommon, cls).setUpClass() + cls.company_2 = cls.env['res.company'].create({ + 'name': 'Second Test Company', + }) + + +class TestRecipients(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestRecipients, cls).setUpClass() + Partner = cls.env['res.partner'].with_context({ + 'mail_create_nolog': True, + 'mail_create_nosubscribe': True, + 'mail_notrack': True, + 'no_reset_password': True, + }) + cls.partner_1 = Partner.create({ + 'name': 'Valid Lelitre', + 'email': 'valid.lelitre@agrolait.com', + 'country_id': cls.env.ref('base.be').id, + 'mobile': '0456001122', + }) + cls.partner_2 = Partner.create({ + 'name': 'Valid Poilvache', + 'email': 'valid.other@gmail.com', + 'country_id': cls.env.ref('base.be').id, + 'mobile': '+32 456 22 11 00', + }) diff --git a/addons/test_mail/tests/test_invite.py b/addons/test_mail/tests/test_invite.py new file mode 100644 index 00000000..c4e91a86 --- /dev/null +++ b/addons/test_mail/tests/test_invite.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.test_mail.tests.common import TestMailCommon +from odoo.tools import mute_logger + + +class TestInvite(TestMailCommon): + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_invite_email(self): + test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + test_partner = self.env['res.partner'].with_context(self._test_context).create({ + 'name': 'Valid Lelitre', + 'email': 'valid.lelitre@agrolait.com'}) + + mail_invite = self.env['mail.wizard.invite'].with_context({ + 'default_res_model': 'mail.test.simple', + 'default_res_id': test_record.id + }).with_user(self.user_employee).create({ + 'partner_ids': [(4, test_partner.id), (4, self.user_admin.partner_id.id)], + 'send_mail': True}) + with self.mock_mail_gateway(): + mail_invite.add_followers() + + # check added followers and that emails were sent + self.assertEqual(test_record.message_partner_ids, + test_partner | self.user_admin.partner_id) + self.assertEqual(test_record.message_follower_ids.mapped('channel_id'), + self.env['mail.channel']) + self.assertSentEmail(self.partner_employee, [test_partner]) + self.assertSentEmail(self.partner_employee, [self.partner_admin]) + self.assertEqual(len(self._mails), 2) diff --git a/addons/test_mail/tests/test_ir_actions.py b/addons/test_mail/tests/test_ir_actions.py new file mode 100644 index 00000000..d67124b2 --- /dev/null +++ b/addons/test_mail/tests/test_ir_actions.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.base.tests.test_ir_actions import TestServerActionsBase +from odoo.addons.test_mail.tests.common import TestMailCommon + + +class TestServerActionsEmail(TestMailCommon, TestServerActionsBase): + + def test_action_email(self): + email_template = self._create_template('res.partner', {'partner_to': '%s' % self.test_partner.id}) + self.action.write({'state': 'email', 'template_id': email_template.id}) + self.action.with_context(self.context).run() + # check an email is waiting for sending + mail = self.env['mail.mail'].sudo().search([('subject', '=', 'About TestingPartner')]) + self.assertEqual(len(mail), 1) + # check email content + self.assertEqual(mail.body, '<p>Hello TestingPartner</p>') + + def test_action_followers(self): + self.test_partner.message_unsubscribe(self.test_partner.message_partner_ids.ids) + random_partner = self.env['res.partner'].create({'name': 'Thierry Wololo'}) + self.action.write({ + 'state': 'followers', + 'partner_ids': [(4, self.env.ref('base.partner_admin').id), (4, random_partner.id)], + 'channel_ids': [(4, self.env.ref('mail.channel_all_employees').id)] + }) + self.action.with_context(self.context).run() + self.assertEqual(self.test_partner.message_partner_ids, self.env.ref('base.partner_admin') | random_partner) + self.assertEqual(self.test_partner.message_channel_ids, self.env.ref('mail.channel_all_employees')) + + def test_action_next_activity(self): + self.action.write({ + 'state': 'next_activity', + 'activity_user_type': 'specific', + 'activity_type_id': self.env.ref('mail.mail_activity_data_meeting').id, + 'activity_summary': 'TestNew', + }) + before_count = self.env['mail.activity'].search_count([]) + run_res = self.action.with_context(self.context).run() + self.assertFalse(run_res, 'ir_actions_server: create next activity action correctly finished should return False') + self.assertEqual(self.env['mail.activity'].search_count([]), before_count + 1) + self.assertEqual(self.env['mail.activity'].search_count([('summary', '=', 'TestNew')]), 1) diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py new file mode 100644 index 00000000..e6d4b003 --- /dev/null +++ b/addons/test_mail/tests/test_mail_activity.py @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date, datetime, timedelta +from dateutil.relativedelta import relativedelta +from unittest.mock import patch +from unittest.mock import DEFAULT + +import pytz + +from odoo import fields, exceptions, tests +from odoo.addons.test_mail.tests.common import TestMailCommon +from odoo.addons.test_mail.models.test_mail_models import MailTestActivity +from odoo.tools import mute_logger +from odoo.tests.common import Form + + +class TestActivityCommon(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestActivityCommon, cls).setUpClass() + cls.test_record = cls.env['mail.test.activity'].with_context(cls._test_context).create({'name': 'Test'}) + # reset ctx + cls._reset_mail_context(cls.test_record) + + +@tests.tagged('mail_activity') +class TestActivityRights(TestActivityCommon): + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_security_user_access_other(self): + activity = self.test_record.with_user(self.user_employee).activity_schedule( + 'test_mail.mail_act_test_todo', + user_id=self.user_admin.id) + self.assertTrue(activity.can_write) + activity.write({'user_id': self.user_employee.id}) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_security_user_access_own(self): + activity = self.test_record.with_user(self.user_employee).activity_schedule( + 'test_mail.mail_act_test_todo') + self.assertTrue(activity.can_write) + activity.write({'user_id': self.user_admin.id}) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_security_user_noaccess_automated(self): + def _employee_crash(*args, **kwargs): + """ If employee is test employee, consider he has no access on document """ + recordset = args[0] + if recordset.env.uid == self.user_employee.id: + raise exceptions.AccessError('Hop hop hop Ernest, please step back.') + return DEFAULT + + with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash): + activity = self.test_record.activity_schedule( + 'test_mail.mail_act_test_todo', + user_id=self.user_employee.id) + + activity2 = self.test_record.activity_schedule('test_mail.mail_act_test_todo') + activity2.write({'user_id': self.user_employee.id}) + + def test_activity_security_user_noaccess_manual(self): + def _employee_crash(*args, **kwargs): + """ If employee is test employee, consider he has no access on document """ + recordset = args[0] + if recordset.env.uid == self.user_employee.id: + raise exceptions.AccessError('Hop hop hop Ernest, please step back.') + return DEFAULT + + # cannot create activities for people that cannot access record + with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash): + with self.assertRaises(exceptions.UserError): + activity = self.env['mail.activity'].create({ + 'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id, + 'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id, + 'res_id': self.test_record.id, + 'user_id': self.user_employee.id, + }) + + # cannot create activities if no access to the document + with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash): + with self.assertRaises(exceptions.AccessError): + activity = self.test_record.with_user(self.user_employee).activity_schedule( + 'test_mail.mail_act_test_todo', + user_id=self.user_admin.id) + + +@tests.tagged('mail_activity') +class TestActivityFlow(TestActivityCommon): + + def test_activity_flow_employee(self): + with self.with_user('employee'): + test_record = self.env['mail.test.activity'].browse(self.test_record.id) + self.assertEqual(test_record.activity_ids, self.env['mail.activity']) + + # employee record an activity and check the deadline + self.env['mail.activity'].create({ + 'summary': 'Test Activity', + 'date_deadline': date.today() + relativedelta(days=1), + 'activity_type_id': self.env.ref('mail.mail_activity_data_email').id, + 'res_model_id': self.env['ir.model']._get(test_record._name).id, + 'res_id': test_record.id, + }) + self.assertEqual(test_record.activity_summary, 'Test Activity') + self.assertEqual(test_record.activity_state, 'planned') + + test_record.activity_ids.write({'date_deadline': date.today() - relativedelta(days=1)}) + test_record.invalidate_cache() # TDE note: should not have to do it I think + self.assertEqual(test_record.activity_state, 'overdue') + + test_record.activity_ids.write({'date_deadline': date.today()}) + test_record.invalidate_cache() # TDE note: should not have to do it I think + self.assertEqual(test_record.activity_state, 'today') + + # activity is done + test_record.activity_ids.action_feedback(feedback='So much feedback') + self.assertEqual(test_record.activity_ids, self.env['mail.activity']) + self.assertEqual(test_record.message_ids[0].subtype_id, self.env.ref('mail.mt_activities')) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_notify_other_user(self): + self.user_admin.notification_type = 'email' + rec = self.test_record.with_user(self.user_employee) + with self.assertSinglePostNotifications( + [{'partner': self.partner_admin, 'type': 'email'}], + message_info={'content': 'assigned you an activity', 'subtype': 'mail.mt_note', 'message_type': 'user_notification'}): + activity = rec.activity_schedule( + 'test_mail.mail_act_test_todo', + user_id=self.user_admin.id) + self.assertEqual(activity.create_uid, self.user_employee) + self.assertEqual(activity.user_id, self.user_admin) + + def test_activity_notify_same_user(self): + self.user_employee.notification_type = 'email' + rec = self.test_record.with_user(self.user_employee) + with self.assertNoNotifications(): + activity = rec.activity_schedule( + 'test_mail.mail_act_test_todo', + user_id=self.user_employee.id) + self.assertEqual(activity.create_uid, self.user_employee) + self.assertEqual(activity.user_id, self.user_employee) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_dont_notify_no_user_change(self): + self.user_employee.notification_type = 'email' + activity = self.test_record.activity_schedule('test_mail.mail_act_test_todo', user_id=self.user_employee.id) + with self.assertNoNotifications(): + activity.with_user(self.user_admin).write({'user_id': self.user_employee.id}) + self.assertEqual(activity.user_id, self.user_employee) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_summary_sync(self): + """ Test summary from type is copied on activities if set (currently only in form-based onchange) """ + ActivityType = self.env['mail.activity.type'] + email_activity_type = ActivityType.create({ + 'name': 'email', + 'summary': 'Email Summary', + }) + call_activity_type = ActivityType.create({'name': 'call'}) + with Form(self.env['mail.activity'].with_context(default_res_model_id=self.env.ref('base.model_res_partner'))) as ActivityForm: + ActivityForm.res_model_id = self.env.ref('base.model_res_partner') + + ActivityForm.activity_type_id = call_activity_type + # activity summary should be empty + self.assertEqual(ActivityForm.summary, False) + + ActivityForm.activity_type_id = email_activity_type + # activity summary should be replaced with email's default summary + self.assertEqual(ActivityForm.summary, email_activity_type.summary) + + ActivityForm.activity_type_id = call_activity_type + # activity summary remains unchanged from change of activity type as call activity doesn't have default summary + self.assertEqual(ActivityForm.summary, email_activity_type.summary) + + def test_action_feedback_attachment(self): + Partner = self.env['res.partner'] + Activity = self.env['mail.activity'] + Attachment = self.env['ir.attachment'] + Message = self.env['mail.message'] + + partner = self.env['res.partner'].create({ + 'name': 'Tester', + }) + + activity = Activity.create({ + 'summary': 'Test', + 'activity_type_id': 1, + 'res_model_id': self.env.ref('base.model_res_partner').id, + 'res_id': partner.id, + }) + + attachments = Attachment + attachments += Attachment.create({ + 'name': 'test', + 'res_name': 'test', + 'res_model': 'mail.activity', + 'res_id': activity.id, + 'datas': 'test', + }) + attachments += Attachment.create({ + 'name': 'test2', + 'res_name': 'test', + 'res_model': 'mail.activity', + 'res_id': activity.id, + 'datas': 'testtest', + }) + + # Checking if the attachment has been forwarded to the message + # when marking an activity as "Done" + activity.action_feedback() + activity_message = Message.search([], order='id desc', limit=1) + self.assertEqual(set(activity_message.attachment_ids.ids), set(attachments.ids)) + for attachment in attachments: + self.assertEqual(attachment.res_id, activity_message.id) + self.assertEqual(attachment.res_model, activity_message._name) + + +@tests.tagged('mail_activity') +class TestActivityMixin(TestActivityCommon): + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_mixin(self): + self.user_employee.tz = self.user_admin.tz + with self.with_user('employee'): + self.test_record = self.env['mail.test.activity'].browse(self.test_record.id) + self.assertEqual(self.test_record.env.user, self.user_employee) + + now_utc = datetime.now(pytz.UTC) + now_user = now_utc.astimezone(pytz.timezone(self.env.user.tz or 'UTC')) + today_user = now_user.date() + + # Test various scheduling of activities + act1 = self.test_record.activity_schedule( + 'test_mail.mail_act_test_todo', + today_user + relativedelta(days=1), + user_id=self.user_admin.id) + self.assertEqual(act1.automated, True) + + act_type = self.env.ref('test_mail.mail_act_test_todo') + self.assertEqual(self.test_record.activity_summary, act_type.summary) + self.assertEqual(self.test_record.activity_state, 'planned') + self.assertEqual(self.test_record.activity_user_id, self.user_admin) + + act2 = self.test_record.activity_schedule( + 'test_mail.mail_act_test_meeting', + today_user + relativedelta(days=-1)) + self.assertEqual(self.test_record.activity_state, 'overdue') + # `activity_user_id` is defined as `fields.Many2one('res.users', 'Responsible User', related='activity_ids.user_id')` + # it therefore relies on the natural order of `activity_ids`, according to which activity comes first. + # As we just created the activity, its not yet in the right order. + # We force it by invalidating it so it gets fetched from database, in the right order. + self.test_record.invalidate_cache(['activity_ids']) + self.assertEqual(self.test_record.activity_user_id, self.user_employee) + + act3 = self.test_record.activity_schedule( + 'test_mail.mail_act_test_todo', + today_user + relativedelta(days=3), + user_id=self.user_employee.id) + self.assertEqual(self.test_record.activity_state, 'overdue') + # `activity_user_id` is defined as `fields.Many2one('res.users', 'Responsible User', related='activity_ids.user_id')` + # it therefore relies on the natural order of `activity_ids`, according to which activity comes first. + # As we just created the activity, its not yet in the right order. + # We force it by invalidating it so it gets fetched from database, in the right order. + self.test_record.invalidate_cache(['activity_ids']) + self.assertEqual(self.test_record.activity_user_id, self.user_employee) + + self.test_record.invalidate_cache(ids=self.test_record.ids) + self.assertEqual(self.test_record.activity_ids, act1 | act2 | act3) + + # Perform todo activities for admin + self.test_record.activity_feedback( + ['test_mail.mail_act_test_todo'], + user_id=self.user_admin.id, + feedback='Test feedback',) + self.assertEqual(self.test_record.activity_ids, act2 | act3) + + # Reschedule all activities, should update the record state + self.assertEqual(self.test_record.activity_state, 'overdue') + self.test_record.activity_reschedule( + ['test_mail.mail_act_test_meeting', 'test_mail.mail_act_test_todo'], + date_deadline=today_user + relativedelta(days=3) + ) + self.assertEqual(self.test_record.activity_state, 'planned') + + # Perform todo activities for remaining people + self.test_record.activity_feedback( + ['test_mail.mail_act_test_todo'], + feedback='Test feedback') + + # Setting activities as done should delete them and post messages + self.assertEqual(self.test_record.activity_ids, act2) + self.assertEqual(len(self.test_record.message_ids), 2) + self.assertEqual(self.test_record.message_ids.mapped('subtype_id'), self.env.ref('mail.mt_activities')) + + # Perform meeting activities + self.test_record.activity_unlink(['test_mail.mail_act_test_meeting']) + + # Canceling activities should simply remove them + self.assertEqual(self.test_record.activity_ids, self.env['mail.activity']) + self.assertEqual(len(self.test_record.message_ids), 2) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_mixin_archive(self): + rec = self.test_record.with_user(self.user_employee) + new_act = rec.activity_schedule( + 'test_mail.mail_act_test_todo', + user_id=self.user_admin.id) + self.assertEqual(rec.activity_ids, new_act) + rec.toggle_active() + self.assertEqual(rec.active, False) + self.assertEqual(rec.activity_ids, self.env['mail.activity']) + rec.toggle_active() + self.assertEqual(rec.active, True) + self.assertEqual(rec.activity_ids, self.env['mail.activity']) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_activity_mixin_reschedule_user(self): + rec = self.test_record.with_user(self.user_employee) + rec.activity_schedule( + 'test_mail.mail_act_test_todo', + user_id=self.user_admin.id) + self.assertEqual(rec.activity_ids[0].user_id, self.user_admin) + + # reschedule its own should not alter other's activities + rec.activity_reschedule( + ['test_mail.mail_act_test_todo'], + user_id=self.user_employee.id, + new_user_id=self.user_employee.id) + self.assertEqual(rec.activity_ids[0].user_id, self.user_admin) + + rec.activity_reschedule( + ['test_mail.mail_act_test_todo'], + user_id=self.user_admin.id, + new_user_id=self.user_employee.id) + self.assertEqual(rec.activity_ids[0].user_id, self.user_employee) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_my_activity_flow_employee(self): + Activity = self.env['mail.activity'] + date_today = date.today() + activity_1 = Activity.create({ + 'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id, + 'date_deadline': date_today, + 'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id, + 'res_id': self.test_record.id, + 'user_id': self.user_admin.id, + }) + activity_2 = Activity.create({ + 'activity_type_id': self.env.ref('test_mail.mail_act_test_call').id, + 'date_deadline': date_today + relativedelta(days=1), + 'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id, + 'res_id': self.test_record.id, + 'user_id': self.user_employee.id, + }) + + test_record_1 = self.env['mail.test.activity'].with_context(self._test_context).create({'name': 'Test 1'}) + activity_3 = Activity.create({ + 'activity_type_id': self.env.ref('test_mail.mail_act_test_todo').id, + 'date_deadline': date_today, + 'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id, + 'res_id': test_record_1.id, + 'user_id': self.user_employee.id, + }) + with self.with_user('employee'): + record = self.env['mail.test.activity'].search([('my_activity_date_deadline', '=', date_today)]) + self.assertEqual(test_record_1, record) + +class TestReadProgressBar(tests.TransactionCase): + """Test for read_progress_bar""" + + def test_week_grouping(self): + """The labels associated to each record in read_progress_bar should match + the ones from read_group, even in edge cases like en_US locale on sundays + """ + model = self.env['mail.test.activity'].with_context(lang='en_US') + + # Don't mistake fields date and date_deadline: + # * date is just a random value + # * date_deadline defines activity_state + model.create({ + 'date': '2021-05-02', + 'name': "Yesterday, all my troubles seemed so far away", + }).activity_schedule( + 'test_mail.mail_act_test_todo', + summary="Make another test super asap (yesterday)", + date_deadline=fields.Date.context_today(model) - timedelta(days=7), + ) + model.create({ + 'date': '2021-05-09', + 'name': "Things we said today", + }).activity_schedule( + 'test_mail.mail_act_test_todo', + summary="Make another test asap", + date_deadline=fields.Date.context_today(model), + ) + model.create({ + 'date': '2021-05-16', + 'name': "Tomorrow Never Knows", + }).activity_schedule( + 'test_mail.mail_act_test_todo', + summary="Make a test tomorrow", + date_deadline=fields.Date.context_today(model) + timedelta(days=7), + ) + + domain = [('date', "!=", False)] + groupby = "date:week" + progress_bar = { + 'field': 'activity_state', + 'colors': { + "overdue": 'danger', + "today": 'warning', + "planned": 'success', + } + } + + # call read_group to compute group names + groups = model.read_group(domain, fields=['date'], groupby=[groupby]) + progressbars = model.read_progress_bar(domain, group_by=groupby, progress_bar=progress_bar) + self.assertEqual(len(groups), 3) + self.assertEqual(len(progressbars), 3) + + # format the read_progress_bar result to get a dictionary under this + # format: {activity_state: group_name}; the original format + # (after read_progress_bar) is {group_name: {activity_state: count}} + pg_groups = { + next(state for state, count in data.items() if count): group_name + for group_name, data in progressbars.items() + } + + self.assertEqual(groups[0][groupby], pg_groups["overdue"]) + self.assertEqual(groups[1][groupby], pg_groups["today"]) + self.assertEqual(groups[2][groupby], pg_groups["planned"]) + diff --git a/addons/test_mail/tests/test_mail_channel.py b/addons/test_mail/tests/test_mail_channel.py new file mode 100644 index 00000000..e3357027 --- /dev/null +++ b/addons/test_mail/tests/test_mail_channel.py @@ -0,0 +1,586 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged, Form +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.test_mail.tests.common import TestMailCommon +from odoo.exceptions import AccessError, ValidationError, UserError +from odoo.tools import mute_logger, formataddr + + +@tagged('mail_channel') +class TestChannelAccessRights(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestChannelAccessRights, cls).setUpClass() + Channel = cls.env['mail.channel'].with_context(cls._test_context) + + cls.user_public = mail_new_test_user(cls.env, login='bert', groups='base.group_public', name='Bert Tartignole') + cls.user_portal = mail_new_test_user(cls.env, login='chell', groups='base.group_portal', name='Chell Gladys') + + # Pigs: base group for tests + cls.group_pigs = Channel.create({ + 'name': 'Pigs', + 'public': 'groups', + 'group_public_id': cls.env.ref('base.group_user').id}) + # Jobs: public group + cls.group_public = Channel.create({ + 'name': 'Jobs', + 'description': 'NotFalse', + 'public': 'public'}) + # Private: private group + cls.group_private = Channel.create({ + 'name': 'Private', + 'public': 'private'}) + + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') + def test_access_rights_public(self): + # Read public group -> ok + self.group_public.with_user(self.user_public).read() + + # Read Pigs -> ko, restricted to employees + with self.assertRaises(AccessError): + self.group_pigs.with_user(self.user_public).read() + + # Read a private group when being a member: ok + self.group_private.write({'channel_partner_ids': [(4, self.user_public.partner_id.id)]}) + self.group_private.with_user(self.user_public).read() + + # Create group: ko, no access rights + with self.assertRaises(AccessError): + self.env['mail.channel'].with_user(self.user_public).create({'name': 'Test'}) + + # Update group: ko, no access rights + with self.assertRaises(AccessError): + self.group_public.with_user(self.user_public).write({'name': 'Broutouschnouk'}) + + # Unlink group: ko, no access rights + with self.assertRaises(AccessError): + self.group_public.with_user(self.user_public).unlink() + + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models', 'odoo.models.unlink') + def test_access_rights_groups(self): + # Employee read employee-based group: ok + self.group_pigs.with_user(self.user_employee).read() + + # Employee can create a group + self.env['mail.channel'].with_user(self.user_employee).create({'name': 'Test'}) + + # Employee update employee-based group: ok + self.group_pigs.with_user(self.user_employee).write({'name': 'modified'}) + + # Employee unlink employee-based group: ok + self.group_pigs.with_user(self.user_employee).unlink() + + # Employee cannot read a private group + with self.assertRaises(AccessError): + self.group_private.with_user(self.user_employee).read() + + # Employee cannot write on private + with self.assertRaises(AccessError): + self.group_private.with_user(self.user_employee).write({'name': 're-modified'}) + + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') + def test_access_rights_followers_ko(self): + # self.group_private.name has been put in the cache during the setup as sudo + # It must therefore be removed from the cache in other to validate the fact user_portal can't read it. + self.group_private.invalidate_cache(['name']) + with self.assertRaises(AccessError): + self.group_private.with_user(self.user_portal).name + + def test_access_rights_followers_portal(self): + # Do: Chell is added into Pigs members and browse it -> ok for messages, ko for partners (no read permission) + self.group_private.write({'channel_partner_ids': [(4, self.user_portal.partner_id.id)]}) + chell_pigs = self.group_private.with_user(self.user_portal) + trigger_read = chell_pigs.name + for message in chell_pigs.message_ids: + trigger_read = message.subject + + with self.assertRaises(AccessError): + chell_pigs.message_partner_ids + + for partner in self.group_private.message_partner_ids: + if partner.id == self.user_portal.partner_id.id: + # Chell can read her own partner record + continue + with self.assertRaises(AccessError): + trigger_read = partner.with_user(self.user_portal).name + + +@tagged('mail_channel') +class TestChannelFeatures(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestChannelFeatures, cls).setUpClass() + cls.test_channel = cls.env['mail.channel'].with_context(cls._test_context).create({ + 'name': 'Test', + 'description': 'Description', + 'alias_name': 'test', + 'public': 'public', + }) + cls.test_partner = cls.env['res.partner'].with_context(cls._test_context).create({ + 'name': 'Test Partner', + 'email': 'test@example.com', + }) + + def _join_channel(self, channel, partners): + for partner in partners: + channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner.id})]}) + channel.invalidate_cache() + + def _leave_channel(self, channel, partners): + for partner in partners: + channel._action_unfollow(partner) + + def test_channel_listeners(self): + self.assertEqual(self.test_channel.message_channel_ids, self.test_channel) + self.assertEqual(self.test_channel.message_partner_ids, self.env['res.partner']) + self.assertEqual(self.test_channel.channel_partner_ids, self.env['res.partner']) + + self._join_channel(self.test_channel, self.test_partner) + self.assertEqual(self.test_channel.message_channel_ids, self.test_channel) + self.assertEqual(self.test_channel.message_partner_ids, self.env['res.partner']) + self.assertEqual(self.test_channel.channel_partner_ids, self.test_partner) + + self._leave_channel(self.test_channel, self.test_partner) + self.assertEqual(self.test_channel.message_channel_ids, self.test_channel) + self.assertEqual(self.test_channel.message_partner_ids, self.env['res.partner']) + self.assertEqual(self.test_channel.channel_partner_ids, self.env['res.partner']) + + def test_channel_post_nofollow(self): + self.test_channel.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment') + self.assertEqual(self.test_channel.message_channel_ids, self.test_channel) + self.assertEqual(self.test_channel.message_partner_ids, self.env['res.partner']) + + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_channel_mailing_list_recipients(self): + """ Posting a message on a mailing list should send one email to all recipients """ + self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'schlouby.fr') + self.test_channel.write({'email_send': True}) + self.user_employee.write({'notification_type': 'email'}) + + # Subscribe an user without email. We shouldn't try to send email to them. + nomail = self.env['res.users'].create({ + "login": "nomail", + "name": "No Mail", + "email": False, + "notification_type": "email", + }) + self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner | nomail.partner_id) + with self.mock_mail_gateway(): + self.test_channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment') + self.assertSentEmail(self.test_channel.env.user.partner_id, [self.partner_employee, self.test_partner]) + + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_channel_chat_recipients(self): + """ Posting a message on a chat should not send emails """ + self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'schlouby.fr') + self.test_channel.write({'email_send': False}) + self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner) + with self.mock_mail_gateway(): + self.test_channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment') + self.assertNotSentEmail() + self.assertEqual(len(self._mails), 0) + + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_channel_classic_recipients(self): + """ Posting a message on a classic channel should work like classic post """ + self.test_channel.write({'alias_name': False}) + self.test_channel.message_subscribe([self.user_employee.partner_id.id, self.test_partner.id]) + with self.mock_mail_gateway(): + self.test_channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment') + self.assertSentEmail(self.test_channel.env.user.partner_id, [self.test_partner]) + + def test_channel_creation(self): + """A user that create a private channel should be able to read it.""" + channel_form = Form(self.env['mail.channel'].with_user(self.user_employee)) + channel_form.name = 'Test private channel' + channel_form.public = 'private' + channel = channel_form.save() + self.assertEqual(channel.name, 'Test private channel', 'Must be able to read the created channel') + + def test_channel_get(self): + current_user = self.env['res.users'].create({ + "login": "adam", + "name": "Jonas", + }) + current_user = current_user.with_user(current_user) + current_partner = current_user.partner_id + other_partner = self.test_partner + + # `channel_get` should return a new channel the first time a partner is given + initial_channel_info = current_user.env['mail.channel'].channel_get(partners_to=other_partner.ids) + self.assertEqual(set(p['id'] for p in initial_channel_info['members']), {current_partner.id, other_partner.id}) + + # `channel_get` should return the existing channel every time the same partner is given + same_channel_info = current_user.env['mail.channel'].channel_get(partners_to=other_partner.ids) + self.assertEqual(same_channel_info['id'], initial_channel_info['id']) + + # `channel_get` should return the existing channel when the current partner is given together with the other partner + together_channel_info = current_user.env['mail.channel'].channel_get(partners_to=(current_partner + other_partner).ids) + self.assertEqual(together_channel_info['id'], initial_channel_info['id']) + + # `channel_get` should return a new channel the first time just the current partner is given, + # even if a channel containing the current partner together with other partners already exists + solo_channel_info = current_user.env['mail.channel'].channel_get(partners_to=current_partner.ids) + self.assertNotEqual(solo_channel_info['id'], initial_channel_info['id']) + self.assertEqual(set(p['id'] for p in solo_channel_info['members']), {current_partner.id}) + + # `channel_get` should return the existing channel every time the current partner is given + same_solo_channel_info = current_user.env['mail.channel'].channel_get(partners_to=current_partner.ids) + self.assertEqual(same_solo_channel_info['id'], solo_channel_info['id']) + + def test_channel_seen(self): + """ + In case of concurrent channel_seen RPC, ensure the oldest call has no effect. + """ + self.test_channel.write({'channel_type': 'chat'}) + self.test_channel.action_follow() + msg_1 = self._add_messages(self.test_channel, 'Body1', author=self.user_employee.partner_id, + channel_ids=[self.test_channel.id]) + msg_2 = self._add_messages(self.test_channel, 'Body2', author=self.user_employee.partner_id, + channel_ids=[self.test_channel.id]) + ChannelAsUser = self.test_channel.with_user(self.user_employee).browse(self.test_channel.id) + + self.test_channel.channel_seen(msg_2.id) + self.assertEqual( + ChannelAsUser.channel_info()[0]['seen_partners_info'][0]['seen_message_id'], + msg_2.id, + "Last message id should have been updated" + ) + + self.test_channel.channel_seen(msg_1.id) + self.assertEqual( + ChannelAsUser.channel_info()[0]['seen_partners_info'][0]['seen_message_id'], + msg_2.id, + "Last message id should stay the same after mark channel as seen with an older message" + ) + + @mute_logger('odoo.models.unlink') + def test_channel_auto_unsubscribe_archived_or_deleted_users(self): + """Archiving / deleting a user should automatically unsubscribe related partner from private channels""" + test_channel_private = self.env['mail.channel'].with_context(self._test_context).create({ + 'name': 'Winden caves', + 'description': 'Channel to travel through time', + 'public': 'private', + }) + test_channel_group = self.env['mail.channel'].with_context(self._test_context).create({ + 'name': 'Sic Mundus', + 'public': 'groups', + 'group_public_id': self.env.ref('base.group_user').id}) + + test_user = self.env['res.users'].create({ + "login": "adam", + "name": "Jonas", + }) + test_partner = test_user.partner_id + test_chat = self.env['mail.channel'].with_context(self._test_context).create({ + 'name': 'test', + 'channel_type': 'chat', + 'public': 'private', + 'channel_partner_ids': [(4, self.user_employee.partner_id.id), (4, test_partner.id)], + }) + + self._join_channel(self.test_channel, self.user_employee.partner_id | test_partner) + self._join_channel(test_channel_private, self.user_employee.partner_id | test_partner) + self._join_channel(test_channel_group, self.user_employee.partner_id | test_partner) + + # Unsubscribe archived user from the private channels, but not from public channels and not from chat + self.user_employee.active = False + self.assertEqual(test_channel_private.channel_partner_ids, test_partner) + self.assertEqual(test_channel_group.channel_partner_ids, test_partner) + self.assertEqual(self.test_channel.channel_partner_ids, self.user_employee.partner_id | test_partner) + self.assertEqual(test_chat.channel_partner_ids, self.user_employee.partner_id | test_partner) + + # Unsubscribe deleted user from the private channels, but not from public channels and not from chat + test_user.unlink() + self.assertEqual(test_channel_private.channel_partner_ids, self.env['res.partner']) + self.assertEqual(test_channel_group.channel_partner_ids, self.env['res.partner']) + self.assertEqual(self.test_channel.channel_partner_ids, self.user_employee.partner_id | test_partner) + self.assertEqual(test_chat.channel_partner_ids, self.user_employee.partner_id | test_partner) + + def test_channel_unfollow_should_also_unsubscribe_the_partner(self): + self.test_channel.message_subscribe(self.test_partner.ids) + self.test_channel._action_unfollow(self.test_partner) + + self.assertFalse(self.test_channel.message_partner_ids) + + def test_channel_unfollow_should_not_post_message_if_the_partner_has_been_removed(self): + ''' + When a partner leaves a channel, the system will help post a message under + that partner's name in the channel to notify others if `email_sent` is set `False`. + The message should only be posted when the partner is still a member of the channel + before method `_action_unfollow()` is called. + If the partner has been removed earlier, no more messages will be posted + even if `_action_unfollow()` is called again. + ''' + self.test_channel.write({'email_send': False}) + self._join_channel(self.test_channel, self.test_partner) + self.test_channel.message_subscribe(self.partner_employee.ids) + + # a message should be posted to notify others when a partner is about to leave + with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], { + 'message_type': 'notification', + 'subtype': 'mail.mt_comment', + }): + self.test_channel._action_unfollow(self.test_partner) + + # no more messages should be posted if the partner has been removed before. + with self.assertNoNotifications(): + self.test_channel._action_unfollow(self.test_partner) + + def test_multi_company_chat(self): + company_A = self.env['res.company'].create({'name': 'Company A'}) + company_B = self.env['res.company'].create({'name': 'Company B'}) + test_user_1 = self.env['res.users'].create({ + 'login': 'user1', + 'name': 'My First New User', + 'company_ids': [(6, 0, company_A.ids)], + 'company_id': company_A.id + }) + test_user_2 = self.env['res.users'].create({ + 'login': 'user2', + 'name': 'My Second New User', + 'company_ids': [(6, 0, company_B.ids)], + 'company_id': company_B.id + }) + initial_channel_info = self.env['mail.channel'].with_user(test_user_1).with_context(allowed_company_ids=company_A.ids).channel_get(test_user_2.partner_id.ids) + self.assertTrue(initial_channel_info, 'should be able to chat with multi company user') + + def test_multi_company_message_post_notifications(self): + company_1 = self.company_admin + company_2 = self.env['res.company'].create({'name': 'Company 2'}) + + # Company 1 and notification_type == "inbox" + user_1 = self.user_employee + + # Company 1 and notification_type == "email" + user_2 = self.user_admin + user_2.notification_type = 'email' + + user_3 = mail_new_test_user( + self.env, login='user3', email='user3@example.com', groups='base.group_user', + company_id=company_2.id, company_ids=[(6, 0, company_2.ids)], + name='user3', notification_type='inbox') + + user_4 = mail_new_test_user( + self.env, login='user4', email='user4@example.com', groups='base.group_user', + company_id=company_2.id, company_ids=[(6, 0, company_2.ids)], + name='user4', notification_type='email') + + partner_without_user = self.env['res.partner'].create({ + 'name': 'Partner', + 'email': 'partner_test_123@example.com', + }) + mail_channel = self.env['mail.channel'].with_user(user_1).create({ + 'name': 'Channel', + 'channel_partner_ids': [ + (4, user_1.partner_id.id), + (4, user_2.partner_id.id), + (4, user_3.partner_id.id), + (4, user_4.partner_id.id), + (4, partner_without_user.id), + ], + 'email_send': True, + }) + + mail_channel.invalidate_cache() + (user_1 | user_2 | user_3 | user_4).invalidate_cache() + + with self.mock_mail_gateway(): + mail_channel.with_user(user_1).with_company(company_1).message_post( + body='Test body message 1337', + channel_ids=mail_channel.ids, + ) + + self.assertSentEmail(user_1.partner_id, [user_2.partner_id]) + self.assertSentEmail(user_1.partner_id, [user_4.partner_id]) + self.assertEqual(len(self._mails), 3, 'Should have send only 3 emails to user 2, user 4 and the partner') + + self.assertBusNotifications([(self.cr.dbname, 'mail.channel', mail_channel.id)]) + + # Should not create mail notifications for user 1 & 3 + self.assertFalse(self.env['mail.notification'].search([('res_partner_id', '=', user_1.partner_id.id)])) + self.assertFalse(self.env['mail.notification'].search([('res_partner_id', '=', user_3.partner_id.id)])) + + # Should create mail notifications for user 2 & 4 + self.assertTrue(self.env['mail.notification'].search([('res_partner_id', '=', user_2.partner_id.id)])) + self.assertTrue(self.env['mail.notification'].search([('res_partner_id', '=', user_4.partner_id.id)])) + + # Check that we did not send a "channel_seen" notifications + # for the users which receive the notifications by email + notification_seen_user_2 = self.env['bus.bus'].search([('create_uid', '=', user_2.id)]) + self.assertFalse(notification_seen_user_2, 'Should not have sent a notification as user 2') + notification_seen_user_4 = self.env['bus.bus'].search([('create_uid', '=', user_4.id)]) + self.assertFalse(notification_seen_user_4, 'Should not have sent a notification as user 4') + + +@tagged('moderation', 'mail_channel') +class TestChannelModeration(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestChannelModeration, cls).setUpClass() + + cls.channel_1 = cls.env['mail.channel'].create({ + 'name': 'Moderation_1', + 'email_send': True, + 'moderation': True, + 'channel_partner_ids': [(4, cls.partner_employee.id)], + 'moderator_ids': [(4, cls.user_employee.id)], + }) + + # ensure initial data + cls.user_employee_2 = mail_new_test_user( + cls.env, login='employee2', groups='base.group_user', company_id=cls.company_admin.id, + name='Enguerrand Employee2', notification_type='inbox', signature='--\nEnguerrand' + ) + cls.partner_employee_2 = cls.user_employee_2.partner_id + + cls.user_portal = cls._create_portal_user() + + def test_moderator_consistency(self): + # moderators should be channel members + with self.assertRaises(ValidationError): + self.channel_1.write({'moderator_ids': [(4, self.user_admin.id)]}) + + # member -> moderator or + self.channel_1.write({'channel_partner_ids': [(4, self.partner_admin.id)]}) + self.channel_1.write({'moderator_ids': [(4, self.user_admin.id)]}) + + # member -> moderator ko if no email + self.channel_1.write({'moderator_ids': [(3, self.partner_admin.id)]}) + self.user_admin.write({'email': False}) + with self.assertRaises(ValidationError): + self.channel_1.write({'moderator_ids': [(4, self.user_admin.id)]}) + + def test_moderation_consistency(self): + # moderation enabled channels are restricted to mailing lists + with self.assertRaises(ValidationError): + self.channel_1.write({'email_send': False}) + + # moderation enabled channels should always have moderators + with self.assertRaises(ValidationError): + self.channel_1.write({'moderator_ids': [(5, 0)]}) + + def test_moderation_count(self): + self.assertEqual(self.channel_1.moderation_count, 0) + self.channel_1.write({'moderation_ids': [ + (0, 0, {'email': 'test0@example.com', 'status': 'allow'}), + (0, 0, {'email': 'test1@example.com', 'status': 'ban'}) + ]}) + self.assertEqual(self.channel_1.moderation_count, 2) + + @mute_logger('odoo.addons.mail.models.mail_channel', 'odoo.models.unlink') + def test_send_guidelines(self): + self.channel_1.write({'channel_partner_ids': [(4, self.partner_portal.id), (4, self.partner_admin.id)]}) + self.channel_1._update_moderation_email([self.partner_admin.email], 'ban') + with self.mock_mail_gateway(): + self.channel_1.with_user(self.user_employee).send_guidelines() + for mail in self._new_mails: + self.assertEqual(mail.author_id, self.partner_employee) + self.assertEqual(mail.subject, 'Guidelines of channel %s' % self.channel_1.name) + self.assertEqual(mail.state, 'outgoing') + self.assertEqual(mail.email_from, self.user_employee.company_id.catchall_formatted) + self.assertEqual(self._new_mails.mapped('recipient_ids'), self.partner_employee | self.partner_portal) + + def test_send_guidelines_crash(self): + self.channel_1.write({ + 'channel_partner_ids': [(4, self.partner_admin.id)], + 'moderator_ids': [(4, self.user_admin.id), (3, self.user_employee.id)] + }) + with self.assertRaises(UserError): + self.channel_1.with_user(self.user_employee).send_guidelines() + + def test_update_moderation_email(self): + self.channel_1.write({'moderation_ids': [ + (0, 0, {'email': 'test0@example.com', 'status': 'allow'}), + (0, 0, {'email': 'test1@example.com', 'status': 'ban'}) + ]}) + self.channel_1._update_moderation_email(['test0@example.com', 'test3@example.com'], 'ban') + self.assertEqual(len(self.channel_1.moderation_ids), 3) + self.assertTrue(all(status == 'ban' for status in self.channel_1.moderation_ids.mapped('status'))) + + def test_moderation_reset(self): + self.channel_2 = self.env['mail.channel'].create({ + 'name': 'Moderation_1', + 'email_send': True, + 'moderation': True, + 'channel_partner_ids': [(4, self.partner_employee.id)], + 'moderator_ids': [(4, self.user_employee.id)], + }) + + self.msg_c1_1 = self._add_messages(self.channel_1, 'Body11', author=self.partner_admin, moderation_status='accepted') + self.msg_c1_2 = self._add_messages(self.channel_1, 'Body12', author=self.partner_admin, moderation_status='pending_moderation') + self.msg_c2_1 = self._add_messages(self.channel_2, 'Body21', author=self.partner_admin, moderation_status='pending_moderation') + + self.assertEqual(self.env['mail.message'].search_count([ + ('moderation_status', '=', 'pending_moderation'), + ('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id) + ]), 1) + self.channel_1.write({'moderation': False}) + self.assertEqual(self.env['mail.message'].search_count([ + ('moderation_status', '=', 'pending_moderation'), + ('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id) + ]), 0) + self.assertEqual(self.env['mail.message'].search_count([ + ('moderation_status', '=', 'pending_moderation'), + ('model', '=', 'mail.channel'), ('res_id', '=', self.channel_2.id) + ]), 1) + self.channel_2.write({'moderation': False}) + self.assertEqual(self.env['mail.message'].search_count([ + ('moderation_status', '=', 'pending_moderation'), + ('model', '=', 'mail.channel'), ('res_id', '=', self.channel_2.id) + ]), 0) + + @mute_logger('odoo.models.unlink') + def test_message_post(self): + email1 = 'test0@example.com' + email2 = 'test1@example.com' + + self.channel_1._update_moderation_email([email1], 'ban') + self.channel_1._update_moderation_email([email2], 'allow') + + msg_admin = self.channel_1.message_post(message_type='email', subtype_xmlid='mail.mt_comment', author_id=self.partner_admin.id) + msg_moderator = self.channel_1.message_post(message_type='comment', subtype_xmlid='mail.mt_comment', author_id=self.partner_employee.id) + msg_email1 = self.channel_1.message_post(message_type='comment', subtype_xmlid='mail.mt_comment', email_from=formataddr(("MyName", email1))) + msg_email2 = self.channel_1.message_post(message_type='email', subtype_xmlid='mail.mt_comment', email_from=email2) + msg_notif = self.channel_1.message_post() + + messages = self.env['mail.message'].search([('model', '=', 'mail.channel'), ('res_id', '=', self.channel_1.id)]) + pending_messages = messages.filtered(lambda m: m.moderation_status == 'pending_moderation') + accepted_messages = messages.filtered(lambda m: m.moderation_status == 'accepted') + + self.assertFalse(msg_email1) + self.assertEqual(msg_admin, pending_messages) + self.assertEqual(accepted_messages, msg_moderator | msg_email2 | msg_notif) + self.assertFalse(msg_admin.channel_ids) + self.assertEqual(msg_email2.channel_ids, self.channel_1) + + def test_user_is_moderator(self): + self.assertTrue(self.user_employee.is_moderator) + self.assertFalse(self.user_employee_2.is_moderator) + self.channel_1.write({ + 'channel_partner_ids': [(4, self.partner_employee_2.id)], + 'moderator_ids': [(4, self.user_employee_2.id)], + }) + self.assertTrue(self.user_employee_2.is_moderator) + + def test_user_moderation_counter(self): + self._add_messages(self.channel_1, 'B', moderation_status='pending_moderation', author=self.partner_employee_2) + self._add_messages(self.channel_1, 'B', moderation_status='accepted', author=self.partner_employee_2) + self._add_messages(self.channel_1, 'B', moderation_status='accepted', author=self.partner_employee) + self._add_messages(self.channel_1, 'B', moderation_status='pending_moderation', author=self.partner_employee) + self._add_messages(self.channel_1, 'B', moderation_status='accepted', author=self.partner_employee) + + self.assertEqual(self.user_employee.moderation_counter, 2) + self.assertEqual(self.user_employee_2.moderation_counter, 0) + + self.channel_1.write({ + 'channel_partner_ids': [(4, self.partner_employee_2.id)], + 'moderator_ids': [(4, self.user_employee_2.id)] + }) + self.assertEqual(self.user_employee.moderation_counter, 2) + self.assertEqual(self.user_employee_2.moderation_counter, 0) diff --git a/addons/test_mail/tests/test_mail_channel_partner.py b/addons/test_mail/tests/test_mail_channel_partner.py new file mode 100644 index 00000000..93dd7072 --- /dev/null +++ b/addons/test_mail/tests/test_mail_channel_partner.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +from odoo.tests.common import SavepointCase +from odoo.exceptions import AccessError, UserError + + +class TestMailSecurity(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.secret_group = cls.env['res.groups'].create({ + 'name': 'secret group', + }) + cls.user_1 = cls.env['res.users'].create({ + 'name': 'User 1', + 'login': 'user_1', + 'email': '---', + 'groups_id': [(6, 0, [cls.secret_group.id, cls.env.ref('base.group_user').id])], + }) + cls.user_2 = cls.env['res.users'].create({ + 'name': 'User 2', + 'login': 'user_2', + 'email': '---', + 'groups_id': [(6, 0, [cls.secret_group.id, cls.env.ref('base.group_user').id])], + }) + cls.user_3 = cls.env['res.users'].create({ + 'name': 'User 3', + 'login': 'user_3', + 'email': '---', + }) + + cls.private_channel_1 = cls.env['mail.channel'].create({ + 'name': 'Secret channel', + 'public': 'private', + 'channel_type': 'channel', + }) + cls.group_channel_1 = cls.env['mail.channel'].create({ + 'name': 'Group channel', + 'public': 'groups', + 'channel_type': 'channel', + 'group_public_id': cls.secret_group.id, + }) + cls.public_channel_1 = cls.env['mail.channel'].create({ + 'name': 'Public channel of user 1', + 'public': 'public', + 'channel_type': 'channel', + }) + cls.private_channel_1.channel_last_seen_partner_ids.unlink() + cls.group_channel_1.channel_last_seen_partner_ids.unlink() + cls.public_channel_1.channel_last_seen_partner_ids.unlink() + + ########################### + # PRIVATE CHANNEL & BASIC # + ########################### + + def test_channel_acls_01(self): + """Test access on private channel.""" + res = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)]) + self.assertFalse(res) + + # User 1 can join private channel with SUDO + self.private_channel_1.with_user(self.user_1).sudo().action_follow() + res = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)]) + self.assertEqual(res.partner_id, self.user_1.partner_id) + + # User 2 can not join private channel + with self.assertRaises(AccessError): + self.private_channel_1.with_user(self.user_2).action_follow() + + # But user 2 can join public channel + self.public_channel_1.with_user(self.user_2).action_follow() + res = self.env['mail.channel.partner'].search([('channel_id', '=', self.public_channel_1.id)]) + self.assertEqual(res.partner_id, self.user_2.partner_id) + + # User 2 can not create a `mail.channel.partner` to join the private channel + with self.assertRaises(AccessError): + self.env['mail.channel.partner'].with_user(self.user_2).create({ + 'partner_id': self.user_2.partner_id.id, + 'channel_id': self.private_channel_1.id, + }) + + # User 2 can not write on `mail.channel.partner` to join the private channel + channel_partner = self.env['mail.channel.partner'].with_user(self.user_2).search([('partner_id', '=', self.user_2.partner_id.id)])[0] + with self.assertRaises(AccessError): + channel_partner.channel_id = self.private_channel_1.id + with self.assertRaises(AccessError): + channel_partner.write({'channel_id': self.private_channel_1.id}) + + # But with SUDO, User 2 can + channel_partner.sudo().channel_id = self.private_channel_1.id + + # User 2 can not write on the `partner_id` of `mail.channel.partner` + # of an other partner to join a private channel + channel_partner_1 = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id), ('partner_id', '=', self.user_1.partner_id.id)]) + with self.assertRaises(AccessError): + channel_partner_1.with_user(self.user_2).partner_id = self.user_2.partner_id + self.assertEqual(channel_partner_1.partner_id, self.user_1.partner_id) + + # but with SUDO he can... + channel_partner_1.with_user(self.user_2).sudo().partner_id = self.user_2.partner_id + self.assertEqual(channel_partner_1.partner_id, self.user_2.partner_id) + + def test_channel_acls_03(self): + """Test invitation in private channel part 1 (invite using crud methods).""" + self.private_channel_1.with_user(self.user_1).sudo().action_follow() + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)]) + self.assertEqual(len(channel_partners), 1) + + # User 2 is not in the private channel, he can not invite user 3 + with self.assertRaises(AccessError): + self.env['mail.channel.partner'].with_user(self.user_2).create({ + 'partner_id': self.user_3.partner_id.id, + 'channel_id': self.private_channel_1.id, + }) + + # User 1 is in the private channel, he can invite other users + self.env['mail.channel.partner'].with_user(self.user_1).create({ + 'partner_id': self.user_3.partner_id.id, + 'channel_id': self.private_channel_1.id, + }) + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_3.partner_id) + + # But User 3 can not write on the `mail.channel.partner` of other user + channel_partner_1 = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id), ('partner_id', '=', self.user_1.partner_id.id)]) + channel_partner_3 = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id), ('partner_id', '=', self.user_3.partner_id.id)]) + channel_partner_3.with_user(self.user_3).custom_channel_name = 'Test' + with self.assertRaises(AccessError): + channel_partner_1.with_user(self.user_2).custom_channel_name = 'Blabla' + self.assertNotEqual(channel_partner_1.custom_channel_name, 'Blabla') + + def test_channel_acls_04(self): + """Test invitation in private channel part 2 (use `invite` action).""" + self.private_channel_1.with_user(self.user_1).sudo().action_follow() + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id) + + # User 2 is not in the channel, he can not invite user 3 + with self.assertRaises(AccessError): + self.private_channel_1.with_user(self.user_2).channel_invite([self.user_3.partner_id.id]) + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id) + + # User 1 is in the channel, he can not invite user 3 + self.private_channel_1.with_user(self.user_1).channel_invite([self.user_3.partner_id.id]) + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_3.partner_id) + + def test_channel_acls_05(self): + """Test kick/leave channel.""" + self.private_channel_1.with_user(self.user_1).sudo().action_follow() + self.private_channel_1.with_user(self.user_3).sudo().action_follow() + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.private_channel_1.id)]) + self.assertEqual(len(channel_partners), 2) + + # User 2 is not in the channel, he can not kick user 1 + with self.assertRaises(AccessError): + channel_partners.with_user(self.user_2).unlink() + + # User 3 is in the channel, he can kick user 1 + channel_partners.with_user(self.user_3).unlink() + + ################# + # GROUP CHANNEL # + ################# + def test_channel_acls_06(self): + """Test basics on group channel.""" + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)]) + self.assertFalse(channel_partners) + + # user 1 is in the group, he can join the channel + self.group_channel_1.with_user(self.user_1).action_follow() + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id) + + # user 3 is not in the group, he can not join + with self.assertRaises(AccessError): + self.group_channel_1.with_user(self.user_3).action_follow() + + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)]) + with self.assertRaises(AccessError): + channel_partners.with_user(self.user_3).partner_id = self.user_3.partner_id + + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id) + + # user 1 can not invite user 3 because he's not in the group + with self.assertRaises(UserError): + self.group_channel_1.with_user(self.user_1).channel_invite([self.user_3.partner_id.id]) + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id) + + # but user 2 is in the group and can be invited by user 1 + self.group_channel_1.with_user(self.user_1).channel_invite([self.user_2.partner_id.id]) + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.group_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id) + + ################## + # PUBLIC CHANNEL # + ################## + def test_channel_acls_07(self): + """Test basics on public channel.""" + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.public_channel_1.id)]) + self.assertFalse(channel_partners) + + self.public_channel_1.with_user(self.user_1).action_follow() + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.public_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id) + + self.public_channel_1.with_user(self.user_2).action_follow() + channel_partners = self.env['mail.channel.partner'].search([('channel_id', '=', self.public_channel_1.id)]) + self.assertEqual(channel_partners.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id) diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py new file mode 100644 index 00000000..6c60ea3d --- /dev/null +++ b/addons/test_mail/tests/test_mail_composer.py @@ -0,0 +1,842 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 + +from unittest.mock import patch + +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.test_mail.models.test_mail_models import MailTestTicket +from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients +from odoo.tests import tagged +from odoo.tests.common import users, Form +from odoo.tools import mute_logger + +@tagged('mail_composer') +class TestMailComposer(TestMailCommon, TestRecipients): + """ Test Composer internals """ + + @classmethod + def setUpClass(cls): + super(TestMailComposer, cls).setUpClass() + cls._init_mail_gateway() + + # ensure employee can create partners, necessary for templates + cls.user_employee.write({ + 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], + }) + + cls.user_employee_2 = mail_new_test_user( + cls.env, login='employee2', groups='base.group_user', + notification_type='email', email='eglantine@example.com', + name='Eglantine Employee', signature='--\nEglantine') + cls.partner_employee_2 = cls.user_employee_2.partner_id + + cls.test_record = cls.env['mail.test.ticket'].with_context(cls._test_context).create({ + 'name': 'TestRecord', + '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_report = cls.env['ir.actions.report'].create({ + 'name': 'Test Report on mail test ticket', + 'model': 'mail.test.ticket', + 'report_type': 'qweb-pdf', + 'report_name': 'test_mail.mail_test_ticket_test_template', + }) + cls.test_record_report = cls.test_report._render_qweb_pdf(cls.test_report.ids) + + cls.test_from = '"John Doe" <john@example.com>' + + 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({ + 'name': 'TestTemplate', + 'subject': 'TemplateSubject ${object.name}', + 'body_html': '<p>TemplateBody ${object.name}</p>', + 'partner_to': '${object.customer_id.id if object.customer_id else ""}', + 'email_to': '${(object.email_from if not object.customer_id else "") | safe}', + 'email_from': '${(object.user_id.email_formatted or user.email_formatted) | safe}', + 'model_id': cls.env['ir.model']._get('mail.test.ticket').id, + 'mail_server_id': cls.mail_server.id, + 'auto_delete': True, + }) + + def _generate_attachments_data(self, count): + return [{ + 'name': '%02d.txt' % x, + 'datas': base64.b64encode(b'Att%02d' % x), + } for x in range(count)] + + def _get_web_context(self, records, add_web=True, **values): + """ Helper to generate composer context. Will make tests a bit less + verbose. + + :param add_web: add web context, generally making noise especially in + mass mail mode (active_id/ids both present in context) + """ + base_context = { + 'default_model': records._name, + } + if len(records) == 1: + base_context['default_composition_mode'] = 'comment' + base_context['default_res_id'] = records.id + else: + base_context['default_composition_mode'] = 'mass_mail' + base_context['active_ids'] = records.ids + if add_web: + base_context['active_model'] = records._name + base_context['active_id'] = records[0].id + if values: + base_context.update(**values) + return base_context + + +@tagged('mail_composer') +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))) + 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.no_auto_thread) + 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): + """Tests that all attachments are added to the composer, static attachments + are not duplicated and while reports are re-generated, and that intermediary + attachments are dropped.""" + attachment_data = self._generate_attachments_data(2) + template_1 = self.template.copy({ + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'report_name': 'TestReport for ${object.name}.html', # test cursor forces html + 'report_template': self.test_report.id, + }) + template_1_attachments = template_1.attachment_ids + self.assertEqual(len(template_1_attachments), 2) + template_2 = self.template.copy({ + 'attachment_ids': False, + 'report_template': self.test_report.id, + }) + + # begins without attachments + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=True, default_attachment_ids=[]) + )) + self.assertEqual(len(composer_form.attachment_ids), 0) + + # change template: 2 static (attachment_ids) and 1 dynamic (report) + composer_form.template_id = template_1 + self.assertEqual(len(composer_form.attachment_ids), 3) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, template_1_attachments) + + # change template: 0 static (attachment_ids) and 1 dynamic (report) + composer_form.template_id = template_2 + self.assertEqual(len(composer_form.attachment_ids), 1) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, self.env['ir.attachment']) + + # change back to template 1 + composer_form.template_id = template_1 + self.assertEqual(len(composer_form.attachment_ids), 3) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, template_1_attachments) + + # reset template + composer_form.template_id = self.env['mail.template'] + self.assertEqual(len(composer_form.attachment_ids), 0) + + @users('employee') + def test_mail_composer_mass(self): + 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') + # 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.no_auto_thread) + self.assertEqual(composer_form.composition_mode, 'mass_mail') + self.assertEqual(composer_form.model, self.test_records._name) + + @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') + self.assertEqual(composer_form.body, self.template.body_html, + 'MailComposer: mass mode should have template raw body if template') + # 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.no_auto_thread) + self.assertEqual(composer_form.composition_mode, 'mass_mail') + self.assertEqual(composer_form.model, self.test_records._name) + + +@tagged('mail_composer') +class TestComposerInternals(TestMailComposer): + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_composer_attachments_comment(self): + """ Test attachments management in comment mode. """ + attachment_data = self._generate_attachments_data(3) + self.template.write({ + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'report_name': 'TestReport for ${object.name}.html', # test cursor forces html + 'report_template': self.test_report.id, + }) + attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) + self.assertEqual(len(attachs), 3) + + composer = self.env['mail.compose.message'].with_context({ + 'default_composition_mode': 'comment', + 'default_model': self.test_record._name, + 'default_res_id': self.test_record.id, + 'default_template_id': self.template.id, + }).create({ + 'body': '<p>Test Body</p>', + }) + # currently onchange necessary + composer.onchange_template_id_wrapper() + + # values coming from template + self.assertEqual(len(composer.attachment_ids), 4) + for attach in attachs: + self.assertIn(attach, composer.attachment_ids) + generated = composer.attachment_ids - attachs + self.assertEqual(len(generated), 1, 'MailComposer: should have 1 additional attachment for report') + self.assertEqual(generated.name, 'TestReport for %s.html' % self.test_record.name) + self.assertEqual(generated.res_model, 'mail.compose.message') + self.assertEqual(generated.res_id, 0) + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_composer_author(self): + """ Test author_id / email_from synchronization, in both comment and mass mail + modes. """ + for composition_mode in ['comment', 'mass_mail']: + 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) + composer = self.env['mail.compose.message'].with_context(ctx).create({ + 'body': '<p>Test Body</p>', + }) + + # default values are current user + self.assertEqual(composer.author_id, self.env.user.partner_id) + self.assertEqual(composer.email_from, self.env.user.email_formatted) + + # author values reset email (FIXME: currently not synchronized) + composer.write({'author_id': self.partner_1}) + self.assertEqual(composer.author_id, self.partner_1) + self.assertEqual(composer.email_from, self.env.user.email_formatted) + # self.assertEqual(composer.email_from, self.partner_1.email_formatted) + + # changing template should update its email_from + composer.write({'template_id': self.template.id, 'author_id': self.env.user.partner_id}) + # currently onchange necessary + composer.onchange_template_id_wrapper() + self.assertEqual(composer.author_id, self.env.user.partner_id, + 'MailComposer: should take value given by user') + if composition_mode == 'comment': + self.assertEqual(composer.email_from, self.test_record.user_id.email_formatted, + 'MailComposer: should take email_from rendered from template') + else: + self.assertEqual(composer.email_from, self.template.email_from, + 'MailComposer: should take email_from raw from template') + + # manual values are kept over template values + composer.write({'email_from': self.test_from}) + self.assertEqual(composer.author_id, self.env.user.partner_id) + self.assertEqual(composer.email_from, self.test_from) + + @users('employee') + def test_mail_composer_content(self): + """ Test content management (subject, body, server) in both comment and + mass mailing mode. Template update is also tested. """ + for composition_mode in ['comment', 'mass_mail']: + if composition_mode == 'comment': + ctx = self._get_web_context(self.test_record, add_web=False) + 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({ + 'subject': 'My amazing subject', + 'body': '<p>Test Body</p>', + }) + + # creation values are taken + self.assertEqual(composer.subject, 'My amazing subject') + self.assertEqual(composer.body, '<p>Test Body</p>') + self.assertEqual(composer.mail_server_id.id, False) + + # changing template should update its content + composer.write({'template_id': self.template.id}) + # currently onchange necessary + composer.onchange_template_id_wrapper() + + # values come from template + if composition_mode == 'comment': + self.assertEqual(composer.subject, 'TemplateSubject %s' % self.test_record.name) + self.assertEqual(composer.body, '<p>TemplateBody %s</p>' % self.test_record.name) + self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + 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) + + # manual values is kept over template + composer.write({'subject': 'Back to my amazing subject'}) + self.assertEqual(composer.subject, 'Back to my amazing subject') + + # reset template should reset values + composer.write({'template_id': False}) + # currently onchange necessary + composer.onchange_template_id_wrapper() + + # values are reset + if composition_mode == 'comment': + self.assertEqual(composer.subject, 'Re: %s' % self.test_record.name) + self.assertEqual(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) + else: + # values are reset TDE FIXME: strange for subject + self.assertEqual(composer.subject, 'Back to my amazing subject') + self.assertEqual(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) + + # 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.subject, 'TemplateSubject %s' % self.test_record.name) + self.assertEqual(composer.body, '<p>TemplateBody %s</p>' % self.test_record.name) + self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + 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) + + # 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.subject, 'TemplateSubject %s' % self.test_record.name) + self.assertEqual(composer.body, '<p>TemplateBody %s</p>' % self.test_record.name) + self.assertEqual(composer.mail_server_id, self.template.mail_server_id) + 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) + + # 4. template + user input + ctx['default_template_id'] = self.template.id + composer = self.env['mail.compose.message'].with_context(ctx).create({ + 'subject': 'My amazing subject', + 'body': '<p>Test Body</p>', + 'mail_server_id': False, + }) + + # creation values are taken + self.assertEqual(composer.subject, 'My amazing subject') + self.assertEqual(composer.body, '<p>Test Body</p>') + self.assertEqual(composer.mail_server_id.id, False) + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_parent(self): + """ Test specific management in comment mode when having parent_id set: + record_name, subject, parent's partners. """ + parent = self.test_record.message_post(body='Test', partner_ids=(self.partner_1 + self.partner_2).ids) + + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=False, default_parent_id=parent.id) + ).create({ + 'body': '<p>Test Body</p>', + }) + + # creation values taken from parent + self.assertEqual(composer.subject, 'Re: %s' % self.test_record.name) + self.assertEqual(composer.body, '<p>Test Body</p>') + self.assertEqual(composer.partner_ids, self.partner_1 + self.partner_2) + + def test_mail_composer_rights_portal(self): + portal_user = self._create_portal_user() + + with patch.object(MailTestTicket, 'check_access_rights', return_value=True): + self.env['mail.compose.message'].with_user(portal_user).with_context( + self._get_web_context(self.test_record) + ).create({ + 'subject': 'Subject', + 'body': '<p>Body text</p>', + 'partner_ids': [] + }).send_mail() + + self.assertEqual(self.test_record.message_ids[0].body, '<p>Body text</p>') + self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) + + self.env['mail.compose.message'].with_user(portal_user).with_context({ + 'default_composition_mode': 'comment', + 'default_parent_id': self.test_record.message_ids.ids[0], + }).create({ + 'subject': 'Subject', + 'body': '<p>Body text 2</p>' + }).send_mail() + + self.assertEqual(self.test_record.message_ids[0].body, '<p>Body text 2</p>') + self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) + + @users('employee') + def test_mail_composer_save_template(self): + self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=False) + ).create({ + 'subject': 'Template Subject', + 'body': '<p>Template Body</p>', + }).save_as_template() + + # Test: email_template subject, body_html, model + template = self.env['mail.template'].search([ + ('model', '=', self.test_record._name), + ('subject', '=', 'Template Subject') + ], limit=1) + self.assertEqual(template.name, "%s: %s" % (self.env['ir.model']._get(self.test_record._name).name, 'Template Subject')) + self.assertEqual(template.body_html, '<p>Template Body</p>', 'email_template incorrect body_html') + + +@tagged('mail_composer') +class TestComposerResultsComment(TestMailComposer): + """ Test global output of composer used in comment mode. Test notably + notification and emails generated during this process. """ + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_notifications_delete(self): + """ Notifications are correctly deleted once sent """ + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record) + ).create({ + 'body': '<p>Test Body</p>', + 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] + }) + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.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._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') + + # ensure ``mail_auto_delete`` context key allow to override this behavior + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record), + mail_auto_delete=False, + ).create({ + 'body': '<p>Test Body</p>', + 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] + }) + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.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._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.exists()), 2, 'Should not have deleted mail.mail records') + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_recipients(self): + """ Test partner_ids given to composer are given to the final message. """ + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record) + ).create({ + 'body': '<p>Test Body</p>', + 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] + }) + composer.send_mail() + + message = self.test_record.message_ids[0] + self.assertEqual(message.body, '<p>Test Body</p>') + self.assertEqual(message.author_id, self.user_employee.partner_id) + self.assertEqual(message.subject, 'Re: %s' % self.test_record.name) + self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) + self.assertEqual(message.partner_ids, self.partner_1 | self.partner_2) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_complete(self): + """ Test a posting process using a complex template, holding several + additional recipients and attachments. + + This tests notifies: 2 new email_to (+ 1 duplicated), 1 email_cc, + test_record followers and partner_admin added in partner_to.""" + attachment_data = self._generate_attachments_data(2) + email_to_1 = 'test.to.1@test.example.com' + email_to_2 = 'test.to.2@test.example.com' + email_to_3 = 'test.to.1@test.example.com' # duplicate: should not sent twice the email + email_cc_1 = 'test.cc.1@test.example.com' + self.template.write({ + 'auto_delete': False, # keep sent emails to check content + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'email_to': '%s, %s, %s' % (email_to_1, email_to_2, email_to_3), + 'email_cc': email_cc_1, + 'partner_to': '%s, ${object.customer_id.id if object.customer_id else ""}' % self.partner_admin.id, + 'report_name': 'TestReport for ${object.name}', # test cursor forces html + 'report_template': self.test_report.id, + }) + attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) + self.assertEqual(len(attachs), 2) + + # ensure initial data + self.assertEqual(self.test_record.user_id, self.user_employee_2) + self.assertEqual(self.test_record.message_partner_ids, self.partner_employee_2) + + # open a composer and run it in comment mode + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=True, + default_template_id=self.template.id) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + composer.send_mail() + + # check new partners have been created based on emails given + new_partners = self.env['res.partner'].search([ + ('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1]) + ]) + self.assertEqual(len(new_partners), 3) + self.assertEqual(set(new_partners.mapped('email')), + set(['test.to.1@test.example.com', 'test.to.2@test.example.com', 'test.cc.1@test.example.com']) + ) + + # global outgoing: one mail.mail (all customer recipients, then all employee recipients) + # and 5 emails, and 1 inbox notification (admin) + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail') + self.assertEqual(len(self._mails), 5, 'Should have sent 5 emails, one per recipient') + + # template is sent only to partners (email_to are transformed) + message = self.test_record.message_ids[0] + self.assertMailMail(self.partner_employee_2, 'sent', + mail_message=message, + author=self.partner_employee, # author != email_from (template sets only email_from) + email_values={ + 'body_content': 'TemplateBody %s' % self.test_record.name, + 'email_from': self.test_record.user_id.email_formatted, # set by template + 'subject': 'TemplateSubject %s' % self.test_record.name, + 'attachments_info': [ + {'name': '00.txt', 'raw': b'Att00', 'type': 'text/plain'}, + {'name': '01.txt', 'raw': b'Att01', 'type': 'text/plain'}, + {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, + ] + }, + fields_values={}, + ) + self.assertMailMail(self.test_record.customer_id + new_partners, 'sent', + mail_message=message, + author=self.partner_employee, # author != email_from (template sets only email_from) + email_values={ + 'body_content': 'TemplateBody %s' % self.test_record.name, + 'email_from': self.test_record.user_id.email_formatted, # set by template + 'subject': 'TemplateSubject %s' % self.test_record.name, + 'attachments_info': [ + {'name': '00.txt', 'raw': b'Att00', 'type': 'text/plain'}, + {'name': '01.txt', 'raw': b'Att01', 'type': 'text/plain'}, + {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, + ] + }, + fields_values={}, + ) + + # message is posted and notified admin + self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) + self.assertNotified(message, [{'partner': self.partner_admin, 'is_read': False, 'type': 'inbox'}]) + # attachments are copied on message and linked to document + self.assertEqual( + set(message.attachment_ids.mapped('name')), + set(['00.txt', '01.txt', 'TestReport for %s.html' % self.test_record.name]) + ) + self.assertEqual(set(message.attachment_ids.mapped('res_model')), set([self.test_record._name])) + self.assertEqual(set(message.attachment_ids.mapped('res_id')), set(self.test_record.ids)) + self.assertTrue(all(attach not in message.attachment_ids for attach in attachs), 'Should have copied attachments') + + +@tagged('mail_composer') +class TestComposerResultsMass(TestMailComposer): + + @classmethod + def setUpClass(cls): + super(TestComposerResultsMass, cls).setUpClass() + # ensure employee can create partners, necessary for templates + cls.user_employee.write({ + 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], + }) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl(self): + self.template.auto_delete = False # keep sent emails to check content + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + + for record in self.test_records: + # message copy is kept + message = record.message_ids[0] + + # template is sent directly using customer field, meaning we have recipients + self.assertMailMail(record.customer_id, 'sent', + mail_message=message, + author=self.partner_employee) + + # message content + self.assertEqual(message.subject, 'TemplateSubject %s' % record.name) + self.assertEqual(message.body, '<p>TemplateBody %s</p>' % record.name) + self.assertEqual(message.author_id, self.user_employee.partner_id) + # post-related fields are void + self.assertEqual(message.subtype_id, self.env['mail.message.subtype']) + self.assertEqual(message.partner_ids, self.env['res.partner']) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_complete(self): + """ Test a composer in mass mode with a quite complete template, containing + notably email-based recipients and attachments. """ + attachment_data = self._generate_attachments_data(2) + email_to_1 = 'test.to.1@test.example.com' + email_to_2 = 'test.to.2@test.example.com' + email_to_3 = 'test.to.1@test.example.com' # duplicate: should not sent twice the email + email_cc_1 = 'test.cc.1@test.example.com' + self.template.write({ + 'auto_delete': False, # keep sent emails to check content + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'email_to': '%s, %s, %s' % (email_to_1, email_to_2, email_to_3), + 'email_cc': email_cc_1, + 'partner_to': '%s, ${object.customer_id.id if object.customer_id else ""}' % self.partner_admin.id, + 'report_name': 'TestReport for ${object.name}', # test cursor forces html + 'report_template': self.test_report.id, + }) + attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) + self.assertEqual(len(attachs), 2) + + # ensure initial data + self.assertEqual(self.test_records.user_id, self.env['res.users']) + self.assertEqual(self.test_records.message_partner_ids, self.env['res.partner']) + + # launch composer in mass mode + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False): + composer.send_mail() + + new_partners = self.env['res.partner'].search([ + ('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1]) + ]) + self.assertEqual(len(new_partners), 3) + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 10, 'Should have sent 5 emails per record') + + # hack to use assertEmails: filtering on from/to only is not sufficient to distinguish emails + _mails_records = [ + [mail for mail in self._mails if '%s-%s' % (record.id, record._name) in mail['message_id']] + for record in self.test_records + ] + _mails_record2 = [mail for mail in self._mails if '%s-%s' % (self.test_records[1].id, self.test_records._name) in mail['message_id']] + + for record, _mails in zip(self.test_records, _mails_records): + # message copy is kept + message = record.message_ids[0] + + # template is sent only to partners (email_to are transformed) + self._mails = _mails + self.assertMailMail(record.customer_id + new_partners + self.partner_admin, + 'sent', + mail_message=message, + author=self.partner_employee, + email_values={ + 'body_content': 'TemplateBody %s' % record.name, + 'subject': 'TemplateSubject %s' % record.name, + # 'attachments': [ + # ('00.txt', b'Att00', 'text/plain'), + # ('01.txt', b'Att01', 'text/plain'), + # ('report.test_mail.mail_test_ticket_test_template.html', b'My second attachment', 'text/plain') + # ] + } + ) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_delete(self): + self.template.auto_delete = True + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records') + + for record in self.test_records: + # message copy is kept + message = record.message_ids[0] + + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + # message content + self.assertEqual(message.subject, 'TemplateSubject %s' % record.name) + self.assertEqual(message.body, '<p>TemplateBody %s</p>' % record.name) + self.assertEqual(message.author_id, self.user_employee.partner_id) + # post-related fields are void + self.assertEqual(message.subtype_id, self.env['mail.message.subtype']) + self.assertEqual(message.partner_ids, self.env['res.partner']) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_delete_notif(self): + self.template.auto_delete = True + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id, + default_auto_delete_message=True) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records') + + for record in self.test_records: + # message copy is unlinked + self.assertEqual(record.message_ids, self.env['mail.message'], 'Should have deleted mail.message records') + + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_recipients(self): + """ Test various combinations of recipients: active_domain, active_id, + active_ids, ... to ensure fallback behavior are working. """ + # 1: active_domain + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id, + active_ids=[], + default_use_active_domain=True, + default_active_domain=[('id', 'in', self.test_records.ids)]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + + for record in self.test_records: + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + # 2: active_domain not taken into account if use_active_domain is False + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id, + default_use_active_domain=False, + default_active_domain=[('id', 'in', -1)]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + + # 3: fallback on active_id if not active_ids + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_records, add_web=True, + default_template_id=self.template.id, + active_ids=[]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False): + composer.send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 1, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 1, 'Should have sent 1 email per record') + + # 3: void is void + composer_form = Form(self.env['mail.compose.message'].with_context( + default_model='mail.test.ticket', + default_template_id=self.template.id + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False), self.assertRaises(ValueError): + composer.send_mail() diff --git a/addons/test_mail/tests/test_mail_followers.py b/addons/test_mail/tests/test_mail_followers.py new file mode 100644 index 00000000..9246073a --- /dev/null +++ b/addons/test_mail/tests/test_mail_followers.py @@ -0,0 +1,498 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from psycopg2 import IntegrityError + +from odoo.addons.test_mail.tests.common import TestMailCommon +from odoo.tests import tagged +from odoo.tests import users +from odoo.tools.misc import mute_logger + + +@tagged('mail_followers') +class BaseFollowersTest(TestMailCommon): + + @classmethod + def setUpClass(cls): + 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)]}) + + Subtype = cls.env['mail.message.subtype'] + # global + cls.mt_al_def = Subtype.create({'name': 'mt_al_def', 'default': True, 'res_model': False}) + cls.mt_al_nodef = Subtype.create({'name': 'mt_al_nodef', 'default': False, 'res_model': False}) + # mail.test.simple + cls.mt_mg_def = Subtype.create({'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.test.simple'}) + cls.mt_mg_nodef = Subtype.create({'name': 'mt_mg_nodef', 'default': False, 'res_model': 'mail.test.simple'}) + cls.mt_mg_def_int = Subtype.create({'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.test.simple', 'internal': True}) + # mail.test.container + cls.mt_cl_def = Subtype.create({'name': 'mt_cl_def', 'default': True, 'res_model': 'mail.test.container'}) + + cls.default_group_subtypes = Subtype.search([('default', '=', True), '|', ('res_model', '=', 'mail.test.simple'), ('res_model', '=', False)]) + cls.default_group_subtypes_portal = Subtype.search([('internal', '=', False), ('default', '=', True), '|', ('res_model', '=', 'mail.test.simple'), ('res_model', '=', False)]) + + def test_field_message_is_follower(self): + test_record = self.test_record.with_user(self.user_employee) + followed_before = test_record.search([('message_is_follower', '=', True)]) + self.assertFalse(test_record.message_is_follower) + test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id]) + followed_after = test_record.search([('message_is_follower', '=', True)]) + self.assertTrue(test_record.message_is_follower) + self.assertEqual(followed_before | test_record, followed_after) + + def test_field_followers(self): + test_record = self.test_record.with_user(self.user_employee) + test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id, self.user_admin.partner_id.id], channel_ids=[self.channel_listen.id]) + followers = self.env['mail.followers'].search([ + ('res_model', '=', 'mail.test.simple'), + ('res_id', '=', test_record.id)]) + self.assertEqual(followers, test_record.message_follower_ids) + self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id) + self.assertEqual(test_record.message_channel_ids, self.channel_listen) + + def test_followers_subtypes_default(self): + test_record = self.test_record.with_user(self.user_employee) + test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id]) + self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id) + follower = self.env['mail.followers'].search([ + ('res_model', '=', 'mail.test.simple'), + ('res_id', '=', test_record.id), + ('partner_id', '=', self.user_employee.partner_id.id)]) + self.assertEqual(follower, test_record.message_follower_ids) + self.assertEqual(follower.subtype_ids, self.default_group_subtypes) + + def test_followers_subtypes_default_internal(self): + test_record = self.test_record.with_user(self.user_employee) + test_record.message_subscribe(partner_ids=[self.partner_portal.id]) + self.assertEqual(test_record.message_partner_ids, self.partner_portal) + follower = self.env['mail.followers'].search([ + ('res_model', '=', 'mail.test.simple'), + ('res_id', '=', test_record.id), + ('partner_id', '=', self.partner_portal.id)]) + self.assertEqual(follower.subtype_ids, self.default_group_subtypes_portal) + + def test_followers_subtypes_specified(self): + test_record = self.test_record.with_user(self.user_employee) + test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_mg_nodef.id]) + self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id) + follower = self.env['mail.followers'].search([ + ('res_model', '=', 'mail.test.simple'), + ('res_id', '=', test_record.id), + ('partner_id', '=', self.user_employee.partner_id.id)]) + self.assertEqual(follower, test_record.message_follower_ids) + self.assertEqual(follower.subtype_ids, self.mt_mg_nodef) + + def test_followers_multiple_subscription_force(self): + test_record = self.test_record.with_user(self.user_employee) + + test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id]) + self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id) + self.assertEqual(test_record.message_channel_ids, self.env['mail.channel']) + self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef) + + test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id, self.mt_al_nodef.id]) + self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id) + self.assertEqual(test_record.message_channel_ids, self.env['mail.channel']) + self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef) + + def test_followers_multiple_subscription_noforce(self): + """ Calling message_subscribe without subtypes on an existing subscription should not do anything (default < existing) """ + test_record = self.test_record.with_user(self.user_employee) + + test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id], subtype_ids=[self.mt_mg_nodef.id, self.mt_al_nodef.id]) + self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id) + self.assertEqual(test_record.message_channel_ids, self.env['mail.channel']) + self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef) + + # set new subtypes with force=False, meaning no rewriting of the subscription is done -> result should not change + test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id]) + self.assertEqual(test_record.message_partner_ids, self.user_admin.partner_id) + self.assertEqual(test_record.message_channel_ids, self.env['mail.channel']) + self.assertEqual(test_record.message_follower_ids.subtype_ids, self.mt_mg_nodef | self.mt_al_nodef) + + def test_followers_multiple_subscription_update(self): + """ Calling message_subscribe with subtypes on an existing subscription should replace them (new > existing) """ + test_record = self.test_record.with_user(self.user_employee) + test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_mg_def.id, self.mt_cl_def.id]) + self.assertEqual(test_record.message_partner_ids, self.user_employee.partner_id) + follower = self.env['mail.followers'].search([ + ('res_model', '=', 'mail.test.simple'), + ('res_id', '=', test_record.id), + ('partner_id', '=', self.user_employee.partner_id.id)]) + self.assertEqual(follower, test_record.message_follower_ids) + self.assertEqual(follower.subtype_ids, self.mt_mg_def | self.mt_cl_def) + + # remove one subtype `mt_mg_def` and set new subtype `mt_al_def` + test_record.message_subscribe(partner_ids=[self.user_employee.partner_id.id], subtype_ids=[self.mt_cl_def.id, self.mt_al_def.id]) + self.assertEqual(follower.subtype_ids, self.mt_cl_def | self.mt_al_def) + + def test_followers_no_DID(self): + """Test that a follower cannot suffer from dissociative identity disorder. + It cannot be both a partner and a channel. + """ + with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'): + self.env['mail.followers'].create({ + 'res_model': self.test_record._name, + 'res_id': self.test_record.id, + 'partner_id': self.user_employee.partner_id.id, + 'channel_id': self.channel_listen.id, + }) + + def test_followers_default_partner_context(self): + """Test that a follower partner_id is not taken from context + when channel id is also defined. + """ + test_record = self.test_record.with_user(self.user_employee) + test_record.with_context(default_partner_id=1).message_subscribe( + partner_ids=[self.user_employee.partner_id.id, self.user_admin.partner_id.id], + channel_ids=[self.channel_listen.id] + ) + + @users('employee') + def test_followers_inactive(self): + """ Test standard API does not subscribe inactive partners """ + customer = self.env['res.partner'].create({ + 'name': 'Valid Lelitre', + 'email': 'valid.lelitre@agrolait.com', + 'country_id': self.env.ref('base.be').id, + 'mobile': '0456001122', + 'active': False, + }) + document = self.env['mail.test.simple'].browse(self.test_record.id) + self.assertEqual(document.message_partner_ids, self.env['res.partner']) + document.message_subscribe(partner_ids=(self.partner_portal | customer).ids) + self.assertEqual(document.message_partner_ids, self.partner_portal) + self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal) + + # works through low-level API + document._message_subscribe(partner_ids=(self.partner_portal | customer).ids) + self.assertEqual(document.message_partner_ids, self.partner_portal, 'No active test: customer not visible') + self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | customer) + + @users('employee') + def test_followers_private_address(self): + """ Test standard API does not subscribe private addresses """ + private_address = self.env['res.partner'].sudo().create({ + 'name': 'Private Address', + 'type': 'private', + }) + document = self.env['mail.test.simple'].browse(self.test_record.id) + document.message_subscribe(partner_ids=(self.partner_portal | private_address).ids) + self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal) + + # works through low-level API + document._message_subscribe(partner_ids=(self.partner_portal | private_address).ids) + self.assertEqual(document.message_follower_ids.partner_id, self.partner_portal | private_address) + + +@tagged('mail_followers') +class AdvancedFollowersTest(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(AdvancedFollowersTest, cls).setUpClass() + cls._create_portal_user() + + cls.test_track = cls.env['mail.test.track'].with_user(cls.user_employee).create({ + 'name': 'Test', + }) + + Subtype = cls.env['mail.message.subtype'] + + # clean demo data to avoid interferences + Subtype.search([('res_model', 'in', ['mail.test.container', 'mail.test.track'])]).unlink() + + # mail.test.track subtypes (aka: task records) + cls.sub_track_1 = Subtype.create({ + 'name': 'Track (with child relation) 1', 'default': False, + 'res_model': 'mail.test.track' + }) + cls.sub_track_2 = Subtype.create({ + 'name': 'Track (with child relation) 2', 'default': False, + 'res_model': 'mail.test.track' + }) + cls.sub_track_nodef = Subtype.create({ + 'name': 'Generic Track subtype', 'default': False, 'internal': False, + 'res_model': 'mail.test.track' + }) + cls.sub_track_def = Subtype.create({ + 'name': 'Default track subtype', 'default': True, 'internal': False, + 'res_model': 'mail.test.track' + }) + + # mail.test.container subtypes (aka: project records) + cls.umb_nodef = Subtype.create({ + 'name': 'Container NoDefault', 'default': False, + 'res_model': 'mail.test.container' + }) + cls.umb_def = Subtype.create({ + 'name': 'Container Default', 'default': True, + 'res_model': 'mail.test.container' + }) + cls.umb_def_int = Subtype.create({ + 'name': 'Container Default', 'default': True, 'internal': True, + 'res_model': 'mail.test.container' + }) + # -> subtypes for auto subscription from container to sub records + cls.umb_autosub_def = Subtype.create({ + 'name': 'Container AutoSub (default)', 'default': True, 'res_model': 'mail.test.container', + 'parent_id': cls.sub_track_1.id, 'relation_field': 'container_id' + }) + cls.umb_autosub_nodef = Subtype.create({ + 'name': 'Container AutoSub 2', 'default': False, 'res_model': 'mail.test.container', + 'parent_id': cls.sub_track_2.id, 'relation_field': 'container_id' + }) + + # generic subtypes + cls.sub_comment = cls.env.ref('mail.mt_comment') + cls.sub_generic_int_nodef = Subtype.create({ + 'name': 'Generic internal subtype', + 'default': False, + 'internal': True, + }) + cls.sub_generic_int_def = Subtype.create({ + 'name': 'Generic internal subtype (default)', + 'default': True, + 'internal': True, + }) + + def test_auto_subscribe_create(self): + """ Creator of records are automatically added as followers """ + self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id) + + def test_auto_subscribe_inactive(self): + """ Test inactive are not added as followers in automated subscription """ + self.test_track.user_id = False + self.user_admin.active = False + self.user_admin.flush() + self.partner_admin.active = False + self.partner_admin.flush() + + self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='comment') + self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id) + self.assertEqual(self.test_track.message_follower_ids.partner_id, self.user_employee.partner_id) + + self.test_track.write({'user_id': self.user_admin.id}) + self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id) + self.assertEqual(self.test_track.message_follower_ids.partner_id, self.user_employee.partner_id) + + def test_auto_subscribe_post(self): + """ People posting a message are automatically added as followers """ + self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='comment') + self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id) + + def test_auto_subscribe_post_email(self): + """ People posting an email are automatically added as followers """ + self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='email') + self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id | self.user_admin.partner_id) + + def test_auto_subscribe_not_on_notification(self): + """ People posting an automatic notification are not subscribed """ + self.test_track.with_user(self.user_admin).message_post(body='Coucou hibou', message_type='notification') + self.assertEqual(self.test_track.message_partner_ids, self.user_employee.partner_id) + + def test_auto_subscribe_responsible(self): + """ Responsibles are tracked and added as followers """ + sub = self.env['mail.test.track'].with_user(self.user_employee).create({ + 'name': 'Test', + 'user_id': self.user_admin.id, + }) + self.assertEqual(sub.message_partner_ids, (self.user_employee.partner_id | self.user_admin.partner_id)) + + def test_auto_subscribe_defaults(self): + """ Test auto subscription based on an container record. This mimics + the behavior of addons like project and task where subscribing to + some project's subtypes automatically subscribe the follower to its tasks. + + Functional rules applied here + + * subscribing to an container subtype with parent_id / relation_field set + automatically create subscription with matching subtypes + * subscribing to a sub-record as creator applies default subtype values + * portal user should not have access to internal subtypes + + Inactive partners should not be auto subscribed. + """ + container = self.env['mail.test.container'].with_context(self._test_context).create({ + 'name': 'Project-Like', + }) + + # have an inactive partner to check auto subscribe does not subscribe it + user_root = self.env.ref('base.user_root') + self.assertFalse(user_root.active) + self.assertFalse(user_root.partner_id.active) + + container.message_subscribe(partner_ids=(self.partner_portal | user_root.partner_id).ids) + container.message_subscribe(partner_ids=self.partner_admin.ids, subtype_ids=(self.sub_comment | self.umb_autosub_nodef | self.sub_generic_int_nodef).ids) + self.assertEqual(container.message_partner_ids, self.partner_portal | self.partner_admin) + follower_por = container.message_follower_ids.filtered(lambda f: f.partner_id == self.partner_portal) + follower_adm = container.message_follower_ids.filtered(lambda f: f.partner_id == self.partner_admin) + self.assertEqual( + follower_por.subtype_ids, + self.sub_comment | self.umb_def | self.umb_autosub_def, + 'Subscribe: Default subtypes: comment (default generic) and two model-related defaults') + self.assertEqual( + follower_adm.subtype_ids, + self.sub_comment | self.umb_autosub_nodef | self.sub_generic_int_nodef, + 'Subscribe: Asked subtypes when subscribing') + + sub1 = self.env['mail.test.track'].with_user(self.user_employee).create({ + 'name': 'Task-Like Test', + 'container_id': container.id, + }) + + self.assertEqual( + sub1.message_partner_ids, self.partner_portal | self.partner_admin | self.user_employee.partner_id, + 'Followers: creator (employee) + auto subscribe from parent (portal)') + follower_por = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal) + follower_adm = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_admin) + follower_emp = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.user_employee.partner_id) + self.assertEqual( + follower_por.subtype_ids, self.sub_comment | self.sub_track_1, + 'AutoSubscribe: comment (generic checked), Track (with child relation) 1 as Umbrella AutoSub (default) was checked' + ) + self.assertEqual( + follower_adm.subtype_ids, self.sub_comment | self.sub_track_2 | self.sub_generic_int_nodef, + 'AutoSubscribe: comment (generic checked), Track (with child relation) 2) as Umbrella AutoSub 2 was checked, Generic internal subtype (generic checked)' + ) + self.assertEqual( + follower_emp.subtype_ids, self.sub_comment | self.sub_track_def | self.sub_generic_int_def, + 'AutoSubscribe: only default one as no subscription on parent' + ) + + # check portal generic subscribe + sub1.message_unsubscribe(partner_ids=self.partner_portal.ids) + sub1.message_subscribe(partner_ids=self.partner_portal.ids) + follower_por = sub1.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal) + + self.assertEqual( + follower_por.subtype_ids, self.sub_comment | self.sub_track_def, + 'AutoSubscribe: only default one as no subscription on parent (no internal as portal)' + ) + + # check auto subscribe as creator + auto subscribe as parent follower takes both subtypes + container.message_subscribe( + partner_ids=self.user_employee.partner_id.ids, + subtype_ids=(self.sub_comment | self.sub_generic_int_nodef | self.umb_autosub_nodef).ids) + sub2 = self.env['mail.test.track'].with_user(self.user_employee).create({ + 'name': 'Task-Like Test', + 'container_id': container.id, + }) + follower_emp = sub2.message_follower_ids.filtered(lambda fol: fol.partner_id == self.user_employee.partner_id) + defaults = self.sub_comment | self.sub_track_def | self.sub_generic_int_def + parents = self.sub_generic_int_nodef | self.sub_track_2 + self.assertEqual( + follower_emp.subtype_ids, defaults + parents, + 'AutoSubscribe: at create auto subscribe as creator + from parent take both subtypes' + ) + + +class AdvancedResponsibleNotifiedTest(TestMailCommon): + def setUp(self): + super(AdvancedResponsibleNotifiedTest, self).setUp() + + # patch registry to simulate a ready environment so that _message_auto_subscribe_notify + # will be executed with the associated notification + old = self.env.registry.ready + self.env.registry.ready = True + self.addCleanup(setattr, self.env.registry, 'ready', old) + + def test_auto_subscribe_notify_email(self): + """ Responsible is notified when assigned """ + partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"}) + notified_user = self.env['res.users'].create({ + 'login': 'demo1', + 'partner_id': partner.id, + 'notification_type': 'email', + }) + + # TODO master: add a 'state' selection field on 'mail.test.track' with a 'done' value to have a complete test + # check that 'default_state' context does not collide with mail.mail default values + sub = self.env['mail.test.track'].with_user(self.user_employee).with_context({ + 'default_state': 'done', + 'mail_notify_force_send': False + }).create({ + 'name': 'Test', + 'user_id': notified_user.id, + }) + + self.assertEqual(sub.message_partner_ids, (self.user_employee.partner_id | notified_user.partner_id)) + # fetch created "You have been assigned to 'Test'" mail.message + mail_message = self.env['mail.message'].search([ + ('model', '=', 'mail.test.track'), + ('res_id', '=', sub.id), + ('partner_ids', 'in', partner.id), + ]) + self.assertEqual(1, len(mail_message)) + + # verify that a mail.mail is attached to it with the correct state ('outgoing') + mail_notification = mail_message.notification_ids + self.assertEqual(1, len(mail_notification)) + self.assertTrue(bool(mail_notification.mail_id)) + self.assertEqual('outgoing', mail_notification.mail_id.state) + + +@tagged('post_install', '-at_install') +class DuplicateNotificationTest(TestMailCommon): + def test_no_duplicate_notification(self): + """ + Check that we only create one mail.notification per partner + + Post install because we need the registery to be ready to send notification + """ + #Simulate case of 2 users that got their partner merged + common_partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"}) + user_1 = self.env['res.users'].create({'login': 'demo1', 'partner_id': common_partner.id, 'notification_type': 'email'}) + user_2 = self.env['res.users'].create({'login': 'demo2', 'partner_id': common_partner.id, 'notification_type': 'inbox'}) + + #Trigger auto subscribe notification + test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": user_2.id}) + mail_message = self.env['mail.message'].search([ + ('res_id', '=', test.id), + ('model', '=', 'mail.test.track'), + ('message_type', '=', 'user_notification') + ]) + notif = self.env['mail.notification'].search([ + ('mail_message_id', '=', mail_message.id), + ('res_partner_id', '=', common_partner.id) + ]) + self.assertEqual(len(notif), 1) + self.assertEqual(notif.notification_type, 'email') + + subtype = self.env.ref('mail.mt_comment') + res = self.env['mail.followers']._get_recipient_data(test, 'comment', subtype.id, pids=common_partner.ids) + partner_notif = [r for r in res if r[0] == common_partner.id] + self.assertEqual(len(partner_notif), 1) + self.assertEqual(partner_notif[0][5], 'email') + +@tagged('post_install', '-at_install') +class UnlinkedNotificationTest(TestMailCommon): + def test_unlinked_notification(self): + """ + Check that we unlink the created user_notification after unlinked the related document + + Post install because we need the registery to be ready to send notification + """ + common_partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"}) + user_1 = self.env['res.users'].create({'login': 'demo1', 'partner_id': common_partner.id, 'notification_type': 'inbox'}) + + test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": user_1.id}) + test_id = test.id + mail_message = self.env['mail.message'].search([ + ('res_id', '=', test_id), + ('model', '=', 'mail.test.track'), + ('message_type', '=', 'user_notification') + ]) + self.assertEqual(len(mail_message), 1) + test.unlink() + mail_message = self.env['mail.message'].search([ + ('res_id', '=', test_id), + ('model', '=', 'mail.test.track'), + ('message_type', '=', 'user_notification') + ]) + self.assertEqual(len(mail_message), 0) diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py new file mode 100644 index 00000000..ea0ef7c6 --- /dev/null +++ b/addons/test_mail/tests/test_mail_gateway.py @@ -0,0 +1,1273 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import socket + +from unittest.mock import DEFAULT +from unittest.mock import patch + +from odoo import exceptions +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.test_mail.data import test_mail_data +from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE +from odoo.addons.test_mail.models.test_mail_models import MailTestGateway +from odoo.addons.test_mail.tests.common import TestMailCommon +from odoo.tests import tagged +from odoo.tests.common import users +from odoo.tools import email_split_and_format, formataddr, mute_logger + + +@tagged('mail_gateway') +class TestEmailParsing(TestMailCommon): + + def test_message_parse_body(self): + # test pure plaintext + plaintext = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>') + res = self.env['mail.thread'].message_parse(self.from_string(plaintext)) + self.assertIn('Please call me as soon as possible this afternoon!', res['body']) + + # test multipart / text and html -> html has priority + multipart = self.format(MAIL_TEMPLATE, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>') + res = self.env['mail.thread'].message_parse(self.from_string(multipart)) + self.assertIn('<p>Please call me as soon as possible this afternoon!</p>', res['body']) + + # test multipart / mixed + res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_MULTIPART_MIXED)) + self.assertNotIn( + 'Should create a multipart/mixed: from gmail, *bold*, with attachment', res['body'], + 'message_parse: text version should not be in body after parsing multipart/mixed') + self.assertIn( + '<div dir="ltr">Should create a multipart/mixed: from gmail, <b>bold</b>, with attachment.<br clear="all"><div><br></div>', res['body'], + 'message_parse: html version should be in body after parsing multipart/mixed') + + res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_MULTIPART_MIXED_TWO)) + self.assertNotIn('First and second part', res['body'], + 'message_parse: text version should not be in body after parsing multipart/mixed') + self.assertIn('First part', res['body'], + 'message_parse: first part of the html version should be in body after parsing multipart/mixed') + self.assertIn('Second part', res['body'], + 'message_parse: second part of the html version should be in body after parsing multipart/mixed') + + res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_SINGLE_BINARY)) + self.assertEqual(res['body'], '') + self.assertEqual(res['attachments'][0][0], 'thetruth.pdf') + + res = self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_MULTIPART_WEIRD_FILENAME)) + self.assertEqual(res['attachments'][0][0], '62_@;,][)=.(ÇÀÉ.txt') + + def test_message_parse_eml(self): + # Test that the parsing of mail with embedded emails as eml(msg) which generates empty attachments, can be processed. + mail = self.format(test_mail_data.MAIL_EML_ATTACHMENT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>', to='generic@test.com') + self.env['mail.thread'].message_parse(self.from_string(mail)) + + def test_message_parse_eml_bounce_headers(self): + # Test Text/RFC822-Headers MIME content-type + msg_id = '<861878175823148.1577183525.736005783081055-openerp-19177-account.invoice@mycompany.example.com>' + mail = self.format( + test_mail_data.MAIL_EML_ATTACHMENT_BOUNCE_HEADERS, + email_from='MAILER-DAEMON@example.com (Mail Delivery System)', + to='test_bounce+82240-account.invoice-19177@mycompany.example.com', + # msg_id goes to the attachment's Message-Id header + msg_id=msg_id, + ) + res = self.env['mail.thread'].message_parse(self.from_string(mail)) + + self.assertEqual(res['bounced_msg_id'], [msg_id], "Message-Id is not extracted from Text/RFC822-Headers attachment") + + def test_message_parse_plaintext(self): + """ Incoming email in plaintext should be stored as html """ + mail = self.format(test_mail_data.MAIL_TEMPLATE_PLAINTEXT, email_from='"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>', to='generic@test.com') + res = self.env['mail.thread'].message_parse(self.from_string(mail)) + self.assertIn('<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>', res['body']) + + def test_message_parse_xhtml(self): + # Test that the parsing of XHTML mails does not fail + self.env['mail.thread'].message_parse(self.from_string(test_mail_data.MAIL_XHTML)) + + +@tagged('mail_gateway') +class TestMailAlias(TestMailCommon): + + @users('employee') + def test_alias_creation(self): + record = self.env['mail.test.container'].create({ + 'name': 'Test Record', + 'alias_name': 'alias.test', + 'alias_contact': 'followers', + }) + self.assertEqual(record.alias_id.alias_model_id, self.env['ir.model']._get('mail.test.container')) + self.assertEqual(record.alias_id.alias_force_thread_id, record.id) + self.assertEqual(record.alias_id.alias_parent_model_id, self.env['ir.model']._get('mail.test.container')) + self.assertEqual(record.alias_id.alias_parent_thread_id, record.id) + self.assertEqual(record.alias_id.alias_name, 'alias.test') + self.assertEqual(record.alias_id.alias_contact, 'followers') + + record.write({ + 'alias_name': 'better.alias.test', + 'alias_defaults': "{'default_name': 'defaults'}" + }) + self.assertEqual(record.alias_id.alias_name, 'better.alias.test') + self.assertEqual(record.alias_id.alias_defaults, "{'default_name': 'defaults'}") + + with self.assertRaises(exceptions.AccessError): + record.write({ + 'alias_force_thread_id': 0, + }) + + with self.assertRaises(exceptions.AccessError): + record.write({ + 'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id, + }) + + with self.assertRaises(exceptions.ValidationError): + record.write({'alias_defaults': "{'custom_field': brokendict"}) + + def test_alias_setup(self): + alias = self.env['mail.alias'].create({ + 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, + 'alias_name': 'b4r+_#_R3wl$$', + }) + self.assertEqual(alias.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens') + + with self.assertRaises(exceptions.ValidationError): + alias.write({'alias_defaults': "{'custom_field': brokendict"}) + + def test_alias_name_unique(self): + alias_model_id = self.env['ir.model']._get('mail.test.gateway').id + catchall_alias = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.alias') + bounce_alias = self.env['ir.config_parameter'].sudo().get_param('mail.bounce.alias') + + # test you cannot create aliases matching bounce / catchall + with self.assertRaises(exceptions.UserError), self.cr.savepoint(): + self.env['mail.alias'].create({'alias_model_id': alias_model_id, 'alias_name': catchall_alias}) + with self.assertRaises(exceptions.UserError), self.cr.savepoint(): + self.env['mail.alias'].create({'alias_model_id': alias_model_id, 'alias_name': bounce_alias}) + + new_mail_alias = self.env['mail.alias'].create({ + 'alias_model_id': alias_model_id, + 'alias_name': 'unused.test.alias' + }) + + # test that re-using catchall and bounce alias raises UserError + with self.assertRaises(exceptions.UserError), self.cr.savepoint(): + new_mail_alias.write({ + 'alias_name': catchall_alias + }) + with self.assertRaises(exceptions.UserError), self.cr.savepoint(): + new_mail_alias.write({ + 'alias_name': bounce_alias + }) + + new_mail_alias.write({'alias_name': 'another.unused.test.alias'}) + + # test that duplicating an alias should have blank name + copy_new_mail_alias = new_mail_alias.copy() + self.assertFalse(copy_new_mail_alias.alias_name) + + # cannot set catchall / bounce to used alias + with self.assertRaises(exceptions.UserError), self.cr.savepoint(): + self.env['ir.config_parameter'].sudo().set_param('mail.catchall.alias', new_mail_alias.alias_name) + with self.assertRaises(exceptions.UserError), self.cr.savepoint(): + self.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', new_mail_alias.alias_name) + + def test_alias_mixin_copy(self): + user_demo = self.env.ref('base.user_demo') + self.assertFalse(user_demo.has_group('base.group_system'), 'Demo user is not supposed to have Administrator access') + self._test_alias_mixin_copy(user_demo, 'alias.test1', False) + self._test_alias_mixin_copy(user_demo, 'alias.test2', '<p>What Is Dead May Never Die</p>') + + def _test_alias_mixin_copy(self, user, alias_name, alias_bounced_content): + record = self.env['mail.test.container'].with_user(user).with_context(lang='en_US').create({ + 'name': 'Test Record', + 'alias_name': alias_name, + 'alias_contact': 'followers', + 'alias_bounced_content': alias_bounced_content, + }) + self.assertEqual(record.alias_bounced_content, alias_bounced_content) + record_copy = record.copy() + self.assertEqual(record_copy.alias_bounced_content, alias_bounced_content) + + +@tagged('mail_gateway') +class TestMailgateway(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestMailgateway, cls).setUpClass() + cls.test_model = cls.env['ir.model']._get('mail.test.gateway') + cls.email_from = '"Sylvie Lelitre" <test.sylvie.lelitre@agrolait.com>' + + cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({ + 'name': 'Test', + 'email_from': 'ignasse@example.com', + }).with_context({}) + + cls.partner_1 = cls.env['res.partner'].with_context(cls._test_context).create({ + 'name': 'Valid Lelitre', + 'email': 'valid.lelitre@agrolait.com', + }) + # groups@.. will cause the creation of new mail.test.gateway + cls.alias = cls.env['mail.alias'].create({ + 'alias_name': 'groups', + 'alias_user_id': False, + 'alias_model_id': cls.test_model.id, + 'alias_contact': 'everyone'}) + + # Set a first message on public group to test update and hierarchy + cls.fake_email = cls.env['mail.message'].create({ + 'model': 'mail.test.gateway', + 'res_id': cls.test_record.id, + 'subject': 'Public Discussion', + 'message_type': 'email', + 'subtype_id': cls.env.ref('mail.mt_comment').id, + 'author_id': cls.partner_1.id, + 'message_id': '<123456-openerp-%s-mail.test.gateway@%s>' % (cls.test_record.id, socket.gethostname()), + }) + + cls._init_mail_gateway() + + # -------------------------------------------------- + # Base low-level tests + # -------------------------------------------------- + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_alias_basic(self): + """ Test details of created message going through mailgateway """ + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Specific') + + # Test: one group created by mailgateway administrator as user_id is not set + self.assertEqual(len(record), 1, 'message_process: a new mail.test should have been created') + res = record.get_metadata()[0].get('create_uid') or [None] + self.assertEqual(res[0], self.env.uid) + + # Test: one message that is the incoming email + self.assertEqual(len(record.message_ids), 1) + msg = record.message_ids[0] + self.assertEqual(msg.subject, 'Specific') + self.assertIn('Please call me as soon as possible this afternoon!', msg.body) + self.assertEqual(msg.message_type, 'email') + self.assertEqual(msg.subtype_id, self.env.ref('mail.mt_comment')) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_cid(self): + record = self.format_and_process(test_mail_data.MAIL_MULTIPART_IMAGE, self.email_from, 'groups@test.com') + message = record.message_ids[0] + for attachment in message.attachment_ids: + self.assertIn('/web/image/%s' % attachment.id, message.body) + self.assertEqual( + set(message.attachment_ids.mapped('name')), + set(['rosaçée.gif', 'verte!µ.gif', 'orangée.gif'])) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_followers(self): + """ Incoming email: recognized author not archived and not odoobot: added as follower """ + with self.mock_mail_gateway(): + record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com') + + self.assertEqual(record.message_ids[0].author_id, self.partner_1, + 'message_process: recognized email -> author_id') + self.assertEqual(record.message_ids[0].email_from, self.partner_1.email_formatted) + self.assertEqual(record.message_follower_ids.partner_id, self.partner_1, + 'message_process: recognized email -> added as follower') + self.assertEqual(record.message_partner_ids, self.partner_1, + 'message_process: recognized email -> added as follower') + + # just an email -> no follower + with self.mock_mail_gateway(): + record2 = self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'groups@test.com', + subject='Another Email') + + self.assertEqual(record2.message_ids[0].author_id, self.env['res.partner']) + self.assertEqual(record2.message_ids[0].email_from, self.email_from) + self.assertEqual(record2.message_follower_ids.partner_id, self.env['res.partner'], + 'message_process: unrecognized email -> no follower') + self.assertEqual(record2.message_partner_ids, self.env['res.partner'], + 'message_process: unrecognized email -> no follower') + + # archived partner -> no follower + self.partner_1.active = False + self.partner_1.flush() + with self.mock_mail_gateway(): + record3 = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com', + subject='Yet Another Email') + + self.assertEqual(record3.message_ids[0].author_id, self.env['res.partner']) + self.assertEqual(record3.message_ids[0].email_from, self.partner_1.email_formatted) + self.assertEqual(record3.message_follower_ids.partner_id, self.env['res.partner'], + 'message_process: unrecognized email -> no follower') + self.assertEqual(record3.message_partner_ids, self.env['res.partner'], + 'message_process: unrecognized email -> no follower') + + + # partner_root -> never again + odoobot = self.env.ref('base.partner_root') + odoobot.active = True + odoobot.email = 'odoobot@example.com' + with self.mock_mail_gateway(): + record4 = self.format_and_process( + MAIL_TEMPLATE, odoobot.email_formatted, 'groups@test.com', + subject='Odoobot Automatic Answer') + + self.assertEqual(record4.message_ids[0].author_id, odoobot) + self.assertEqual(record4.message_ids[0].email_from, odoobot.email_formatted) + self.assertEqual(record4.message_follower_ids.partner_id, self.env['res.partner'], + 'message_process: unrecognized email -> no follower') + self.assertEqual(record4.message_partner_ids, self.env['res.partner'], + 'message_process: unrecognized email -> no follower') + + # -------------------------------------------------- + # Author recognition + # -------------------------------------------------- + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_email_email_from(self): + """ Incoming email: not recognized author: email_from, no author_id, no followers """ + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com') + self.assertFalse(record.message_ids[0].author_id, 'message_process: unrecognized email -> no author_id') + self.assertEqual(record.message_ids[0].email_from, self.email_from) + self.assertEqual(len(record.message_partner_ids), 0, + 'message_process: newly create group should not have any follower') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_email_author(self): + """ Incoming email: recognized author: email_from, author_id, added as follower """ + with self.mock_mail_gateway(): + record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com', subject='Test1') + + self.assertEqual(record.message_ids[0].author_id, self.partner_1, + 'message_process: recognized email -> author_id') + self.assertEqual(record.message_ids[0].email_from, self.partner_1.email_formatted) + self.assertNotSentEmail() # No notification / bounce should be sent + + # Email recognized if partner has a formatted email + self.partner_1.write({'email': '"Valid Lelitre" <%s>' % self.partner_1.email}) + record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email, 'groups@test.com', subject='Test2') + + self.assertEqual(record.message_ids[0].author_id, self.partner_1, + 'message_process: recognized email -> author_id') + self.assertEqual(record.message_ids[0].email_from, self.partner_1.email) + self.assertNotSentEmail() # No notification / bounce should be sent + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_email_partner_find(self): + """ Finding the partner based on email, based on partner / user / follower """ + self.alias.write({'alias_force_thread_id': self.test_record.id}) + from_1 = self.env['res.partner'].create({'name': 'Brice Denisse', 'email': 'from.test@example.com'}) + + self.format_and_process(MAIL_TEMPLATE, from_1.email_formatted, 'groups@test.com') + self.assertEqual(self.test_record.message_ids[0].author_id, from_1) + self.test_record.message_unsubscribe([from_1.id]) + + from_2 = mail_new_test_user(self.env, login='B', groups='base.group_user', name='User Denisse', email='from.test@example.com') + + self.format_and_process(MAIL_TEMPLATE, from_1.email_formatted, 'groups@test.com') + self.assertEqual(self.test_record.message_ids[0].author_id, from_2.partner_id) + self.test_record.message_unsubscribe([from_2.partner_id.id]) + + from_3 = self.env['res.partner'].create({'name': 'FOllower Denisse', 'email': 'from.test@example.com'}) + self.test_record.message_subscribe([from_3.id]) + + self.format_and_process(MAIL_TEMPLATE, from_1.email_formatted, 'groups@test.com') + self.assertEqual(self.test_record.message_ids[0].author_id, from_3) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_email_author_exclude_alias(self): + """ Do not set alias as author to avoid including aliases in discussions """ + from_1 = self.env['res.partner'].create({'name': 'Brice Denisse', 'email': 'from.test@test.com'}) + self.env['mail.alias'].create({ + 'alias_name': 'from.test', + 'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id + }) + + record = self.format_and_process(MAIL_TEMPLATE, from_1.email_formatted, 'groups@test.com') + self.assertFalse(record.message_ids[0].author_id) + self.assertEqual(record.message_ids[0].email_from, from_1.email_formatted) + + # -------------------------------------------------- + # Alias configuration + # -------------------------------------------------- + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_message_process_alias_config_bounced_content(self): + """ Custom bounced message for the alias => Received this custom message """ + self.alias.write({ + 'alias_contact': 'partners', + 'alias_bounced_content': '<p>What Is Dead May Never Die</p>' + }) + + # Test: custom bounced content + with self.mock_mail_gateway(): + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce') + self.assertFalse(record, 'message_process: should have bounced') + self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], body_content='<p>What Is Dead May Never Die</p>') + + self.alias.write({ + 'alias_contact': 'partners', + 'alias_bounced_content': '<p></br></p>' + }) + + # Test: with "empty" bounced content (simulate view, putting always '<p></br></p>' in html field) + with self.mock_mail_gateway(): + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce') + self.assertFalse(record, 'message_process: should have bounced') + # Check if default (hardcoded) value is in the mail content + self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], body_content='The following email sent to') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_message_process_alias_config_bounced_to(self): + """ Check bounce message contains the bouncing alias, not a generic "to" """ + self.alias.write({'alias_contact': 'partners'}) + bounce_message_with_alias = "The following email sent to %s cannot be accepted because this is a private email address." % self.alias.display_name.lower() + + # Bounce is To + with self.mock_mail_gateway(): + self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'groups@example.com', + cc='other@gmail.com', subject='Should Bounce') + self.assertIn(bounce_message_with_alias, self._mails[0].get('body')) + + # Bounce is CC + with self.mock_mail_gateway(): + self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'other@gmail.com', + cc='groups@example.com', subject='Should Bounce') + self.assertIn(bounce_message_with_alias, self._mails[0].get('body')) + + # Bounce is part of To + with self.mock_mail_gateway(): + self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'other@gmail.com, groups@example.com', + subject='Should Bounce') + self.assertIn(bounce_message_with_alias, self._mails[0].get('body')) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_alias_defaults(self): + """ Test alias defaults and inner values """ + self.alias.write({ + 'alias_user_id': self.user_employee.id, + 'alias_defaults': "{'custom_field': 'defaults_custom'}" + }) + + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Specific') + self.assertEqual(len(record), 1) + res = record.get_metadata()[0].get('create_uid') or [None] + self.assertEqual(res[0], self.user_employee.id) + self.assertEqual(record.name, 'Specific') + self.assertEqual(record.custom_field, 'defaults_custom') + + self.alias.write({'alias_defaults': '""'}) + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Specific2') + self.assertEqual(len(record), 1) + res = record.get_metadata()[0].get('create_uid') or [None] + self.assertEqual(res[0], self.user_employee.id) + self.assertEqual(record.name, 'Specific2') + self.assertFalse(record.custom_field) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_alias_user_id(self): + """ Test alias ownership """ + self.alias.write({'alias_user_id': self.user_employee.id}) + + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com') + self.assertEqual(len(record), 1) + res = record.get_metadata()[0].get('create_uid') or [None] + self.assertEqual(res[0], self.user_employee.id) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_alias_everyone(self): + """ Incoming email: everyone: new record + message_new """ + self.alias.write({'alias_contact': 'everyone'}) + + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Specific') + self.assertEqual(len(record), 1) + self.assertEqual(len(record.message_ids), 1) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_message_process_alias_partners_bounce(self): + """ Incoming email from an unknown partner on a Partners only alias -> bounce + test bounce email """ + self.alias.write({'alias_contact': 'partners'}) + + # Test: no group created, email bounced + with self.mock_mail_gateway(): + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce') + self.assertFalse(record) + self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], subject='Re: Should Bounce') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_message_process_alias_followers_bounce(self): + """ Incoming email from unknown partner / not follower partner on a Followers only alias -> bounce """ + self.alias.write({ + 'alias_contact': 'followers', + 'alias_parent_model_id': self.env['ir.model']._get('mail.test.gateway').id, + 'alias_parent_thread_id': self.test_record.id, + }) + + # Test: unknown on followers alias -> bounce + with self.mock_mail_gateway(): + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Should Bounce') + self.assertFalse(record, 'message_process: should have bounced') + self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], subject='Re: Should Bounce') + + # Test: partner on followers alias -> bounce + self._init_mail_mock() + with self.mock_mail_gateway(): + record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com', subject='Should Bounce') + self.assertFalse(record, 'message_process: should have bounced') + self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], subject='Re: Should Bounce') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_alias_partner(self): + """ Incoming email from a known partner on a Partners alias -> ok (+ test on alias.user_id) """ + self.alias.write({'alias_contact': 'partners'}) + record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com') + + # Test: one group created by alias user + self.assertEqual(len(record), 1) + self.assertEqual(len(record.message_ids), 1) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_alias_followers(self): + """ Incoming email from a parent document follower on a Followers only alias -> ok """ + self.alias.write({ + 'alias_contact': 'followers', + 'alias_parent_model_id': self.env['ir.model']._get('mail.test.gateway').id, + 'alias_parent_thread_id': self.test_record.id, + }) + self.test_record.message_subscribe(partner_ids=[self.partner_1.id]) + record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@test.com') + + # Test: one group created by Raoul (or Sylvie maybe, if we implement it) + self.assertEqual(len(record), 1) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_message_process_alias_update(self): + """ Incoming email update discussion + notification email """ + self.alias.write({'alias_force_thread_id': self.test_record.id}) + + self.test_record.message_subscribe(partner_ids=[self.partner_1.id]) + with self.mock_mail_gateway(): + record = self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'groups@test.com>', + msg_id='<1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>', subject='Re: cats') + + # Test: no new group + new message + self.assertFalse(record, 'message_process: alias update should not create new records') + self.assertEqual(len(self.test_record.message_ids), 2) + # Test: sent emails: 1 (Sylvie copy of the incoming email) + self.assertSentEmail(self.email_from, [self.partner_1], subject='Re: cats') + + # -------------------------------------------------- + # Creator recognition + # -------------------------------------------------- + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_create_uid_crash(self): + def _employee_crash(*args, **kwargs): + """ If employee is test employee, consider he has no access on document """ + recordset = args[0] + if recordset.env.uid == self.user_employee.id and not recordset.env.su: + if kwargs.get('raise_exception', True): + raise exceptions.AccessError('Hop hop hop Ernest, please step back.') + return False + return DEFAULT + + with patch.object(MailTestGateway, 'check_access_rights', autospec=True, side_effect=_employee_crash): + record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_formatted, 'groups@test.com', subject='NoEmployeeAllowed') + self.assertEqual(record.create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].subject, 'NoEmployeeAllowed') + self.assertEqual(record.message_ids[0].create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_create_uid_email(self): + record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_formatted, 'groups@test.com', subject='Email Found') + self.assertEqual(record.create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].subject, 'Email Found') + self.assertEqual(record.message_ids[0].create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id) + + record = self.format_and_process(MAIL_TEMPLATE, 'Another name <%s>' % self.user_employee.email, 'groups@test.com', subject='Email OtherName') + self.assertEqual(record.create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].subject, 'Email OtherName') + self.assertEqual(record.message_ids[0].create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id) + + record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_normalized, 'groups@test.com', subject='Email SimpleEmail') + self.assertEqual(record.create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].subject, 'Email SimpleEmail') + self.assertEqual(record.message_ids[0].create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_create_uid_email_follower(self): + self.alias.write({ + 'alias_parent_model_id': self.test_model.id, + 'alias_parent_thread_id': self.test_record.id, + }) + follower_user = mail_new_test_user(self.env, login='better', groups='base.group_user', name='Ernest Follower', email=self.user_employee.email) + self.test_record.message_subscribe(follower_user.partner_id.ids) + + record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_formatted, 'groups@test.com', subject='FollowerWinner') + self.assertEqual(record.create_uid, follower_user) + self.assertEqual(record.message_ids[0].subject, 'FollowerWinner') + self.assertEqual(record.message_ids[0].create_uid, follower_user) + self.assertEqual(record.message_ids[0].author_id, follower_user.partner_id) + + # name order win + self.test_record.message_unsubscribe(follower_user.partner_id.ids) + self.test_record.flush() + record = self.format_and_process(MAIL_TEMPLATE, self.user_employee.email_formatted, 'groups@test.com', subject='FirstFoundWinner') + self.assertEqual(record.create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].subject, 'FirstFoundWinner') + self.assertEqual(record.message_ids[0].create_uid, self.user_employee) + self.assertEqual(record.message_ids[0].author_id, self.user_employee.partner_id) + + # -------------------------------------------------- + # Alias routing management + # -------------------------------------------------- + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_alias_no_domain(self): + """ Incoming email: write to alias even if no domain set: considered as valid alias """ + self.env['ir.config_parameter'].set_param('mail.catchall.domain', '') + + new_record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, 'groups@another.domain.com', subject='Test Subject') + # Test: one group created + self.assertEqual(len(new_record), 1, 'message_process: a new mail.test.simple should have been created') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_alias_forward_bypass_reply_first(self): + """ Incoming email: write to two "new thread" alias, one as a reply, one being another model -> consider as a forward """ + self.assertEqual(len(self.test_record.message_ids), 1) + + # test@.. will cause the creation of new mail.test + new_alias_2 = self.env['mail.alias'].create({ + 'alias_name': 'test', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, + 'alias_contact': 'everyone', + }) + new_rec = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s, %s@%s' % (new_alias_2.alias_name, self.alias_domain, self.alias.alias_name, self.alias_domain), + subject='Test Subject', + extra='In-Reply-To:\r\n\t%s\n' % self.fake_email.message_id, + target_model=new_alias_2.alias_model_id.model + ) + # Forward created a new record in mail.test + self.assertEqual(len(new_rec), 1, 'message_process: a new mail.test should have been created') + self.assertEqual(new_rec._name, new_alias_2.alias_model_id.model) + # No new post on test_record, no new record in mail.test.simple either + self.assertEqual(len(self.test_record.message_ids), 1, 'message_process: should not post on replied record as forward should bypass it') + new_simple = self.env['mail.test.simple'].search([('name', '=', 'Test Subject')]) + self.assertEqual(len(new_simple), 0, 'message_process: a new mail.test should not have been created') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_alias_forward_bypass_reply_second(self): + """ Incoming email: write to two "new thread" alias, one as a reply, one being another model -> consider as a forward """ + self.assertEqual(len(self.test_record.message_ids), 1) + + # test@.. will cause the creation of new mail.test + new_alias_2 = self.env['mail.alias'].create({ + 'alias_name': 'test', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, + 'alias_contact': 'everyone', + }) + new_rec = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s, %s@%s' % (self.alias.alias_name, self.alias_domain, new_alias_2.alias_name, self.alias_domain), + subject='Test Subject', + extra='In-Reply-To:\r\n\t%s\n' % self.fake_email.message_id, + target_model=new_alias_2.alias_model_id.model + ) + # Forward created a new record in mail.test + self.assertEqual(len(new_rec), 1, 'message_process: a new mail.test should have been created') + self.assertEqual(new_rec._name, new_alias_2.alias_model_id.model) + # No new post on test_record, no new record in mail.test.simple either + self.assertEqual(len(self.test_record.message_ids), 1, 'message_process: should not post on replied record as forward should bypass it') + new_simple = self.env['mail.test.simple'].search([('name', '=', 'Test Subject')]) + self.assertEqual(len(new_simple), 0, 'message_process: a new mail.test should not have been created') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_alias_forward_bypass_update_alias(self): + """ Incoming email: write to one "update", one "new thread" alias, one as a reply, one being another model -> consider as a forward """ + self.assertEqual(len(self.test_record.message_ids), 1) + self.alias.write({ + 'alias_force_thread_id': self.test_record.id, + }) + + # test@.. will cause the creation of new mail.test + new_alias_2 = self.env['mail.alias'].create({ + 'alias_name': 'test', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, + 'alias_contact': 'everyone', + }) + new_rec = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s, %s@%s' % (new_alias_2.alias_name, self.alias_domain, self.alias.alias_name, self.alias_domain), + subject='Test Subject', + extra='In-Reply-To:\r\n\t%s\n' % self.fake_email.message_id, + target_model=new_alias_2.alias_model_id.model + ) + # Forward created a new record in mail.test + self.assertEqual(len(new_rec), 1, 'message_process: a new mail.test should have been created') + self.assertEqual(new_rec._name, new_alias_2.alias_model_id.model) + # No new post on test_record, no new record in mail.test.simple either + self.assertEqual(len(self.test_record.message_ids), 1, 'message_process: should not post on replied record as forward should bypass it') + # No new record on first alias model + new_simple = self.env['mail.test.gateway'].search([('name', '=', 'Test Subject')]) + self.assertEqual(len(new_simple), 0, 'message_process: a new mail.test should not have been created') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_alias_multiple_new(self): + """ Incoming email: write to two aliases creating records: both should be activated """ + # test@.. will cause the creation of new mail.test + new_alias_2 = self.env['mail.alias'].create({ + 'alias_name': 'test', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, + 'alias_contact': 'everyone', + }) + new_rec = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s, %s@%s' % (self.alias.alias_name, self.alias_domain, new_alias_2.alias_name, self.alias_domain), + subject='Test Subject', + target_model=new_alias_2.alias_model_id.model + ) + # New record in both mail.test (new_alias_2) and mail.test.simple (self.alias) + self.assertEqual(len(new_rec), 1, 'message_process: a new mail.test should have been created') + self.assertEqual(new_rec._name, new_alias_2.alias_model_id.model) + new_simple = self.env['mail.test.gateway'].search([('name', '=', 'Test Subject')]) + self.assertEqual(len(new_simple), 1, 'message_process: a new mail.test should have been created') + + # -------------------------------------------------- + # Email Management + # -------------------------------------------------- + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_bounce(self): + """Incoming email: bounce using bounce alias: no record creation """ + with self.mock_mail_gateway(): + new_recs = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s+%s-%s-%s@%s' % ( + self.alias_bounce, self.fake_email.id, + self.fake_email.model, self.fake_email.res_id, + self.alias_domain + ), + subject='Should bounce', + ) + self.assertFalse(new_recs) + self.assertNotSentEmail() + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_bounce_if_static_but_still_has_plus_addressing(self): + """Incoming email: bounce using bounce alias without plus addressing: keep old behavior.""" + self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True) + with self.mock_mail_gateway(): + new_recs = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s+%s-%s-%s@%s' % ( + self.alias_bounce, self.fake_email.id, + self.fake_email.model, self.fake_email.res_id, + self.alias_domain + ), + subject='Should bounce', + ) + self.assertFalse(new_recs) + self.assertEqual(len(self._mails), 0, 'message_process: incoming bounce produces no mails') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_bounce_if_static_without_plus_addressing(self): + """Incoming email: bounce using bounce alias without plus addressing: bounce it.""" + self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True) + with self.mock_mail_gateway(): + new_recs = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s' % (self.alias_bounce, self.alias_domain), + subject='Should bounce', + ) + self.assertFalse(new_recs) + self.assertEqual(len(self._mails), 0, 'message_process: incoming bounce produces no mails') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_no_bounce_if_not_static_without_plus_addressing(self): + """Incoming email: bounce using bounce alias without plus addressing: raise as + considering as a direct write to bounce alias -> invalid """ + self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', False) + with self.assertRaises(ValueError): + self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s' % (self.alias_bounce, self.alias_domain), + subject="Should fail because it is not a bounce and there's no alias", + ) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_bounce_other_recipients(self): + """Incoming email: bounce processing: bounce should be computed even if not first recipient """ + with self.mock_mail_gateway(): + new_recs = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s, %s+%s-%s-%s@%s' % ( + self.alias.alias_name, self.alias_domain, + self.alias_bounce, self.fake_email.id, + self.fake_email.model, self.fake_email.res_id, + self.alias_domain + ), + subject='Should bounce', + ) + self.assertFalse(new_recs) + self.assertNotSentEmail() + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_message_route_write_to_catchall(self): + """ Writing directly to catchall should bounce """ + # Test: no group created, email bounced + with self.mock_mail_gateway(): + record = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '"My Super Catchall" <%s@%s>' % (self.alias_catchall, self.alias_domain), + subject='Should Bounce') + self.assertFalse(record) + self.assertSentEmail('"MAILER-DAEMON" <bounce.test@test.com>', ['whatever-2a840@postmaster.twitter.com'], subject='Re: Should Bounce') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_write_to_catchall_other_recipients_first(self): + """ Writing directly to catchall and a valid alias should take alias """ + # Test: no group created, email bounced + with self.mock_mail_gateway(): + record = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s, %s@%s' % (self.alias_catchall, self.alias_domain, self.alias.alias_name, self.alias_domain), + subject='Catchall Not Blocking' + ) + # Test: one group created + self.assertEqual(len(record), 1, 'message_process: a new mail.test should have been created') + # No bounce email + self.assertNotSentEmail() + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_route_write_to_catchall_other_recipients_second(self): + """ Writing directly to catchall and a valid alias should take alias """ + # Test: no group created, email bounced + with self.mock_mail_gateway(): + record = self.format_and_process( + MAIL_TEMPLATE, self.partner_1.email_formatted, + '%s@%s, %s@%s' % (self.alias.alias_name, self.alias_domain, self.alias_catchall, self.alias_domain), + subject='Catchall Not Blocking' + ) + # Test: one group created + self.assertEqual(len(record), 1, 'message_process: a new mail.test should have been created') + # No bounce email + self.assertNotSentEmail() + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_alias(self): + """ Writing to bounce alias is considered as a bounce even if not multipart/report bounce structure """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + bounced_mail_id = 4442 + bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com') + record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender') + self.assertFalse(record) + # No information found in bounce email -> not possible to do anything except avoiding email + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_from_mailer_demon(self): + """ MAILER_DAEMON emails are considered as bounce """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + record = self.format_and_process(MAIL_TEMPLATE, 'MAILER-DAEMON@example.com', 'groups@test.com', subject='Undelivered Mail Returned to Sender') + self.assertFalse(record) + # No information found in bounce email -> not possible to do anything except avoiding email + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_multipart_alias(self): + """ Multipart/report bounce correctly make related partner bounce """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + bounced_mail_id = 4442 + bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com') + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender') + self.assertFalse(record) + # Missing in reply to message_id -> cannot find original record + self.assertEqual(self.partner_1.message_bounce, 1) + self.assertEqual(self.test_record.message_bounce, 0) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_multipart_alias_reply(self): + """ Multipart/report bounce correctly make related partner and record found in bounce email bounce """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + bounced_mail_id = 4442 + bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com') + extra = self.fake_email.message_id + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender', extra=extra) + self.assertFalse(record) + self.assertEqual(self.partner_1.message_bounce, 1) + self.assertEqual(self.test_record.message_bounce, 1) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_multipart_alias_whatever_from(self): + """ Multipart/report bounce correctly make related record found in bounce email bounce """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + bounced_mail_id = 4442 + bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com') + extra = self.fake_email.message_id + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'Whatever <what@ever.com>', bounce_email_to, subject='Undelivered Mail Returned to Sender', extra=extra) + self.assertFalse(record) + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 1) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_multipart_whatever_to_and_from(self): + """ Multipart/report bounce correctly make related record found in bounce email bounce """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + extra = self.fake_email.message_id + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'Whatever <what@ever.com>', 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra) + self.assertFalse(record) + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 1) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_records_channel(self): + """ Test blacklist allow to multi-bounce and auto update of mail.channel """ + other_record = self.env['mail.test.gateway'].create({ + 'email_from': 'Another name <%s>' % self.partner_1.email + }) + yet_other_record = self.env['mail.test.gateway'].create({ + 'email_from': 'Yet Another name <%s>' % self.partner_1.email.upper() + }) + test_channel = self.env['mail.channel'].create({ + 'name': 'Test', + 'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.partner_1.id})], + }) + self.fake_email.write({ + 'model': 'mail.channel', + 'res_id': test_channel.id, + }) + self.assertIn(self.partner_1, test_channel.channel_partner_ids) + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(other_record.message_bounce, 0) + self.assertEqual(yet_other_record.message_bounce, 0) + + extra = self.fake_email.message_id + for i in range(10): + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'A third name <%s>' % self.partner_1.email, 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra) + self.assertFalse(record) + self.assertEqual(self.partner_1.message_bounce, 10) + self.assertEqual(self.test_record.message_bounce, 0) + self.assertEqual(other_record.message_bounce, 10) + self.assertEqual(yet_other_record.message_bounce, 10) + self.assertNotIn(self.partner_1, test_channel.channel_partner_ids) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_records_partner(self): + """ Test blacklist + bounce on ``res.partner`` model """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.fake_email.write({ + 'model': 'res.partner', + 'res_id': self.partner_1.id, + }) + + extra = self.fake_email.message_id + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra) + self.assertFalse(record) + self.assertEqual(self.partner_1.message_bounce, 1) + self.assertEqual(self.test_record.message_bounce, 0) + + # -------------------------------------------------- + # Thread formation + # -------------------------------------------------- + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_in_reply_to(self): + """ Incoming email using in-rely-to should go into the right destination even with a wrong destination """ + init_msg_count = len(self.test_record.message_ids) + self.format_and_process( + MAIL_TEMPLATE, 'valid.other@gmail.com', 'erroneous@test.com>', + subject='Re: news', extra='In-Reply-To:\r\n\t%s\n' % self.fake_email.message_id) + + self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1) + self.assertEqual(self.fake_email.child_ids, self.test_record.message_ids[0]) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_references(self): + """ Incoming email using references should go into the right destination even with a wrong destination """ + init_msg_count = len(self.test_record.message_ids) + self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'erroneous@test.com', + extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id) + + self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1) + self.assertEqual(self.fake_email.child_ids, self.test_record.message_ids[0]) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_references_external(self): + """ Incoming email being a reply to an external email processed by odoo should update thread accordingly """ + new_message_id = '<ThisIsTooMuchFake.MonsterEmail.789@agrolait.com>' + self.fake_email.write({ + 'message_id': new_message_id + }) + init_msg_count = len(self.test_record.message_ids) + self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'erroneous@test.com', + extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id) + + self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1) + self.assertEqual(self.fake_email.child_ids, self.test_record.message_ids[0]) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_references_external_buggy_message_id(self): + """ + Incoming email being a reply to an external email processed by + odoo should update thread accordingly. Special case when the + external mail service wrongly folds the message_id on several + lines. + """ + new_message_id = '<ThisIsTooMuchFake.MonsterEmail.789@agrolait.com>' + buggy_message_id = new_message_id.replace('MonsterEmail', 'Monster\r\n Email') + self.fake_email.write({ + 'message_id': new_message_id + }) + init_msg_count = len(self.test_record.message_ids) + self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'erroneous@test.com', + extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % buggy_message_id) + + self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1) + self.assertEqual(self.fake_email.child_ids, self.test_record.message_ids[0]) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_references_forward(self): + """ Incoming email using references but with alias forward should not go into references destination """ + self.env['mail.alias'].create({ + 'alias_name': 'test.alias', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, + 'alias_contact': 'everyone', + }) + init_msg_count = len(self.test_record.message_ids) + res_test = self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'test.alias@test.com', + subject='My Dear Forward', extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id, + target_model='mail.test.container') + + self.assertEqual(len(self.test_record.message_ids), init_msg_count) + self.assertEqual(len(self.fake_email.child_ids), 0) + self.assertEqual(res_test.name, 'My Dear Forward') + self.assertEqual(len(res_test.message_ids), 1) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_references_forward_same_model(self): + """ Incoming email using references but with alias forward on same model should be considered as a reply """ + self.env['mail.alias'].create({ + 'alias_name': 'test.alias', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id, + 'alias_contact': 'everyone', + }) + init_msg_count = len(self.test_record.message_ids) + res_test = self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'test.alias@test.com', + subject='My Dear Forward', extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id, + target_model='mail.test.container') + + self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1) + self.assertEqual(len(self.fake_email.child_ids), 1) + self.assertFalse(res_test) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_references_forward_cc(self): + """ Incoming email using references but with alias forward in CC should be considered as a repy (To > Cc) """ + self.env['mail.alias'].create({ + 'alias_name': 'test.alias', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, + 'alias_contact': 'everyone', + }) + init_msg_count = len(self.test_record.message_ids) + res_test = self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'catchall.test@test.com', cc='test.alias@test.com', + subject='My Dear Forward', extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id, + target_model='mail.test.container') + + self.assertEqual(len(self.test_record.message_ids), init_msg_count + 1) + self.assertEqual(len(self.fake_email.child_ids), 1) + self.assertFalse(res_test) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models', 'odoo.addons.mail.models.mail_mail') + def test_message_process_reply_to_new_thread(self): + """ Test replies not being considered as replies but use destination information instead (aka, mass post + specific reply to using aliases) """ + first_record = self.env['mail.test.simple'].with_user(self.user_employee).create({'name': 'Replies to Record'}) + record_msg = first_record.message_post( + subject='Discussion', + no_auto_thread=False, + subtype_xmlid='mail.mt_comment', + ) + self.assertEqual(record_msg.reply_to, formataddr(('%s %s' % (self.user_employee.company_id.name, first_record.name), '%s@%s' % ('catchall.test', 'test.com')))) + mail_msg = first_record.message_post( + subject='Replies to Record', + reply_to='groups@test.com', + no_auto_thread=True, + subtype_xmlid='mail.mt_comment', + ) + self.assertEqual(mail_msg.reply_to, 'groups@test.com') + + # reply to mail but should be considered as a new mail for alias + msgID = '<this.is.duplicate.test@iron.sky>' + res_test = self.format_and_process( + MAIL_TEMPLATE, self.email_from, record_msg.reply_to, cc='', + subject='Re: Replies to Record', extra='In-Reply-To: %s' % record_msg.message_id, + msg_id=msgID, target_model='mail.test.simple') + incoming_msg = self.env['mail.message'].search([('message_id', '=', msgID)]) + self.assertFalse(res_test) + self.assertEqual(incoming_msg.model, 'mail.test.simple') + self.assertEqual(incoming_msg.parent_id, first_record.message_ids[-1]) + self.assertTrue(incoming_msg.res_id == first_record.id) + + # reply to mail but should be considered as a new mail for alias + msgID = '<this.is.for.testing@iron.sky>' + res_test = self.format_and_process( + MAIL_TEMPLATE, self.email_from, mail_msg.reply_to, cc='', + subject='Re: Replies to Record', extra='In-Reply-To: %s' % mail_msg.message_id, + msg_id=msgID, target_model='mail.test.gateway') + incoming_msg = self.env['mail.message'].search([('message_id', '=', msgID)]) + self.assertEqual(len(res_test), 1) + self.assertEqual(res_test.name, 'Re: Replies to Record') + self.assertEqual(incoming_msg.model, 'mail.test.gateway') + self.assertFalse(incoming_msg.parent_id) + self.assertTrue(incoming_msg.res_id == res_test.id) + + # -------------------------------------------------- + # Thread formation: mail gateway corner cases + # -------------------------------------------------- + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_extra_model_res_id(self): + """ Incoming email with ref holding model / res_id but that does not match any message in the thread: must raise since OpenERP saas-3 """ + self.assertRaises(ValueError, + self.format_and_process, MAIL_TEMPLATE, + self.partner_1.email_formatted, 'noone@test.com', subject='spam', + extra='In-Reply-To: <12321321-openerp-%d-mail.test.gateway@%s>' % (self.test_record.id, socket.gethostname())) + + # when 6.1 messages are present, compat mode is available + # Odoo 10 update: compat mode has been removed and should not work anymore + self.fake_email.write({'message_id': False}) + # Do: compat mode accepts partial-matching emails + self.assertRaises( + ValueError, + self.format_and_process, MAIL_TEMPLATE, + self.partner_1.email_formatted, 'noone@test.com>', subject='spam', + extra='In-Reply-To: <12321321-openerp-%d-mail.test.gateway@%s>' % (self.test_record.id, socket.gethostname())) + + # Test created messages + self.assertEqual(len(self.test_record.message_ids), 1) + self.assertEqual(len(self.test_record.message_ids[0].child_ids), 0) + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_duplicate(self): + """ Duplicate emails (same message_id) are not processed """ + self.alias.write({'alias_force_thread_id': self.test_record.id,}) + + # Post a base message + record = self.format_and_process(MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Re: super cats', msg_id='<123?456.diff1@agrolait.com>') + self.assertFalse(record) + self.assertEqual(len(self.test_record.message_ids), 2) + + # Do: due to some issue, same email goes back into the mailgateway + record = self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'groups@test.com', subject='Re: news', + msg_id='<123?456.diff1@agrolait.com>', extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>\n') + self.assertFalse(record) + self.assertEqual(len(self.test_record.message_ids), 2) + + # Test: message_id is still unique + no_of_msg = self.env['mail.message'].search_count([('message_id', 'ilike', '<123?456.diff1@agrolait.com>')]) + self.assertEqual(no_of_msg, 1, + 'message_process: message with already existing message_id should not have been duplicated') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_crash_wrong_model(self): + """ Incoming email with model that does not accepts incoming emails must raise """ + self.assertRaises(ValueError, + self.format_and_process, + MAIL_TEMPLATE, self.email_from, 'noone@test.com', + subject='spam', extra='', model='res.country') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_crash_no_data(self): + """ Incoming email without model and without alias must raise """ + self.assertRaises(ValueError, + self.format_and_process, + MAIL_TEMPLATE, self.email_from, 'noone@test.com', + subject='spam', extra='') + + @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') + def test_message_process_fallback(self): + """ Incoming email with model that accepting incoming emails as fallback """ + record = self.format_and_process( + MAIL_TEMPLATE, self.email_from, 'noone@test.com', + subject='Spammy', extra='', model='mail.test.gateway') + self.assertEqual(len(record), 1) + self.assertEqual(record.name, 'Spammy') + self.assertEqual(record._name, 'mail.test.gateway') + + +class TestMailThreadCC(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestMailThreadCC, cls).setUpClass() + + cls.email_from = 'Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>' + cls.alias = cls.env['mail.alias'].create({ + 'alias_name': 'cc_record', + 'alias_user_id': False, + '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', + cc='cc1@example.com, cc2@example.com', target_model='mail.test.cc') + cc = email_split_and_format(record.email_cc) + self.assertEqual(sorted(cc), ['cc1@example.com', 'cc2@example.com']) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_cc_update_with_old(self): + record = self.env['mail.test.cc'].create({'email_cc': 'cc1 <cc1@example.com>, cc2@example.com'}) + self.alias.write({'alias_force_thread_id': record.id}) + + self.format_and_process(MAIL_TEMPLATE, self.email_from, 'cc_record@test.com', + cc='cc2 <cc2@example.com>, cc3@example.com', target_model='mail.test.cc') + cc = email_split_and_format(record.email_cc) + self.assertEqual(sorted(cc), ['"cc1" <cc1@example.com>', 'cc2@example.com', 'cc3@example.com'], 'new cc should have been added on record (unique)') + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_cc_update_no_old(self): + record = self.env['mail.test.cc'].create({}) + self.alias.write({'alias_force_thread_id': record.id}) + + self.format_and_process(MAIL_TEMPLATE, self.email_from, 'cc_record@test.com', + cc='cc2 <cc2@example.com>, cc3@example.com', target_model='mail.test.cc') + cc = email_split_and_format(record.email_cc) + self.assertEqual(sorted(cc), ['"cc2" <cc2@example.com>', 'cc3@example.com'], 'new cc should have been added on record (unique)') diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py new file mode 100644 index 00000000..3ebee263 --- /dev/null +++ b/addons/test_mail/tests/test_mail_mail.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import psycopg2 + +from odoo import api +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): + + @classmethod + def setUpClass(cls): + super(TestMailMail, cls).setUpClass() + cls._init_mail_gateway() + + cls.test_record = cls.env['mail.test.gateway'].with_context(cls._test_context).create({ + 'name': 'Test', + 'email_from': 'ignasse@example.com', + }).with_context({}) + + @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 + mail = self.env['mail.mail'].sudo().create({ + 'body_html': '<p>Test</p>', + 'email_to': 'test@example.com', + 'partner_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.assertEqual(len(self._mails), 1) + + def test_mail_mail_return_path(self): + # mail without thread-enabled record + base_values = { + 'body_html': '<p>Test</p>', + 'email_to': 'test@example.com', + } + + mail = self.env['mail.mail'].create(base_values) + with self.mock_mail_gateway(): + mail.send() + self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s+%d@%s' % (self.alias_bounce, mail.id, self.alias_domain)) + + # mail on thread-enabled record + mail = self.env['mail.mail'].create(dict(base_values, **{ + 'model': self.test_record._name, + 'res_id': self.test_record.id, + })) + with self.mock_mail_gateway(): + mail.send() + self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s+%d-%s-%s@%s' % (self.alias_bounce, mail.id, self.test_record._name, self.test_record.id, self.alias_domain)) + + # force static addressing on bounce alias + self.env['ir.config_parameter'].set_param('mail.bounce.alias.static', True) + + # mail without thread-enabled record + mail = self.env['mail.mail'].create(base_values) + with self.mock_mail_gateway(): + mail.send() + self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain)) + + # mail on thread-enabled record + mail = self.env['mail.mail'].create(dict(base_values, **{ + 'model': self.test_record._name, + 'res_id': self.test_record.id, + })) + with self.mock_mail_gateway(): + mail.send() + self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain)) + + +class TestMailMailRace(common.TransactionCase): + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_bounce_during_send(self): + self.partner = self.env['res.partner'].create({ + 'name': 'Ernest Partner', + }) + # we need to simulate a mail sent by the cron task, first create mail, message and notification by hand + mail = self.env['mail.mail'].sudo().create({ + 'body_html': '<p>Test</p>', + 'notification': True, + 'state': 'outgoing', + 'recipient_ids': [(4, self.partner.id)] + }) + message = self.env['mail.message'].create({ + 'subject': 'S', + 'body': 'B', + 'subtype_id': self.ref('mail.mt_comment'), + 'notification_ids': [(0, 0, { + 'res_partner_id': self.partner.id, + 'mail_id': mail.id, + 'notification_type': 'email', + 'is_read': True, + 'notification_status': 'ready', + })], + }) + notif = self.env['mail.notification'].search([('res_partner_id', '=', self.partner.id)]) + # we need to commit transaction or cr will keep the lock on notif + self.cr.commit() + + # patch send_email in order to create a concurent update and check the notif is already locked by _send() + this = self # coding in javascript ruinned my life + bounce_deferred = [] + @api.model + def send_email(self, message, *args, **kwargs): + with this.registry.cursor() as cr, mute_logger('odoo.sql_db'): + try: + # try ro aquire lock (no wait) on notification (should fail) + cr.execute("SELECT notification_status FROM mail_message_res_partner_needaction_rel WHERE id = %s FOR UPDATE NOWAIT", [notif.id]) + except psycopg2.OperationalError: + # record already locked by send, all good + bounce_deferred.append(True) + else: + # this should trigger psycopg2.extensions.TransactionRollbackError in send(). + # Only here to simulate the initial use case + # If the record is lock, this line would create a deadlock since we are in the same thread + # In practice, the update will wait the end of the send() transaction and set the notif as bounce, as expeced + cr.execute("UPDATE mail_message_res_partner_needaction_rel SET notification_status='bounce' WHERE id = %s", [notif.id]) + return message['Message-Id'] + self.env['ir.mail_server']._patch_method('send_email', send_email) + + mail.send() + + self.assertTrue(bounce_deferred, "The bounce should have been deferred") + self.assertEqual(notif.notification_status, 'sent') + + # some cleaning since we commited the cr + self.env['ir.mail_server']._revert_method('send_email') + + notif.unlink() + message.unlink() + mail.unlink() + self.partner.unlink() + self.env.cr.commit() diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py new file mode 100644 index 00000000..8d384f72 --- /dev/null +++ b/addons/test_mail/tests/test_mail_message.py @@ -0,0 +1,591 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +from unittest.mock import patch + +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.test_mail.tests.common import TestMailCommon +from odoo.addons.test_mail.models.test_mail_models import MailTestSimple +from odoo.exceptions import AccessError +from odoo.tools import mute_logger, formataddr +from odoo.tests import tagged + + +class TestMessageValues(TestMailCommon): + + @classmethod + 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', + 'alias_contact': 'followers', + }) + cls.Message = cls.env['mail.message'].with_user(cls.user_employee) + + @mute_logger('odoo.models.unlink') + def test_mail_message_format(self): + record1 = self.env['mail.test.simple'].create({'name': 'Test1'}) + message = self.env['mail.message'].create([{ + 'model': 'mail.test.simple', + 'res_id': record1.id, + }]) + res = message.message_format() + self.assertEqual(res[0].get('record_name'), 'Test1') + + record1.write({"name": "Test2"}) + res = message.message_format() + self.assertEqual(res[0].get('record_name'), 'Test2') + + @mute_logger('odoo.models.unlink') + def test_mail_message_format_access(self): + """ + User that doesn't have access to a record should still be able to fetch + the record_name inside message_format. + """ + company_2 = self.env['res.company'].create({'name': 'Second Test Company'}) + record1 = self.env['mail.test.multi.company'].create({ + 'name': 'Test1', + 'company_id': company_2.id, + }) + message = record1.message_post(body='', partner_ids=[self.user_employee.partner_id.id]) + # We need to flush and invalidate the ORM cache since the record_name + # is already cached from the creation. Otherwise it will leak inside + # message_format. + message.flush() + message.invalidate_cache() + res = message.with_user(self.user_employee).message_format() + self.assertEqual(res[0].get('record_name'), 'Test1') + + @mute_logger('odoo.models.unlink') + def test_mail_message_values_no_document_values(self): + msg = self.Message.create({ + 'reply_to': 'test.reply@example.com', + 'email_from': 'test.from@example.com', + }) + self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one') + self.assertEqual(msg.reply_to, 'test.reply@example.com') + self.assertEqual(msg.email_from, 'test.from@example.com') + + @mute_logger('odoo.models.unlink') + def test_mail_message_values_no_document(self): + msg = self.Message.create({}) + self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one') + reply_to_name = self.env.user.company_id.name + reply_to_email = '%s@%s' % (self.alias_catchall, self.alias_domain) + self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email))) + self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email))) + + # no alias domain -> author + self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink() + + msg = self.Message.create({}) + self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one') + self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email))) + self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email))) + + # no alias catchall, no alias -> author + self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain) + self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink() + + msg = self.Message.create({}) + self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one') + self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email))) + self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email))) + + @mute_logger('odoo.models.unlink') + def test_mail_message_values_document_alias(self): + msg = self.Message.create({ + 'model': 'mail.test.container', + 'res_id': self.alias_record.id + }) + self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0]) + reply_to_name = '%s %s' % (self.env.user.company_id.name, self.alias_record.name) + reply_to_email = '%s@%s' % (self.alias_record.alias_name, self.alias_domain) + self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email))) + self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email))) + + # no alias domain -> author + self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink() + + msg = self.Message.create({ + 'model': 'mail.test.container', + 'res_id': self.alias_record.id + }) + self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0]) + self.assertEqual(msg.reply_to, formataddr((self.user_employee.name, self.user_employee.email))) + self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email))) + + # no catchall -> don't care, alias + self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain) + self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink() + + msg = self.Message.create({ + 'model': 'mail.test.container', + 'res_id': self.alias_record.id + }) + self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0]) + reply_to_name = '%s %s' % (self.env.company.name, self.alias_record.name) + reply_to_email = '%s@%s' % (self.alias_record.alias_name, self.alias_domain) + self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email))) + self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email))) + + @mute_logger('odoo.models.unlink') + def test_mail_message_values_document_no_alias(self): + test_record = self.env['mail.test.simple'].create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + + msg = self.Message.create({ + 'model': 'mail.test.simple', + 'res_id': test_record.id + }) + self.assertIn('-openerp-%d-mail.test.simple' % test_record.id, msg.message_id.split('@')[0]) + reply_to_name = '%s %s' % (self.env.user.company_id.name, test_record.name) + reply_to_email = '%s@%s' % (self.alias_catchall, self.alias_domain) + self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email))) + self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email))) + + @mute_logger('odoo.models.unlink') + def test_mail_message_values_document_manual_alias(self): + test_record = self.env['mail.test.simple'].create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + alias = self.env['mail.alias'].create({ + 'alias_name': 'MegaLias', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.simple').id, + 'alias_parent_model_id': self.env['ir.model']._get('mail.test.simple').id, + 'alias_parent_thread_id': test_record.id, + }) + + msg = self.Message.create({ + 'model': 'mail.test.simple', + 'res_id': test_record.id + }) + + self.assertIn('-openerp-%d-mail.test.simple' % test_record.id, msg.message_id.split('@')[0]) + reply_to_name = '%s %s' % (self.env.user.company_id.name, test_record.name) + reply_to_email = '%s@%s' % (alias.alias_name, self.alias_domain) + self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email))) + self.assertEqual(msg.email_from, formataddr((self.user_employee.name, self.user_employee.email))) + + def test_mail_message_values_no_auto_thread(self): + msg = self.Message.create({ + 'model': 'mail.test.container', + 'res_id': self.alias_record.id, + 'no_auto_thread': True, + }) + self.assertIn('reply_to', msg.message_id.split('@')[0]) + self.assertNotIn('mail.test.container', msg.message_id.split('@')[0]) + self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id.split('@')[0]) + + def test_mail_message_base64_image(self): + msg = self.env['mail.message'].with_user(self.user_employee).create({ + 'body': 'taratata <img src="data:image/png;base64,iV/+OkI=" width="2"> <img src="data:image/png;base64,iV/+OkI=" width="2">', + }) + self.assertEqual(len(msg.attachment_ids), 1) + body = '<p>taratata <img src="/web/image/%s?access_token=%s" alt="image0" width="2"> <img src="/web/image/%s?access_token=%s" alt="image0" width="2"></p>' + body = body % (msg.attachment_ids[0].id, msg.attachment_ids[0].access_token, msg.attachment_ids[0].id, msg.attachment_ids[0].access_token) + self.assertEqual(msg.body, body) + + +class TestMessageAccess(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestMessageAccess, cls).setUpClass() + + cls.user_public = mail_new_test_user(cls.env, login='bert', groups='base.group_public', name='Bert Tartignole') + cls.user_portal = mail_new_test_user(cls.env, login='chell', groups='base.group_portal', name='Chell Gladys') + + Channel = cls.env['mail.channel'].with_context(cls._test_context) + # Pigs: base group for tests + cls.group_pigs = Channel.create({ + 'name': 'Pigs', + 'public': 'groups', + 'group_public_id': cls.env.ref('base.group_user').id}) + # Jobs: public group + cls.group_public = Channel.create({ + 'name': 'Jobs', + 'description': 'NotFalse', + 'public': 'public'}) + # Private: private gtroup + cls.group_private = Channel.create({ + 'name': 'Private', + 'public': 'private'}) + cls.message = cls.env['mail.message'].create({ + 'body': 'My Body', + 'model': 'mail.channel', + 'res_id': cls.group_private.id, + }) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_message_access_search(self): + # Data: various author_ids, partner_ids, documents + msg1 = self.env['mail.message'].create({ + 'subject': '_ZTest', 'body': 'A', 'subtype_id': self.ref('mail.mt_comment')}) + msg2 = self.env['mail.message'].create({ + 'subject': '_ZTest', 'body': 'A+B', 'subtype_id': self.ref('mail.mt_comment'), + 'partner_ids': [(6, 0, [self.user_public.partner_id.id])]}) + msg3 = self.env['mail.message'].create({ + 'subject': '_ZTest', 'body': 'A Pigs', 'subtype_id': False, + 'model': 'mail.channel', 'res_id': self.group_pigs.id}) + msg4 = self.env['mail.message'].create({ + 'subject': '_ZTest', 'body': 'A+P Pigs', 'subtype_id': self.ref('mail.mt_comment'), + 'model': 'mail.channel', 'res_id': self.group_pigs.id, + 'partner_ids': [(6, 0, [self.user_public.partner_id.id])]}) + msg5 = self.env['mail.message'].create({ + 'subject': '_ZTest', 'body': 'A+E Pigs', 'subtype_id': self.ref('mail.mt_comment'), + 'model': 'mail.channel', 'res_id': self.group_pigs.id, + 'partner_ids': [(6, 0, [self.user_employee.partner_id.id])]}) + msg6 = self.env['mail.message'].create({ + 'subject': '_ZTest', 'body': 'A Birds', 'subtype_id': self.ref('mail.mt_comment'), + 'model': 'mail.channel', 'res_id': self.group_private.id}) + msg7 = self.env['mail.message'].with_user(self.user_employee).create({ + 'subject': '_ZTest', 'body': 'B', 'subtype_id': self.ref('mail.mt_comment')}) + msg8 = self.env['mail.message'].with_user(self.user_employee).create({ + 'subject': '_ZTest', 'body': 'B+E', 'subtype_id': self.ref('mail.mt_comment'), + 'partner_ids': [(6, 0, [self.user_employee.partner_id.id])]}) + + # Test: Public: 2 messages (recipient) + messages = self.env['mail.message'].with_user(self.user_public).search([('subject', 'like', '_ZTest')]) + self.assertEqual(messages, msg2 | msg4) + + # Test: Employee: 3 messages on Pigs Raoul can read (employee can read group with default values) + messages = self.env['mail.message'].with_user(self.user_employee).search([('subject', 'like', '_ZTest'), ('body', 'ilike', 'A')]) + self.assertEqual(messages, msg3 | msg4 | msg5) + + # Test: Raoul: 3 messages on Pigs Raoul can read (employee can read group with default values), 0 on Birds (private group) + 2 messages as author + messages = self.env['mail.message'].with_user(self.user_employee).search([('subject', 'like', '_ZTest')]) + self.assertEqual(messages, msg3 | msg4 | msg5 | msg7 | msg8) + + # Test: Admin: all messages + messages = self.env['mail.message'].search([('subject', 'like', '_ZTest')]) + self.assertEqual(messages, msg1 | msg2 | msg3 | msg4 | msg5 | msg6 | msg7 | msg8) + + # Test: Portal: 0 (no access to groups, not recipient) + messages = self.env['mail.message'].with_user(self.user_portal).search([('subject', 'like', '_ZTest')]) + self.assertFalse(messages) + + # Test: Portal: 2 messages (public group with a subtype) + self.group_pigs.write({'public': 'public'}) + messages = self.env['mail.message'].with_user(self.user_portal).search([('subject', 'like', '_ZTest')]) + self.assertEqual(messages, msg4 | msg5) + + # -------------------------------------------------- + # READ + # -------------------------------------------------- + + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') + def test_mail_message_access_read_crash(self): + with self.assertRaises(AccessError): + self.message.with_user(self.user_employee).read() + + @mute_logger('odoo.models') + def test_mail_message_access_read_crash_portal(self): + with self.assertRaises(AccessError): + self.message.with_user(self.user_portal).read(['body', 'message_type', 'subtype_id']) + + def test_mail_message_access_read_ok_portal(self): + self.message.write({'subtype_id': self.ref('mail.mt_comment'), 'res_id': self.group_public.id}) + self.message.with_user(self.user_portal).read(['body', 'message_type', 'subtype_id']) + + def test_mail_message_access_read_notification(self): + attachment = self.env['ir.attachment'].create({ + 'datas': base64.b64encode(b'My attachment'), + 'name': 'doc.txt'}) + # attach the attachment to the message + self.message.write({'attachment_ids': [(4, attachment.id)]}) + self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]}) + self.message.with_user(self.user_employee).read() + # Test: Bert has access to attachment, ok because he can read message + attachment.with_user(self.user_employee).read(['name', 'datas']) + + def test_mail_message_access_read_author(self): + self.message.write({'author_id': self.user_employee.partner_id.id}) + self.message.with_user(self.user_employee).read() + + def test_mail_message_access_read_doc(self): + self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id}) + # Test: Bert reads the message, ok because linked to a doc he is allowed to read + self.message.with_user(self.user_employee).read() + + def test_mail_message_access_read_crash_moderation(self): + # with self.assertRaises(AccessError): + self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'}) + # Test: Bert reads the message, ok because linked to a doc he is allowed to read + self.message.with_user(self.user_employee).read() + + # -------------------------------------------------- + # CREATE + # -------------------------------------------------- + + @mute_logger('odoo.addons.base.models.ir_model') + def test_mail_message_access_create_crash_public(self): + # Do: Bert creates a message on Pigs -> ko, no creation rights + with self.assertRaises(AccessError): + self.env['mail.message'].with_user(self.user_public).create({'model': 'mail.channel', 'res_id': self.group_pigs.id, 'body': 'Test'}) + + # Do: Bert create a message on Jobs -> ko, no creation rights + with self.assertRaises(AccessError): + self.env['mail.message'].with_user(self.user_public).create({'model': 'mail.channel', 'res_id': self.group_public.id, 'body': 'Test'}) + + @mute_logger('odoo.models') + def test_mail_message_access_create_crash(self): + # Do: Bert create a private message -> ko, no creation rights + with self.assertRaises(AccessError): + self.env['mail.message'].with_user(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test'}) + + @mute_logger('odoo.models') + def test_mail_message_access_create_doc(self): + Message = self.env['mail.message'].with_user(self.user_employee) + # Do: Raoul creates a message on Jobs -> ok, write access to the related document + Message.create({'model': 'mail.channel', 'res_id': self.group_public.id, 'body': 'Test'}) + # Do: Raoul creates a message on Priv -> ko, no write access to the related document + with self.assertRaises(AccessError): + Message.create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test'}) + + def test_mail_message_access_create_private(self): + self.env['mail.message'].with_user(self.user_employee).create({'body': 'Test'}) + + def test_mail_message_access_create_reply(self): + # TDE FIXME: should it really work ? not sure - catchall makes crash (aka, post will crash also) + self.env['ir.config_parameter'].set_param('mail.catchall.domain', False) + self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]}) + self.env['mail.message'].with_user(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test', 'parent_id': self.message.id}) + + def test_mail_message_access_create_wo_parent_access(self): + """ Purpose is to test posting a message on a record whose first message / parent + is not accessible by current user. """ + test_record = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + partner_1 = self.env['res.partner'].create({ + 'name': 'Jitendra Prajapati (jpr-odoo)', + 'email': 'jpr@odoo.com', + }) + test_record.message_subscribe((partner_1 | self.user_admin.partner_id).ids) + + message = test_record.message_post( + body='<p>This is First Message</p>', subject='Subject', + message_type='comment', subtype_xmlid='mail.mt_note') + # portal user have no rights to read the message + with self.assertRaises(AccessError): + message.with_user(self.user_portal).read(['subject, body']) + + with patch.object(MailTestSimple, 'check_access_rights', return_value=True): + with self.assertRaises(AccessError): + message.with_user(self.user_portal).read(['subject, body']) + + # parent message is accessible to references notification mail values + # for _notify method and portal user have no rights to send the message for this model + new_msg = test_record.with_user(self.user_portal).message_post( + body='<p>This is Second Message</p>', + subject='Subject', + parent_id=message.id, + message_type='comment', + subtype_xmlid='mail.mt_comment', + mail_auto_delete=False) + + new_mail = self.env['mail.mail'].sudo().search([ + ('mail_message_id', '=', new_msg.id), + ('references', '=', message.message_id), + ]) + + self.assertTrue(new_mail) + self.assertEqual(new_msg.parent_id, message) + + # -------------------------------------------------- + # WRITE + # -------------------------------------------------- + + def test_mail_message_access_write_moderation(self): + """ Only moderators can modify pending messages """ + self.group_public.write({ + 'email_send': True, + 'moderation': True, + 'channel_partner_ids': [(4, self.partner_employee.id)], + 'moderator_ids': [(4, self.user_employee.id)], + }) + self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'}) + self.message.with_user(self.user_employee).write({'moderation_status': 'accepted'}) + + def test_mail_message_access_write_crash_moderation(self): + self.message.write({'model': 'mail.channel', 'res_id': self.group_public.id, 'moderation_status': 'pending_moderation'}) + with self.assertRaises(AccessError): + self.message.with_user(self.user_employee).write({'moderation_status': 'accepted'}) + + @mute_logger('openerp.addons.mail.models.mail_mail') + def test_mark_all_as_read(self): + self.user_employee.notification_type = 'inbox' + emp_partner = self.user_employee.partner_id.with_user(self.user_employee) + + group_private = self.env['mail.channel'].with_context({ + 'mail_create_nolog': True, + 'mail_create_nosubscribe': True, + 'mail_channel_noautofollow': True, + }).create({ + 'name': 'Private', + 'description': 'Private James R.', + 'public': 'private', + 'alias_name': 'private', + 'alias_contact': 'followers'} + ).with_context({'mail_create_nosubscribe': False}) + + # mark all as read clear needactions + msg1 = group_private.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[emp_partner.id]) + self._reset_bus() + emp_partner.env['mail.message'].mark_all_as_read(domain=[]) + self.assertBusNotifications([(self.cr.dbname, 'res.partner', emp_partner.id)], [{ 'type': 'mark_as_read', 'message_ids': [msg1.id], 'needaction_inbox_counter': 0 }]) + na_count = emp_partner.get_needaction_count() + self.assertEqual(na_count, 0, "mark all as read should conclude all needactions") + + # mark all as read also clear inaccessible needactions + msg2 = group_private.message_post(body='Zest', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[emp_partner.id]) + needaction_accessible = len(emp_partner.env['mail.message'].search([['needaction', '=', True]])) + self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner") + + msg2.sudo().partner_ids = self.env['res.partner'] + emp_partner.env['mail.message'].search([['needaction', '=', True]]) + needaction_length = len(emp_partner.env['mail.message'].search([['needaction', '=', True]])) + self.assertEqual(needaction_length, 1, "message should still be readable when notified") + + na_count = emp_partner.get_needaction_count() + self.assertEqual(na_count, 1, "message not accessible is currently still counted") + + self._reset_bus() + emp_partner.env['mail.message'].mark_all_as_read(domain=[]) + self.assertBusNotifications([(self.cr.dbname, 'res.partner', emp_partner.id)], [{ 'type': 'mark_as_read', 'message_ids': [msg2.id], 'needaction_inbox_counter': 0 }]) + na_count = emp_partner.get_needaction_count() + self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones") + + @mute_logger('openerp.addons.mail.models.mail_mail') + def test_mark_all_as_read_share(self): + self.user_portal.notification_type = 'inbox' + portal_partner = self.user_portal.partner_id.with_user(self.user_portal) + + # mark all as read clear needactions + self.group_pigs.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[portal_partner.id]) + portal_partner.env['mail.message'].mark_all_as_read(domain=[]) + na_count = portal_partner.get_needaction_count() + self.assertEqual(na_count, 0, "mark all as read should conclude all needactions") + + # mark all as read also clear inaccessible needactions + new_msg = self.group_pigs.message_post(body='Zest', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[portal_partner.id]) + needaction_accessible = len(portal_partner.env['mail.message'].search([['needaction', '=', True]])) + self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner") + + new_msg.sudo().partner_ids = self.env['res.partner'] + needaction_length = len(portal_partner.env['mail.message'].search([['needaction', '=', True]])) + self.assertEqual(needaction_length, 1, "message should still be readable when notified") + + na_count = portal_partner.get_needaction_count() + self.assertEqual(na_count, 1, "message not accessible is currently still counted") + + portal_partner.env['mail.message'].mark_all_as_read(domain=[]) + na_count = portal_partner.get_needaction_count() + self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones") + + +@tagged('moderation') +class TestMessageModeration(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestMessageModeration, cls).setUpClass() + + cls.channel_1 = cls.env['mail.channel'].create({ + 'name': 'Moderation_1', + 'email_send': True, + 'moderation': True + }) + cls.user_employee.write({'moderation_channel_ids': [(6, 0, [cls.channel_1.id])]}) + cls.user_portal = cls._create_portal_user() + + # A pending moderation message needs to have field channel_ids empty. Moderators + # need to be able to notify a pending moderation message (in a channel they moderate). + cls.msg_c1_admin1 = cls._add_messages(cls.channel_1, 'Body11', author=cls.partner_admin, moderation_status='pending_moderation') + cls.msg_c1_admin2 = cls._add_messages(cls.channel_1, 'Body12', author=cls.partner_admin, moderation_status='pending_moderation') + cls.msg_c1_portal = cls._add_messages(cls.channel_1, 'Body21', author=cls.partner_portal, moderation_status='pending_moderation') + + @mute_logger('odoo.models.unlink') + def test_moderate_accept(self): + self._reset_bus() + self.assertFalse(self.msg_c1_admin1.channel_ids | self.msg_c1_admin2.channel_ids | self.msg_c1_portal.channel_ids) + + self.msg_c1_admin1.with_user(self.user_employee)._moderate('accept') + self.assertEqual(self.msg_c1_admin1.channel_ids, self.channel_1) + self.assertEqual(self.msg_c1_admin1.moderation_status, 'accepted') + self.assertEqual(self.msg_c1_admin2.moderation_status, 'pending_moderation') + self.assertBusNotifications([(self.cr.dbname, 'mail.channel', self.channel_1.id)]) + + @mute_logger('odoo.models.unlink') + def test_moderate_allow(self): + self._reset_bus() + + self.msg_c1_admin1.with_user(self.user_employee)._moderate('allow') + self.assertEqual(self.msg_c1_admin1.channel_ids, self.channel_1) + self.assertEqual(self.msg_c1_admin2.channel_ids, self.channel_1) + self.assertEqual(self.msg_c1_admin1.moderation_status, 'accepted') + self.assertEqual(self.msg_c1_admin2.moderation_status, 'accepted') + self.assertBusNotifications([ + (self.cr.dbname, 'mail.channel', self.channel_1.id), + (self.cr.dbname, 'mail.channel', self.channel_1.id)]) + + @mute_logger('odoo.models.unlink') + def test_moderate_reject(self): + with self.mock_mail_gateway(): + (self.msg_c1_admin1 | self.msg_c1_portal).with_user(self.user_employee)._moderate_send_reject_email('Title', 'Message to author') + self.assertEqual(len(self._new_mails), 2) + for mail in self._new_mails: + self.assertEqual(mail.author_id, self.partner_employee) + self.assertEqual(mail.subject, 'Title') + self.assertEqual(mail.state, 'outgoing') + self.assertEqual( + set(self._new_mails.mapped('email_to')), + set([self.msg_c1_admin1.email_from, self.msg_c1_portal.email_from]) + ) + self.assertEqual( + set(self._new_mails.mapped('body_html')), + set(['<div>Message to author</div>\n%s\n' % self.msg_c1_admin1.body, '<div>Message to author</div>\n%s\n' % self.msg_c1_portal.body]) + ) # TDE note: \n are added by append content to html, because why not + + @mute_logger('odoo.models.unlink') + def test_moderate_discard(self): + self._reset_bus() + id1, id2, id3 = self.msg_c1_admin1.id, self.msg_c1_admin2.id, self.msg_c1_portal.id # save ids because unlink will discard them + (self.msg_c1_admin1 | self.msg_c1_admin2 | self.msg_c1_portal).with_user(self.user_employee)._moderate_discard() + + self.assertBusNotifications( + [(self.cr.dbname, 'res.partner', self.partner_admin.id), + (self.cr.dbname, 'res.partner', self.partner_employee.id), + (self.cr.dbname, 'res.partner', self.partner_portal.id)], + [{'type': 'deletion', 'message_ids': [id1, id2]}, # author of 2 messages + {'type': 'deletion', 'message_ids': [id1, id2, id3]}, # moderator + {'type': 'deletion', 'message_ids': [id3]}] # author of 1 message + ) + + @mute_logger('odoo.models.unlink') + def test_notify_moderators(self): + # create pending messages in another channel to have two notification to push + channel_2 = self.env['mail.channel'].create({ + 'name': 'Moderation_1', + 'email_send': True, + 'moderation': True + }) + self.user_admin.write({'moderation_channel_ids': [(6, 0, [channel_2.id])]}) + self.msg_c2_portal = self._add_messages(channel_2, 'Body31', author=self.partner_portal, moderation_status='pending_moderation') + + # one notification for each moderator: employee (channel1), admin (channel2) + with self.assertPostNotifications([{ + 'content': 'Hello %s' % self.partner_employee.name, + 'message_type': 'user_notification', 'subtype': 'mail.mt_note', + 'notif': [{ + 'partner': self.partner_employee, + 'type': 'inbox'}] + }, { + 'content': 'Hello %s' % self.partner_admin.name, + 'message_type': 'user_notification', 'subtype': 'mail.mt_note', + 'notif': [{ + 'partner': self.partner_admin, + 'type': 'inbox'}] + }]): + self.env['mail.message']._notify_moderators() diff --git a/addons/test_mail/tests/test_mail_template.py b/addons/test_mail/tests/test_mail_template.py new file mode 100644 index 00000000..5bf72f3d --- /dev/null +++ b/addons/test_mail/tests/test_mail_template.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 + +from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients +from odoo.tools import mute_logger + + +class TestMailTemplate(TestMailCommon, TestRecipients): + + @classmethod + def setUpClass(cls): + super(TestMailTemplate, cls).setUpClass() + cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + + cls.user_employee.write({ + 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], + }) + + cls._attachments = [{ + 'name': 'first.txt', + 'datas': base64.b64encode(b'My first attachment'), + 'res_model': 'res.partner', + 'res_id': cls.user_admin.partner_id.id + }, { + 'name': 'second.txt', + 'datas': base64.b64encode(b'My second attachment'), + 'res_model': 'res.partner', + 'res_id': cls.user_admin.partner_id.id + }] + + cls.email_1 = 'test1@example.com' + cls.email_2 = 'test2@example.com' + cls.email_3 = cls.partner_1.email + cls._create_template('mail.test.simple', { + 'attachment_ids': [(0, 0, cls._attachments[0]), (0, 0, cls._attachments[1])], + 'partner_to': '%s,%s' % (cls.partner_2.id, cls.user_admin.partner_id.id), + 'email_to': '%s, %s' % (cls.email_1, cls.email_2), + 'email_cc': '%s' % cls.email_3, + }) + + # admin should receive emails + cls.user_admin.write({'notification_type': 'email'}) + # Force the attachments of the template to be in the natural order. + cls.email_template.invalidate_cache(['attachment_ids'], ids=cls.email_template.ids) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_template_send_email(self): + mail_id = self.email_template.send_mail(self.test_record.id) + mail = self.env['mail.mail'].sudo().browse(mail_id) + self.assertEqual(mail.subject, 'About %s' % self.test_record.name) + self.assertEqual(mail.email_to, self.email_template.email_to) + self.assertEqual(mail.email_cc, self.email_template.email_cc) + self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_template_translation(self): + self.env['res.lang']._activate_lang('es_ES') + self.env.ref('base.module_base')._update_translations(['es_ES']) + + partner = self.env['res.partner'].create({'name': "test", 'lang': 'es_ES'}) + email_template = self.env['mail.template'].create({ + 'name': 'TestTemplate', + 'subject': 'English Subject', + 'body_html': '<p>English Body</p>', + 'model_id': self.env['ir.model']._get(partner._name).id, + 'lang': '${object.lang}' + }) + # Make sure Spanish translations have not been altered + description_translations = self.env['ir.translation'].search([('module', '=', 'base'), ('src', '=', partner._description), ('lang', '=', 'es_ES')]) + description_translations.update({'value': 'Spanish description'}) + + self.env['ir.translation'].create({ + 'type': 'model', + 'name': 'mail.template,subject', + 'module': 'mail', + 'lang': 'es_ES', + 'res_id': email_template.id, + 'value': 'Spanish Subject', + 'state': 'translated', + }) + self.env['ir.translation'].create({ + 'type': 'model', + 'name': 'mail.template,body_html', + 'module': 'mail', + 'lang': 'es_ES', + 'res_id': email_template.id, + 'value': '<p>Spanish Body</p>', + 'state': 'translated', + }) + view = self.env['ir.ui.view'].create({ + 'name': 'test_layout', + 'key': 'test_layout', + 'type': 'qweb', + 'arch_db': '<body><t t-raw="message.body"/> English Layout <t t-esc="model_description"/></body>' + }) + self.env['ir.model.data'].create({ + 'name': 'test_layout', + 'module': 'test_mail', + 'model': 'ir.ui.view', + 'res_id': view.id + }) + self.env['ir.translation'].create({ + 'type': 'model_terms', + 'name': 'ir.ui.view,arch_db', + 'module': 'test_mail', + 'lang': 'es_ES', + 'res_id': view.id, + 'src': 'English Layout', + 'value': 'Spanish Layout', + 'state': 'translated', + }) + + mail_id = email_template.send_mail(partner.id, notif_layout='test_mail.test_layout') + mail = self.env['mail.mail'].sudo().browse(mail_id) + self.assertEqual(mail.subject, 'Spanish Subject') + self.assertEqual(mail.body_html, '<body><p>Spanish Body</p> Spanish Layout Spanish description</body>') + + def test_template_add_context_action(self): + self.email_template.create_action() + + # check template act_window has been updated + self.assertTrue(bool(self.email_template.ref_ir_act_window)) + + # check those records + action = self.email_template.ref_ir_act_window + self.assertEqual(action.name, 'Send Mail (%s)' % self.email_template.name) + self.assertEqual(action.binding_model_id.model, 'mail.test.simple') + + # def test_template_scheduled_date(self): + # from unittest.mock import patch + + # self.email_template_in_2_days = self.email_template.copy() + + # with patch('odoo.addons.mail.tests.test_mail_template.datetime', wraps=datetime) as mock_datetime: + # mock_datetime.now.return_value = datetime(2017, 11, 15, 11, 30, 28) + # mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw) + + # self.email_template_in_2_days.write({ + # 'scheduled_date': "${(datetime.datetime.now() + relativedelta(days=2)).strftime('%s')}" % DEFAULT_SERVER_DATETIME_FORMAT, + # }) + + # mail_now_id = self.email_template.send_mail(self.test_record.id) + # mail_in_2_days_id = self.email_template_in_2_days.send_mail(self.test_record.id) + + # mail_now = self.env['mail.mail'].browse(mail_now_id) + # mail_in_2_days = self.env['mail.mail'].browse(mail_in_2_days_id) + + # # mail preparation + # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_now | mail_in_2_days) + # self.assertEqual(bool(mail_now.scheduled_date), False) + # self.assertEqual(mail_now.state, 'outgoing') + # self.assertEqual(mail_in_2_days.state, 'outgoing') + # scheduled_date = datetime.strptime(mail_in_2_days.scheduled_date, DEFAULT_SERVER_DATETIME_FORMAT) + # date_in_2_days = datetime.now() + timedelta(days = 2) + # self.assertEqual(scheduled_date, date_in_2_days) + # # self.assertEqual(scheduled_date.month, date_in_2_days.month) + # # self.assertEqual(scheduled_date.year, date_in_2_days.year) + + # # Launch the scheduler on the first mail, it should be reported in self.mails + # # and the mail_mail is now deleted + # self.env['mail.mail'].process_email_queue() + # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_in_2_days) + + # # Launch the scheduler on the first mail, it's still in 'outgoing' state + # self.env['mail.mail'].process_email_queue(ids=[mail_in_2_days.id]) + # self.assertEqual(mail_in_2_days.state, 'outgoing') + # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_in_2_days) diff --git a/addons/test_mail/tests/test_mail_thread_internals.py b/addons/test_mail/tests/test_mail_thread_internals.py new file mode 100644 index 00000000..afdf2522 --- /dev/null +++ b/addons/test_mail/tests/test_mail_thread_internals.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from werkzeug.urls import url_parse, url_decode + +from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients +from odoo.tests.common import tagged, HttpCase +from odoo.tools import mute_logger + + +class TestChatterTweaks(TestMailCommon, TestRecipients): + + @classmethod + def setUpClass(cls): + super(TestChatterTweaks, cls).setUpClass() + cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + + def test_post_no_subscribe_author(self): + original = self.test_record.message_follower_ids + self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True}).message_post( + body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment') + self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id')) + self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id')) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_post_no_subscribe_recipients(self): + original = self.test_record.message_follower_ids + self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True}).message_post( + body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id]) + self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id')) + self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id')) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_post_subscribe_recipients(self): + original = self.test_record.message_follower_ids + self.test_record.with_user(self.user_employee).with_context({'mail_create_nosubscribe': True, 'mail_post_autofollow': True}).message_post( + body='Test Body', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[self.partner_1.id, self.partner_2.id]) + self.assertEqual(self.test_record.message_follower_ids.mapped('partner_id'), original.mapped('partner_id') | self.partner_1 | self.partner_2) + self.assertEqual(self.test_record.message_follower_ids.mapped('channel_id'), original.mapped('channel_id')) + + def test_chatter_mail_create_nolog(self): + """ Test disable of automatic chatter message at create """ + rec = self.env['mail.test.simple'].with_user(self.user_employee).with_context({'mail_create_nolog': True}).create({'name': 'Test'}) + self.flush_tracking() + self.assertEqual(rec.message_ids, self.env['mail.message']) + + rec = self.env['mail.test.simple'].with_user(self.user_employee).with_context({'mail_create_nolog': False}).create({'name': 'Test'}) + self.flush_tracking() + self.assertEqual(len(rec.message_ids), 1) + + def test_chatter_mail_notrack(self): + """ Test disable of automatic value tracking at create and write """ + rec = self.env['mail.test.track'].with_user(self.user_employee).create({'name': 'Test', 'user_id': self.user_employee.id}) + self.flush_tracking() + self.assertEqual(len(rec.message_ids), 1, + "A creation message without tracking values should have been posted") + self.assertEqual(len(rec.message_ids.sudo().tracking_value_ids), 0, + "A creation message without tracking values should have been posted") + + rec.with_context({'mail_notrack': True}).write({'user_id': self.user_admin.id}) + self.flush_tracking() + self.assertEqual(len(rec.message_ids), 1, + "No new message should have been posted with mail_notrack key") + + rec.with_context({'mail_notrack': False}).write({'user_id': self.user_employee.id}) + self.flush_tracking() + self.assertEqual(len(rec.message_ids), 2, + "A tracking message should have been posted") + self.assertEqual(len(rec.message_ids.sudo().mapped('tracking_value_ids')), 1, + "New tracking message should have tracking values") + + def test_chatter_tracking_disable(self): + """ Test disable of all chatter features at create and write """ + rec = self.env['mail.test.track'].with_user(self.user_employee).with_context({'tracking_disable': True}).create({'name': 'Test', 'user_id': self.user_employee.id}) + self.flush_tracking() + self.assertEqual(rec.sudo().message_ids, self.env['mail.message']) + self.assertEqual(rec.sudo().mapped('message_ids.tracking_value_ids'), self.env['mail.tracking.value']) + + rec.write({'user_id': self.user_admin.id}) + self.flush_tracking() + self.assertEqual(rec.sudo().mapped('message_ids.tracking_value_ids'), self.env['mail.tracking.value']) + + rec.with_context({'tracking_disable': False}).write({'user_id': self.user_employee.id}) + self.flush_tracking() + self.assertEqual(len(rec.sudo().mapped('message_ids.tracking_value_ids')), 1) + + rec = self.env['mail.test.track'].with_user(self.user_employee).with_context({'tracking_disable': False}).create({'name': 'Test', 'user_id': self.user_employee.id}) + self.flush_tracking() + self.assertEqual(len(rec.sudo().message_ids), 1, + "Creation message without tracking values should have been posted") + self.assertEqual(len(rec.sudo().mapped('message_ids.tracking_value_ids')), 0, + "Creation message without tracking values should have been posted") + + def test_cache_invalidation(self): + """ Test that creating a mail-thread record does not invalidate the whole cache. """ + # make a new record in cache + record = self.env['res.partner'].new({'name': 'Brave New Partner'}) + self.assertTrue(record.name) + + # creating a mail-thread record should not invalidate the whole cache + self.env['res.partner'].create({'name': 'Actual Partner'}) + self.assertTrue(record.name) + + +class TestDiscuss(TestMailCommon, TestRecipients): + + @classmethod + def setUpClass(cls): + super(TestDiscuss, cls).setUpClass() + cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + + def test_set_message_done_user(self): + with self.assertSinglePostNotifications([{'partner': self.partner_employee, 'type': 'inbox'}], message_info={'content': 'Test'}): + message = self.test_record.message_post( + body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', + partner_ids=[self.user_employee.partner_id.id]) + message.with_user(self.user_employee).set_message_done() + self.assertMailNotifications(message, [{'notif': [{'partner': self.partner_employee, 'type': 'inbox', 'is_read': True}]}]) + # TDE TODO: it seems bus notifications could be checked + + def test_set_star(self): + msg = self.test_record.with_user(self.user_admin).message_post(body='My Body', subject='1') + msg_emp = self.env['mail.message'].with_user(self.user_employee).browse(msg.id) + + # Admin set as starred + msg.toggle_message_starred() + self.assertTrue(msg.starred) + + # Employee set as starred + msg_emp.toggle_message_starred() + self.assertTrue(msg_emp.starred) + + # Do: Admin unstars msg + msg.toggle_message_starred() + self.assertFalse(msg.starred) + self.assertTrue(msg_emp.starred) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_cc_recipient_suggestion(self): + record = self.env['mail.test.cc'].create({'email_cc': 'cc1@example.com, cc2@example.com, cc3 <cc3@example.com>'}) + suggestions = record._message_get_suggested_recipients()[record.id] + self.assertEqual(sorted(suggestions), [ + (False, '"cc3" <cc3@example.com>', 'CC Email'), + (False, 'cc1@example.com', 'CC Email'), + (False, 'cc2@example.com', 'CC Email'), + ], 'cc should be in suggestions') + + def test_inbox_message_fetch_needaction(self): + user1 = self.env['res.users'].create({'login': 'user1', 'name': 'User 1'}) + user1.notification_type = 'inbox' + user2 = self.env['res.users'].create({'login': 'user2', 'name': 'User 2'}) + user2.notification_type = 'inbox' + message1 = self.test_record.with_user(self.user_admin).message_post(body='Message 1', partner_ids=[user1.partner_id.id, user2.partner_id.id]) + message2 = self.test_record.with_user(self.user_admin).message_post(body='Message 2', partner_ids=[user1.partner_id.id, user2.partner_id.id]) + + # both notified users should have the 2 messages in Inbox initially + messages = self.env['mail.message'].with_user(user1).message_fetch(domain=[['needaction', '=', True]]) + self.assertEqual(len(messages), 2) + messages = self.env['mail.message'].with_user(user2).message_fetch(domain=[['needaction', '=', True]]) + self.assertEqual(len(messages), 2) + + # first user is marking one message as done: the other message is still Inbox, while the other user still has the 2 messages in Inbox + message1.with_user(user1).set_message_done() + messages = self.env['mail.message'].with_user(user1).message_fetch(domain=[['needaction', '=', True]]) + self.assertEqual(len(messages), 1) + self.assertEqual(messages[0].get('id'), message2.id) + messages = self.env['mail.message'].with_user(user2).message_fetch(domain=[['needaction', '=', True]]) + self.assertEqual(len(messages), 2) + + def test_notification_has_error_filter(self): + """Ensure message_has_error filter is only returning threads for which + the current user is author of a failed message.""" + message = self.test_record.with_user(self.user_admin).message_post( + body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', + partner_ids=[self.user_employee.partner_id.id] + ) + self.assertFalse(message.has_error) + with self.mock_mail_gateway(sim_error='connect_smtp_notfound'): + 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', + partner_ids=[self.user_admin.partner_id.id] + ) + self.assertTrue(message2.has_error) + # employee is author of message which has a failure + threads_employee = self.test_record.with_user(self.user_employee).search([('message_has_error', '=', True)]) + self.assertEqual(len(threads_employee), 1) + # admin is also author of a message, but it doesn't have a failure + # and the failure from employee's message should not be taken into account for admin + threads_admin = self.test_record.with_user(self.user_admin).search([('message_has_error', '=', True)]) + self.assertEqual(len(threads_admin), 0) + + +@tagged('-at_install', 'post_install') +class TestMultiCompany(HttpCase): + + def test_redirect_to_records(self): + + self.company_A = self.env['res.company'].create({ + 'name': 'Company A', + 'user_ids': [(4, self.ref('base.user_admin'))], + }) + + self.company_B = self.env['res.company'].create({ + 'name': 'Company B', + }) + + self.multi_company_record = self.env['mail.test.multi.company'].create({ + 'name': 'Multi Company Record', + 'company_id': self.company_A.id, + }) + + # Test Case 0 + # Not logged, redirect to web/login + response = self.url_open('/mail/view?model=%s&res_id=%s' % ( + self.multi_company_record._name, + self.multi_company_record.id), timeout=15) + + path = url_parse(response.url).path + self.assertEqual(path, '/web/login') + + self.authenticate('admin', 'admin') + + # Test Case 1 + # Logged into company 1, try accessing record in company A + # _redirect_to_record should add company A in allowed_company_ids + response = self.url_open('/mail/view?model=%s&res_id=%s' % ( + self.multi_company_record._name, + self.multi_company_record.id), timeout=15) + + self.assertEqual(response.status_code, 200) + + fragment = url_parse(response.url).fragment + cids = url_decode(fragment)['cids'] + + self.assertEqual(cids, '1,%s' % (self.company_A.id)) + + # Test Case 2 + # Logged into company 1, try accessing record in company B + # _redirect_to_record should redirect to messaging as the user + # doesn't have any access for this company + self.multi_company_record.company_id = self.company_B + + response = self.url_open('/mail/view?model=%s&res_id=%s' % ( + self.multi_company_record._name, + self.multi_company_record.id), timeout=15) + + self.assertEqual(response.status_code, 200) + + fragment = url_parse(response.url).fragment + action = url_decode(fragment)['action'] + + self.assertEqual(action, 'mail.action_discuss') diff --git a/addons/test_mail/tests/test_mail_tools.py b/addons/test_mail/tests/test_mail_tools.py new file mode 100644 index 00000000..b36a1335 --- /dev/null +++ b/addons/test_mail/tests/test_mail_tools.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients +from odoo.tests import tagged, users + + +@tagged('mail_tools') +class TestMailTools(TestMailCommon, TestRecipients): + + @classmethod + def setUpClass(cls): + super(TestMailTools, cls).setUpClass() + + cls._test_email = 'alfredoastaire@test.example.com' + cls.test_partner = cls.env['res.partner'].create({ + 'country_id': cls.env.ref('base.be').id, + 'email': cls._test_email, + 'mobile': '0456001122', + 'name': 'Alfred Astaire', + 'phone': '0456334455', + }) + + @users('employee') + def test_find_partner_from_emails(self): + Partner = self.env['res.partner'] + test_partner = Partner.browse(self.test_partner.ids) + self.assertEqual(test_partner.email, self._test_email) + + # test direct match + found = Partner._mail_find_partner_from_emails([self._test_email]) + self.assertEqual(found, [test_partner]) + + # test encapsulated email + found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email]) + self.assertEqual(found, [test_partner]) + + # test with wildcard "_" + found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com']) + self.assertEqual(found, [self.env['res.partner']]) + + # sub-check: this search does not consider _ as a wildcard + found = Partner._mail_search_on_partner(['alfred_astaire@test.example.com']) + self.assertEqual(found, self.env['res.partner']) + + # test partners with encapsulated emails + # ------------------------------------------------------------ + test_partner.sudo().write({'email': '"Alfred Mighty Power Astaire" <%s>' % self._test_email}) + + # test direct match + found = Partner._mail_find_partner_from_emails([self._test_email]) + self.assertEqual(found, [test_partner]) + + # test encapsulated email + found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email]) + self.assertEqual(found, [test_partner]) + + # test with wildcard "_" + found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com']) + self.assertEqual(found, [self.env['res.partner']]) + + # sub-check: this search does not consider _ as a wildcard + found = Partner._mail_search_on_partner(['alfred_astaire@test.example.com']) + self.assertEqual(found, self.env['res.partner']) + + # test partners with look-alike emails + # ------------------------------------------------------------ + for email_lookalike in [ + 'alfred.astaire@test.example.com', + 'alfredoastaire@example.com', + 'aalfredoastaire@test.example.com', + 'alfredoastaire@test.example.comm']: + test_partner.sudo().write({'email': '"Alfred Astaire" <%s>' % email_lookalike}) + + # test direct match + found = Partner._mail_find_partner_from_emails([self._test_email]) + self.assertEqual(found, [self.env['res.partner']]) + # test encapsulated email + found = Partner._mail_find_partner_from_emails(['"Norbert Poiluchette" <%s>' % self._test_email]) + self.assertEqual(found, [self.env['res.partner']]) + # test with wildcard "_" + found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com']) + self.assertEqual(found, [self.env['res.partner']]) diff --git a/addons/test_mail/tests/test_message_management.py b/addons/test_mail/tests/test_message_management.py new file mode 100644 index 00000000..289b3681 --- /dev/null +++ b/addons/test_mail/tests/test_message_management.py @@ -0,0 +1,120 @@ +# -*- 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.common import TestMailCommon +from odoo.tests import tagged +from odoo.tools import mute_logger + + +@tagged('mail_wizards') +class TestMailResend(TestMailCommon): + + @classmethod + def setUpClass(cls): + super(TestMailResend, cls).setUpClass() + cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + + #Two users + cls.user1 = mail_new_test_user(cls.env, login='e1', groups='base.group_public', name='Employee 1', notification_type='email', email='e1') # invalid email + cls.user2 = mail_new_test_user(cls.env, login='e2', groups='base.group_portal', name='Employee 2', notification_type='email', email='e2@example.com') + #Two partner + cls.partner1 = cls.env['res.partner'].with_context(cls._test_context).create({ + 'name': 'Partner 1', + 'email': 'p1' # invalid email + }) + cls.partner2 = cls.env['res.partner'].with_context(cls._test_context).create({ + 'name': 'Partner 2', + 'email': 'p2@example.com' + }) + cls.partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.user2.partner_id, cls.partner1, cls.partner2) + cls.invalid_email_partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.partner1) + + # @mute_logger('odoo.addons.mail.models.mail_mail') + 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 = 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({}) + self.assertEqual(wizard.notification_ids.mapped('res_partner_id'), self.partners, "wizard should manage notifications for each failed partner") + + # three more failure sent on bus, one for each mail in failure and one for resend + self._reset_bus() + with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 3): + wizard.resend_mail_action() + done_msgs, done_notifs = self.assertMailNotifications(message, [ + {'content': '', 'message_type': 'notification', + 'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}] + ) + self.assertEqual(wizard.notification_ids, done_notifs) + self.assertEqual(done_msgs, message) + + self.user1.write({"email": 'u1@example.com'}) + + # two more failure update sent on bus, one for failed mail and one for resend + self._reset_bus() + with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 2): + self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action() + done_msgs, done_notifs = self.assertMailNotifications(message, [ + {'content': '', 'message_type': 'notification', + 'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner == self.partner1 else 'sent', 'check_send': partner == self.partner1} for partner in self.partners]}] + ) + self.assertEqual(wizard.notification_ids, done_notifs) + self.assertEqual(done_msgs, message) + + self.partner1.write({"email": 'p1@example.com'}) + + # A success update should be sent on bus once the email has no more failure + self._reset_bus() + with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)]): + self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action() + self.assertMailNotifications(message, [ + {'content': '', 'message_type': 'notification', + 'notif': [{'partner': partner, 'type': 'email', 'status': 'sent', 'check_send': partner == self.partner1} for partner in self.partners]}] + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_remove_mail_become_canceled(self): + # two failure sent on bus, one for each mail + self._reset_bus() + with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 2): + message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification') + + self.assertMailNotifications(message, [ + {'content': '', 'message_type': 'notification', + 'notif': [{'partner': partner, 'type': 'email', 'status': 'exception' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}] + ) + + wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}) + partners = wizard.partner_ids.mapped("partner_id") + self.assertEqual(self.invalid_email_partners, partners) + wizard.partner_ids.filtered(lambda p: p.partner_id == self.partner1).write({"resend": False}) + wizard.resend_mail_action() + + self.assertMailNotifications(message, [ + {'content': '', 'message_type': 'notification', + 'notif': [{'partner': partner, 'type': 'email', + 'status': (partner == self.user1.partner_id and 'exception') or (partner == self.partner1 and 'canceled') or 'sent'} for partner in self.partners]}] + ) + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_cancel_all(self): + self._reset_bus() + with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 2): + 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({}) + # one update for cancell + self._reset_bus() + with self.mock_mail_gateway(), self.assertBus([(self.cr.dbname, 'res.partner', self.partner_admin.id)] * 1): + wizard.cancel_mail_action() + + self.assertMailNotifications(message, [ + {'content': '', 'message_type': 'notification', + 'notif': [{'partner': partner, 'type': 'email', + 'check_send': partner in self.user1.partner_id | self.partner1, + 'status': 'canceled' if partner in self.user1.partner_id | self.partner1 else 'sent'} for partner in self.partners]}] + ) diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py new file mode 100644 index 00000000..edf052d7 --- /dev/null +++ b/addons/test_mail/tests/test_message_post.py @@ -0,0 +1,397 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 + +from unittest.mock import patch + +from odoo.addons.test_mail.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 +from odoo.api import call_kw +from odoo.exceptions import AccessError +from odoo.tests import tagged +from odoo.tools import mute_logger, formataddr +from odoo.tests.common import users + + +@tagged('mail_post') +class TestMessagePost(TestMailCommon, TestRecipients): + + @classmethod + def setUpClass(cls): + super(TestMessagePost, cls).setUpClass() + cls._create_portal_user() + cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + cls._reset_mail_context(cls.test_record) + cls.user_admin.write({'notification_type': 'email'}) + + # This method should be run inside a post_install class to ensure that all + # message_post overrides are tested. + def test_message_post_return(self): + test_channel = self.env['mail.channel'].create({ + 'name': 'Test', + }) + # Use call_kw as shortcut to simulate a RPC call. + messageId = call_kw(self.env['mail.channel'], 'message_post', [test_channel.id], {'body': 'test'}) + self.assertTrue(isinstance(messageId, int)) + + @users('employee') + def test_notify_prepare_template_context_company_value(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. + """ + current_user = self.env.user + main_company = current_user.company_id + other_company = self.env['res.company'].with_user(self.user_admin).create({'name': 'Company B'}) + current_user.sudo().write({'company_ids': [(4, other_company.id)]}) + test_record = self.env['mail.test.multi.company'].with_user(self.user_admin).create({ + 'name': 'Multi Company Record', + 'company_id': False, + }) + + # self.env.company.id = Main Company AND test_record.company_id = False + self.assertEqual(self.env.company.id, main_company.id) + self.assertEqual(test_record.company_id.id, False) + template_values = test_record._notify_prepare_template_context(test_record.message_ids, {}) + self.assertEqual(template_values.get('company').id, self.env.company.id) + + # self.env.company.id = Other Company AND test_record.company_id = False + current_user.company_id = other_company + test_record = self.env['mail.test.multi.company'].browse(test_record.id) + self.assertEqual(self.env.company.id, other_company.id) + self.assertEqual(test_record.company_id.id, False) + template_values = test_record._notify_prepare_template_context(test_record.message_ids, {}) + self.assertEqual(template_values.get('company').id, self.env.company.id) + + # self.env.company.id = Other Company AND test_record.company_id = Main Company + test_record.company_id = main_company + test_record = self.env['mail.test.multi.company'].browse(test_record.id) + self.assertEqual(self.env.company.id, other_company.id) + self.assertEqual(test_record.company_id.id, main_company.id) + template_values = test_record._notify_prepare_template_context(test_record.message_ids, {}) + self.assertEqual(template_values.get('company').id, main_company.id) + + def test_notify_recipients_internals(self): + pdata = self._generate_notify_recipients(self.partner_1 | self.partner_employee) + msg_vals = { + 'body': 'Message body', + 'model': self.test_record._name, + 'res_id': self.test_record.id, + 'subject': 'Message subject', + } + link_vals = { + 'token': 'token_val', + 'access_token': 'access_token_val', + 'auth_signup_token': 'auth_signup_token_val', + 'auth_login': 'auth_login_val', + } + notify_msg_vals = dict(msg_vals, **link_vals) + classify_res = self.env[self.test_record._name]._notify_classify_recipients(pdata, 'My Custom Model Name', msg_vals=notify_msg_vals) + # find back information for each recipients + partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids) + emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids) + + # partner: no access button + self.assertFalse(partner_info['has_button_access']) + + # employee: access button and link + self.assertTrue(emp_info['has_button_access']) + for param, value in link_vals.items(): + self.assertIn('%s=%s' % (param, value), emp_info['button_access']['url']) + self.assertIn('model=%s' % self.test_record._name, emp_info['button_access']['url']) + self.assertIn('res_id=%s' % self.test_record.id, emp_info['button_access']['url']) + self.assertNotIn('body', emp_info['button_access']['url']) + self.assertNotIn('subject', emp_info['button_access']['url']) + + @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 = '<p>Test Body</p>', '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) + self.assertEqual(msg.channel_ids, self.env['mail.channel']) + + # 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}) + + 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='<p>Test</p>', 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': '<p>Test Answer</p>', 'notif': [{'partner': self.partner_1, 'type': 'email'}]}]): + msg = self.test_record.with_user(self.user_employee).message_post( + body='<p>Test Answer</p>', + 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='<p>Test Answer Bis</p>', + 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_portal_ok(self): + self.test_record.message_subscribe((self.partner_1 | self.user_employee.partner_id).ids) + + with self.assertPostNotifications([{'content': '<p>Test</p>', '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='<p>Test</p>', 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='<p>Test</p>', 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='<p>Labrador</p>', + ) + + self.assertEqual(new_note.subtype_id, self.env.ref('mail.mt_note')) + self.assertEqual(new_note.body, '<p>Labrador</p>') + 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', + body='<p>You have received a notification</p>', + partner_ids=[self.partner_1.id, self.partner_admin.id, self.user_employee.partner_id.id], + ) + + 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, '<p>You have received a notification</p>') + 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.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]] + 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 + + 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 + + @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)], + }) + _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 + }] + 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])], + '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, + }) + # admin should receive emails + self.user_admin.write({'notification_type': 'email'}) + # Force the attachments of the template to be in the natural order. + self.email_template.invalidate_cache(['attachment_ids'], ids=self.email_template.ids) + + with self.mock_mail_gateway(): + test_record.with_user(self.user_employee).message_post_with_template(self.email_template.id, composition_mode='comment') + + new_partners = self.env['res.partner'].search([('email', 'in', [email_1, email_2])]) + for r in [self.partner_1, self.partner_2, new_partners[0], new_partners[1], self.partner_admin]: + self.assertSentEmail( + self.user_employee.partner_id, + [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')]) diff --git a/addons/test_mail/tests/test_message_track.py b/addons/test_mail/tests/test_message_track.py new file mode 100644 index 00000000..df024c1e --- /dev/null +++ b/addons/test_mail/tests/test_message_track.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + +from odoo.addons.test_mail.tests.common import TestMailCommon +from odoo.tests.common import tagged +from odoo.tests import Form + + +@tagged('mail_track') +class TestTracking(TestMailCommon): + + def setUp(self): + super(TestTracking, self).setUp() + + record = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context).create({ + 'name': 'Test', + }) + self.flush_tracking() + self.record = record.with_context(mail_notrack=False) + + def test_message_track_no_tracking(self): + """ Update a set of non tracked fields -> no message, no tracking """ + self.record.write({ + 'name': 'Tracking or not', + 'count': 32, + }) + self.flush_tracking() + self.assertEqual(self.record.message_ids, self.env['mail.message']) + + def test_message_track_no_subtype(self): + """ Update some tracked fields not linked to some subtype -> message with onchange """ + customer = self.env['res.partner'].create({'name': 'Customer', 'email': 'cust@example.com'}) + with self.mock_mail_gateway(): + self.record.write({ + 'name': 'Test2', + 'customer_id': customer.id, + }) + self.flush_tracking() + + # one new message containing tracking; without subtype linked to tracking, a note is generated + self.assertEqual(len(self.record.message_ids), 1) + self.assertEqual(self.record.message_ids.subtype_id, self.env.ref('mail.mt_note')) + + # no specific recipients except those following notes, no email + self.assertEqual(self.record.message_ids.partner_ids, self.env['res.partner']) + self.assertEqual(self.record.message_ids.notified_partner_ids, self.env['res.partner']) + self.assertNotSentEmail() + + # verify tracked value + self.assertTracking( + self.record.message_ids, + [('customer_id', 'many2one', False, customer) # onchange tracked field + ]) + + def test_message_track_subtype(self): + """ Update some tracked fields linked to some subtype -> message with onchange """ + self.record.message_subscribe( + partner_ids=[self.user_admin.partner_id.id], + subtype_ids=[self.env.ref('test_mail.st_mail_test_ticket_container_upd').id] + ) + + container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'}) + self.record.write({ + 'name': 'Test2', + 'email_from': 'noone@example.com', + 'container_id': container.id, + }) + self.flush_tracking() + # one new message containing tracking; subtype linked to tracking + self.assertEqual(len(self.record.message_ids), 1) + self.assertEqual(self.record.message_ids.subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd')) + + # no specific recipients except those following container + self.assertEqual(self.record.message_ids.partner_ids, self.env['res.partner']) + self.assertEqual(self.record.message_ids.notified_partner_ids, self.user_admin.partner_id) + + # verify tracked value + self.assertTracking( + self.record.message_ids, + [('container_id', 'many2one', False, container) # onchange tracked field + ]) + + def test_message_track_template(self): + """ Update some tracked fields linked to some template -> message with onchange """ + self.record.write({'mail_template': self.env.ref('test_mail.mail_test_ticket_tracking_tpl').id}) + self.assertEqual(self.record.message_ids, self.env['mail.message']) + + with self.mock_mail_gateway(): + self.record.write({ + 'name': 'Test2', + 'customer_id': self.user_admin.partner_id.id, + }) + self.flush_tracking() + + self.assertEqual(len(self.record.message_ids), 2, 'should have 2 new messages: one for tracking, one for template') + + # one new message containing the template linked to tracking + self.assertEqual(self.record.message_ids[0].subject, 'Test Template') + self.assertEqual(self.record.message_ids[0].body, '<p>Hello Test2</p>') + + # one email send due to template + self.assertSentEmail(self.record.env.user.partner_id, [self.partner_admin], body='<p>Hello Test2</p>') + + # one new message containing tracking; without subtype linked to tracking + self.assertEqual(self.record.message_ids[1].subtype_id, self.env.ref('mail.mt_note')) + self.assertTracking( + self.record.message_ids[1], + [('customer_id', 'many2one', False, self.user_admin.partner_id) # onchange tracked field + ]) + + def test_message_track_template_at_create(self): + """ Create a record with tracking template on create, template should be sent.""" + + Model = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context) + Model = Model.with_context(mail_notrack=False) + with self.mock_mail_gateway(): + record = Model.create({ + 'name': 'Test', + 'customer_id': self.user_admin.partner_id.id, + 'mail_template': self.env.ref('test_mail.mail_test_ticket_tracking_tpl').id, + }) + self.flush_tracking() + + self.assertEqual(len(record.message_ids), 1, 'should have 1 new messages for template') + # one new message containing the template linked to tracking + self.assertEqual(record.message_ids[0].subject, 'Test Template') + self.assertEqual(record.message_ids[0].body, '<p>Hello Test</p>') + # one email send due to template + self.assertSentEmail(self.record.env.user.partner_id, [self.partner_admin], body='<p>Hello Test</p>') + + def test_create_partner_from_tracking_multicompany(self): + company1 = self.env['res.company'].create({'name': 'company1'}) + self.env.user.write({'company_ids': [(4, company1.id, False)]}) + self.assertNotEqual(self.env.company, company1) + + email_new_partner = "diamonds@rust.com" + Partner = self.env['res.partner'] + self.assertFalse(Partner.search([('email', '=', email_new_partner)])) + + template = self.env['mail.template'].create({ + 'model_id': self.env['ir.model']._get('mail.test.track').id, + 'name': 'AutoTemplate', + 'subject': 'autoresponse', + 'email_from': self.env.user.email_formatted, + 'email_to': "${object.email_from}", + 'body_html': "<div>A nice body</div>", + }) + + def patched_message_track_post_template(*args, **kwargs): + if args[0]._name == "mail.test.track": + args[0].message_post_with_template(template.id) + return True + + with patch('odoo.addons.mail.models.mail_thread.MailThread._message_track_post_template', patched_message_track_post_template): + self.env['mail.test.track'].create({ + 'email_from': email_new_partner, + 'company_id': company1.id, + 'user_id': self.env.user.id, # trigger track template + }) + self.flush_tracking() + + new_partner = Partner.search([('email', '=', email_new_partner)]) + self.assertTrue(new_partner) + self.assertEqual(new_partner.company_id, company1) + + def test_track_template(self): + # Test: Check that default_* keys are not taken into account in _message_track_post_template + magic_code = 'Up-Up-Down-Down-Left-Right-Left-Right-Square-Triangle' + + mt_name_changed = self.env['mail.message.subtype'].create({ + 'name': 'MAGIC CODE WOOP WOOP', + 'description': 'SPECIAL CONTENT UNLOCKED' + }) + self.env['ir.model.data'].create({ + 'name': 'mt_name_changed', + 'model': 'mail.message.subtype', + 'module': 'mail', + 'res_id': mt_name_changed.id + }) + mail_template = self.env['mail.template'].create({ + 'name': 'SPECIAL CONTENT UNLOCKED', + 'subject': 'SPECIAL CONTENT UNLOCKED', + 'model_id': self.env.ref('test_mail.model_mail_test_container').id, + 'auto_delete': True, + 'body_html': '''<div>WOOP WOOP</div>''', + }) + + def _track_subtype(self, init_values): + if 'name' in init_values and init_values['name'] == magic_code: + return 'mail.mt_name_changed' + return False + self.registry('mail.test.container')._patch_method('_track_subtype', _track_subtype) + + def _track_template(self, changes): + res = {} + if 'name' in changes: + res['name'] = (mail_template, {'composition_mode': 'mass_mail'}) + return res + self.registry('mail.test.container')._patch_method('_track_template', _track_template) + + cls = type(self.env['mail.test.container']) + self.assertFalse(hasattr(getattr(cls, 'name'), 'track_visibility')) + getattr(cls, 'name').track_visibility = 'always' + + @self.addCleanup + def cleanup(): + del getattr(cls, 'name').track_visibility + + test_mail_record = self.env['mail.test.container'].create({ + 'name': 'Zizizatestmailname', + 'description': 'Zizizatestmaildescription', + }) + test_mail_record.with_context(default_parent_id=2147483647).write({'name': magic_code}) + + def test_message_track_multiple(self): + """ check that multiple updates generate a single tracking message """ + container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'}) + self.record.name = 'Zboub' + self.record.customer_id = self.user_admin.partner_id + self.record.user_id = self.user_admin + self.record.container_id = container + self.flush_tracking() + + # should have a single message with all tracked fields + self.assertEqual(len(self.record.message_ids), 1, 'should have 1 tracking message') + self.assertTracking(self.record.message_ids[0], [ + ('customer_id', 'many2one', False, self.user_admin.partner_id), + ('user_id', 'many2one', False, self.user_admin), + ('container_id', 'many2one', False, container), + ]) + + def test_tracked_compute(self): + # no tracking at creation + record = self.env['mail.test.track.compute'].create({}) + self.flush_tracking() + self.assertEqual(len(record.message_ids), 1) + self.assertEqual(len(record.message_ids[0].tracking_value_ids), 0) + + # assign partner_id: one tracking message for the modified field and all + # the stored and non-stored computed fields on the record + partner = self.env['res.partner'].create({ + 'name': 'Foo', + 'email': 'foo@example.com', + 'phone': '1234567890', + }) + record.partner_id = partner + self.flush_tracking() + self.assertEqual(len(record.message_ids), 2) + self.assertEqual(len(record.message_ids[0].tracking_value_ids), 4) + self.assertTracking(record.message_ids[0], [ + ('partner_id', 'many2one', False, partner), + ('partner_name', 'char', False, 'Foo'), + ('partner_email', 'char', False, 'foo@example.com'), + ('partner_phone', 'char', False, '1234567890'), + ]) + + # modify partner: one tracking message for the only recomputed field + partner.write({'name': 'Fool'}) + self.flush_tracking() + self.assertEqual(len(record.message_ids), 3) + self.assertEqual(len(record.message_ids[0].tracking_value_ids), 1) + self.assertTracking(record.message_ids[0], [ + ('partner_name', 'char', 'Foo', 'Fool'), + ]) + + # modify partner: one tracking message for both stored computed fields; + # the non-stored computed fields have no tracking + partner.write({ + 'name': 'Bar', + 'email': 'bar@example.com', + 'phone': '0987654321', + }) + # force recomputation of 'partner_phone' to make sure it does not + # generate tracking values + self.assertEqual(record.partner_phone, '0987654321') + self.flush_tracking() + self.assertEqual(len(record.message_ids), 4) + self.assertEqual(len(record.message_ids[0].tracking_value_ids), 2) + self.assertTracking(record.message_ids[0], [ + ('partner_name', 'char', 'Fool', 'Bar'), + ('partner_email', 'char', 'foo@example.com', 'bar@example.com'), + ]) + + +@tagged('mail_track') +class TestTrackingInternals(TestMailCommon): + + def setUp(self): + super(TestTrackingInternals, self).setUp() + + record = self.env['mail.test.ticket'].with_user(self.user_employee).with_context(self._test_context).create({ + 'name': 'Test', + }) + self.flush_tracking() + self.record = record.with_context(mail_notrack=False) + + def test_track_groups(self): + field = self.record._fields['email_from'] + self.addCleanup(setattr, field, 'groups', field.groups) + field.groups = 'base.group_erp_manager' + + self.record.sudo().write({'email_from': 'X'}) + self.flush_tracking() + + msg_emp = self.record.message_ids.message_format() + msg_sudo = self.record.sudo().message_ids.message_format() + self.assertFalse(msg_emp[0].get('tracking_value_ids'), "should not have protected tracking values") + self.assertTrue(msg_sudo[0].get('tracking_value_ids'), "should have protected tracking values") + + msg_emp = self.record._notify_prepare_template_context(self.record.message_ids, {}) + msg_sudo = self.record.sudo()._notify_prepare_template_context(self.record.message_ids, {}) + self.assertFalse(msg_emp.get('tracking_values'), "should not have protected tracking values") + self.assertTrue(msg_sudo.get('tracking_values'), "should have protected tracking values") + + # test editing the record with user not in the group of the field + self.record.invalidate_cache() + self.record.clear_caches() + record_form = Form(self.record.with_user(self.user_employee)) + record_form.name = 'TestDoNoCrash' + # the employee user must be able to save the fields on which he can write + # if we fetch all the tracked fields, ignoring the group of the current user + # it will crash and it shouldn't + record = record_form.save() + self.assertEqual(record.name, 'TestDoNoCrash') + + def test_track_sequence(self): + """ Update some tracked fields and check that the mail.tracking.value are ordered according to their tracking_sequence""" + self.record.write({ + 'name': 'Zboub', + 'customer_id': self.user_admin.partner_id.id, + 'user_id': self.user_admin.id, + 'container_id': self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({'name': 'Container'}).id + }) + self.flush_tracking() + self.assertEqual(len(self.record.message_ids), 1, 'should have 1 tracking message') + + tracking_values = self.env['mail.tracking.value'].search([('mail_message_id', '=', self.record.message_ids.id)]) + self.assertEqual(tracking_values[0].tracking_sequence, 1) + self.assertEqual(tracking_values[1].tracking_sequence, 2) + self.assertEqual(tracking_values[2].tracking_sequence, 100) + + def test_unlinked_field(self): + record_sudo = self.record.sudo() + record_sudo.write({'email_from': 'new_value'}) # create a tracking value + self.flush_tracking() + self.assertEqual(len(record_sudo.message_ids.tracking_value_ids), 1) + ir_model_field = self.env['ir.model.fields'].search([ + ('model', '=', 'mail.test.ticket'), + ('name', '=', 'email_from')]) + ir_model_field.with_context(_force_unlink=True).unlink() + self.assertEqual(len(record_sudo.message_ids.tracking_value_ids), 0) diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py new file mode 100644 index 00000000..6862160a --- /dev/null +++ b/addons/test_mail/tests/test_performance.py @@ -0,0 +1,1021 @@ +# -*- 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.tests.common import users, warmup +from odoo.tests import tagged +from odoo.tools import mute_logger, formataddr + + +@tagged('mail_performance') +class BaseMailPerformance(TransactionCaseWithUserDemo): + + def setUp(self): + super(BaseMailPerformance, self).setUp() + self._quick_create_ctx = { + 'no_reset_password': True, + 'mail_create_nolog': True, + 'mail_create_nosubscribe': True, + 'mail_notrack': True, + } + self.user_employee = self.env['res.users'].with_context(self._quick_create_ctx).create({ + 'name': 'Ernest Employee', + 'login': 'emp', + 'email': 'e.e@example.com', + 'signature': '--\nErnest', + 'notification_type': 'inbox', + 'groups_id': [(6, 0, [self.env.ref('base.group_user').id, self.env.ref('base.group_partner_manager').id])], + }) + + # patch registry to simulate a ready environment + self.patch(self.env.registry, 'ready', True) + + +@tagged('mail_performance') +class TestMailPerformance(BaseMailPerformance): + + def setUp(self): + super(TestMailPerformance, self).setUp() + + self.res_partner_3 = self.env['res.partner'].create({ + 'name': 'Gemini Furniture', + 'email': 'gemini.furniture39@example.com', + }) + self.res_partner_4 = self.env['res.partner'].create({ + 'name': 'Ready Mat', + 'email': 'ready.mat28@example.com', + }) + self.res_partner_10 = self.env['res.partner'].create({ + 'name': 'The Jackson Group', + 'email': 'jackson.group82@example.com', + }) + self.res_partner_12 = self.env['res.partner'].create({ + 'name': 'Azure Interior', + 'email': 'azure.Interior24@example.com', + }) + self.env['mail.performance.thread'].create([ + { + 'name': 'Object 0', + 'value': 0, + 'partner_id': self.res_partner_3.id, + }, { + 'name': 'Object 1', + 'value': 10, + 'partner_id': self.res_partner_3.id, + }, { + 'name': 'Object 2', + 'value': 20, + 'partner_id': self.res_partner_4.id, + }, { + 'name': 'Object 3', + 'value': 30, + 'partner_id': self.res_partner_10.id, + }, { + 'name': 'Object 4', + 'value': 40, + 'partner_id': self.res_partner_12.id, + } + ]) + + @users('__system__', 'demo') + @warmup + def test_read_mail(self): + """ Read records inheriting from 'mail.thread'. """ + records = self.env['mail.performance.thread'].search([]) + self.assertEqual(len(records), 5) + + with self.assertQueryCount(__system__=2, demo=2): + # without cache + for record in records: + record.partner_id.country_id.name + + with self.assertQueryCount(0): + # with cache + for record in records: + record.partner_id.country_id.name + + with self.assertQueryCount(0): + # value_pc must have been prefetched, too + for record in records: + record.value_pc + + @users('__system__', 'demo') + @warmup + def test_write_mail(self): + """ Write records inheriting from 'mail.thread' (no recomputation). """ + records = self.env['mail.performance.thread'].search([]) + self.assertEqual(len(records), 5) + + with self.assertQueryCount(__system__=2, demo=2): + records.write({'name': 'X'}) + + @users('__system__', 'demo') + @warmup + def test_write_mail_with_recomputation(self): + """ Write records inheriting from 'mail.thread' (with recomputation). """ + records = self.env['mail.performance.thread'].search([]) + self.assertEqual(len(records), 5) + + with self.assertQueryCount(__system__=2, demo=2): + records.write({'value': 42}) + + @users('__system__', 'demo') + @warmup + def test_write_mail_with_tracking(self): + """ Write records inheriting from 'mail.thread' (with field tracking). """ + record = self.env['mail.performance.thread'].create({ + 'name': 'Test', + 'track': 'Y', + 'value': 40, + 'partner_id': self.res_partner_12.id, + }) + + with self.assertQueryCount(__system__=3, demo=3): + record.track = 'X' + + @users('__system__', 'demo') + @warmup + def test_create_mail(self): + """ Create records inheriting from 'mail.thread' (without field tracking). """ + model = self.env['mail.performance.thread'] + + with self.assertQueryCount(__system__=2, demo=2): + model.with_context(tracking_disable=True).create({'name': 'X'}) + + @users('__system__', 'demo') + @warmup + def test_create_mail_with_tracking(self): + """ Create records inheriting from 'mail.thread' (with field tracking). """ + with self.assertQueryCount(__system__=7, demo=7): + self.env['mail.performance.thread'].create({'name': 'X'}) + + @users('__system__', 'emp') + @warmup + def test_create_mail_simple(self): + with self.assertQueryCount(__system__=6, emp=6): + self.env['mail.test.simple'].create({'name': 'Test'}) + + @users('__system__', 'emp') + @warmup + def test_write_mail_simple(self): + rec = self.env['mail.test.simple'].create({'name': 'Test'}) + with self.assertQueryCount(__system__=1, emp=1): + rec.write({ + 'name': 'Test2', + 'email_from': 'test@test.com', + }) + + +@tagged('mail_performance') +class TestMailAPIPerformance(BaseMailPerformance): + + def setUp(self): + super(TestMailAPIPerformance, self).setUp() + self.customer = self.env['res.partner'].with_context(self._quick_create_ctx).create({ + 'name': 'Test Customer', + 'email': 'customer.test@example.com', + }) + self.user_test = self.env['res.users'].with_context(self._quick_create_ctx).create({ + 'name': 'Paulette Testouille', + 'login': 'paul', + 'email': 'user.test.paulette@example.com', + 'notification_type': 'inbox', + 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])], + }) + + # automatically follow activities, for backward compatibility concerning query count + 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({ + 'name': 'TestRecord', + 'customer_id': self.customer.id, + 'user_id': self.user_test.id, + 'email_from': 'nopartner.test@example.com', + }) + self.test_template_full = self.env['mail.template'].create({ + 'name': 'TestTemplate', + 'model_id': self.env['ir.model']._get('mail.test.ticket').id, + 'subject': 'About ${object.name}', + 'body_html': '<p>Hello ${object.name}</p>', + 'email_from': '${object.user_id.email_formatted | safe}', + 'partner_to': '${object.customer_id.id}', + 'email_to': '${("%s Customer <%s>" % (object.name, object.email_from)) | safe}', + }) + + @users('__system__', 'emp') + @warmup + def test_adv_activity(self): + model = self.env['mail.test.activity'] + + with self.assertQueryCount(__system__=6, emp=6): + model.create({'name': 'Test'}) + + @users('__system__', 'emp') + @warmup + @mute_logger('odoo.models.unlink') + def test_adv_activity_full(self): + record = self.env['mail.test.activity'].create({'name': 'Test'}) + MailActivity = self.env['mail.activity'].with_context({ + 'default_res_model': 'mail.test.activity', + }) + + with self.assertQueryCount(__system__=6, emp=6): + activity = MailActivity.create({ + 'summary': 'Test Activity', + '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 + + with self.assertQueryCount(__system__=16, emp=20): + activity.action_feedback(feedback='Zizisse Done !') + + @users('__system__', 'emp') + @warmup + @mute_logger('odoo.models.unlink') + def test_adv_activity_mixin(self): + record = self.env['mail.test.activity'].create({'name': 'Test'}) + + with self.assertQueryCount(__system__=8, emp=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__=17, emp=20): + record.action_close('Dupe feedback') + + self.assertEqual(record.activity_ids, self.env['mail.activity']) + + @users('__system__', 'emp') + @warmup + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer(self): + self._create_test_records() + test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) + customer_id = self.customer.id + with self.assertQueryCount(__system__=2, emp=2): + composer = self.env['mail.compose.message'].with_context({ + 'default_composition_mode': 'comment', + 'default_model': test_record._name, + 'default_res_id': test_record.id, + }).create({ + 'body': '<p>Test Body</p>', + 'partner_ids': [(4, customer_id)], + }) + + with self.assertQueryCount(__system__=32, emp=39): + composer.send_mail() + + @users('__system__', 'emp') + @warmup + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_nodelete(self): + self._create_test_records() + test_record = self.env['mail.test.ticket'].browse(self.test_record_full.id) + customer_id = self.customer.id + with self.assertQueryCount(__system__=2, emp=2): + composer = self.env['mail.compose.message'].with_context({ + 'default_composition_mode': 'comment', + 'default_model': test_record._name, + 'default_res_id': test_record.id, + 'mail_auto_delete': False, + }).create({ + 'body': '<p>Test Body</p>', + 'partner_ids': [(4, customer_id)], + }) + + with self.assertQueryCount(__system__=25, emp=32): + composer.send_mail() + + @users('__system__', 'emp') + @warmup + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + def test_mail_composer_w_template(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) + # TODO FIXME non deterministic, last check 25 Mar 2020. Runbot at most +7 compared to local. + with self.assertQueryCount(__system__=12, emp=14): + 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__=33, emp=40): + composer.send_mail() + + # remove created partner to ensure tests are the same each run + self.env['res.partner'].sudo().search([('email', '=', 'nopartner.test@example.com')]).unlink() + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + 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__=37, emp=38): + record.write({ + 'user_id': self.user_test.id, + }) + + @users('__system__', 'emp') + @warmup + def test_message_assignation_inbox(self): + record = self.env['mail.test.track'].create({'name': 'Test'}) + with self.assertQueryCount(__system__=18, emp=21): + record.write({ + 'user_id': self.user_test.id, + }) + + @users('__system__', 'emp') + @warmup + def test_message_log(self): + record = self.env['mail.test.simple'].create({'name': 'Test'}) + + with self.assertQueryCount(__system__=1, emp=1): + record._message_log( + body='<p>Test _message_log</p>', + message_type='comment') + + @users('__system__', 'emp') + @warmup + def test_message_log_with_post(self): + record = self.env['mail.test.simple'].create({'name': 'Test'}) + + with self.assertQueryCount(__system__=3, emp=6): + record.message_post( + body='<p>Test message_post as log</p>', + subtype_xmlid='mail.mt_note', + message_type='comment') + + @users('__system__', 'emp') + @warmup + def test_message_post_no_notification(self): + record = self.env['mail.test.simple'].create({'name': 'Test'}) + + with self.assertQueryCount(__system__=3, emp=6): + record.message_post( + body='<p>Test Post Performances basic</p>', + partner_ids=[], + message_type='comment', + subtype_xmlid='mail.mt_comment') + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_message_post_one_email_notification(self): + record = self.env['mail.test.simple'].create({'name': 'Test'}) + + with self.assertQueryCount(__system__=28, emp=31): + record.message_post( + body='<p>Test Post Performances with an email ping</p>', + partner_ids=self.customer.ids, + message_type='comment', + subtype_xmlid='mail.mt_comment') + + @users('__system__', 'emp') + @warmup + def test_message_post_one_inbox_notification(self): + record = self.env['mail.test.simple'].create({'name': 'Test'}) + + with self.assertQueryCount(__system__=12, emp=17): + record.message_post( + body='<p>Test Post Performances with an inbox ping</p>', + partner_ids=self.user_test.partner_id.ids, + message_type='comment', + subtype_xmlid='mail.mt_comment') + + @mute_logger('odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_message_subscribe_default(self): + record = self.env['mail.test.simple'].create({'name': 'Test'}) + + with self.assertQueryCount(__system__=6, emp=6): + record.message_subscribe(partner_ids=self.user_test.partner_id.ids) + + with self.assertQueryCount(__system__=3, emp=3): + record.message_subscribe(partner_ids=self.user_test.partner_id.ids) + + @mute_logger('odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_message_subscribe_subtypes(self): + record = self.env['mail.test.simple'].create({'name': 'Test'}) + subtype_ids = (self.env.ref('test_mail.st_mail_test_simple_external') | self.env.ref('mail.mt_comment')).ids + + with self.assertQueryCount(__system__=5, emp=5): + record.message_subscribe(partner_ids=self.user_test.partner_id.ids, subtype_ids=subtype_ids) + + with self.assertQueryCount(__system__=2, emp=2): + record.message_subscribe(partner_ids=self.user_test.partner_id.ids, subtype_ids=subtype_ids) + + @mute_logger('odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_message_track(self): + record = self.env['mail.performance.tracking'].create({'name': 'Zizizatestname'}) + with self.assertQueryCount(__system__=3, emp=3): + record.write({'name': 'Zizizanewtestname'}) + record.flush() + + with self.assertQueryCount(__system__=5, emp=5): + record.write({'field_%s' % (i): 'Tracked Char Fields %s' % (i) for i in range(3)}) + record.flush() + + with self.assertQueryCount(__system__=6, emp=6): + record.write({'field_%s' % (i): 'Field Without Cache %s' % (i) for i in range(3)}) + record.flush() + record.write({'field_%s' % (i): 'Field With Cache %s' % (i) for i in range(3)}) + record.flush() + + +@tagged('mail_performance') +class TestMailComplexPerformance(BaseMailPerformance): + + def setUp(self): + super(TestMailComplexPerformance, self).setUp() + self.user_portal = self.env['res.users'].with_context(self._quick_create_ctx).create({ + 'name': 'Olivia Portal', + 'login': 'port', + 'email': 'p.p@example.com', + 'signature': '--\nOlivia', + 'notification_type': 'email', + 'groups_id': [(6, 0, [self.env.ref('base.group_portal').id])], + }) + + # setup mail gateway + self.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'example.com') + self.env['ir.config_parameter'].sudo().set_param('mail.catchall.alias', 'test-catchall') + self.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', 'test-bounce') + + self.channel = self.env['mail.channel'].with_context(self._quick_create_ctx).create({ + 'name': 'Listener', + }) + + # prepare recipients to test for more realistic workload + self.customer = self.env['res.partner'].with_context(self._quick_create_ctx).create({ + 'name': 'Test Customer', + 'email': 'test@example.com' + }) + self.container = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({ + 'name': 'Test Container', + 'customer_id': self.customer.id, + 'alias_name': 'test-alias', + }) + Partners = self.env['res.partner'].with_context(self._quick_create_ctx) + 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}) + 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] + ) + # `test_complex_mail_mail_send` + self.container.flush() + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_mail_mail_send(self): + message = self.env['mail.message'].sudo().create({ + 'subject': 'Test', + 'body': '<p>Test</p>', + 'author_id': self.env.user.partner_id.id, + 'email_from': self.env.user.partner_id.email, + 'model': 'mail.test.container', + 'res_id': self.container.id, + }) + mail = self.env['mail.mail'].sudo().create({ + 'body_html': '<p>Test</p>', + 'mail_message_id': message.id, + 'recipient_ids': [(4, pid) for pid in self.partners.ids], + }) + mail_ids = mail.ids + with self.assertQueryCount(__system__=7, emp=7): + self.env['mail.mail'].sudo().browse(mail_ids).send() + + self.assertEqual(mail.body_html, '<p>Test</p>') + self.assertEqual(mail.reply_to, formataddr(('%s %s' % (self.env.company.name, self.container.name), 'test-alias@example.com'))) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_message_post(self): + self.container.message_subscribe(self.user_portal.partner_id.ids) + record = self.container.with_user(self.env.user) + + with self.assertQueryCount(__system__=63, emp=64): + record.message_post( + body='<p>Test Post Performances</p>', + message_type='comment', + subtype_xmlid='mail.mt_comment') + + self.assertEqual(record.message_ids[0].body, '<p>Test Post Performances</p>') + self.assertEqual(record.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_message_post_template(self): + self.container.message_subscribe(self.user_portal.partner_id.ids) + record = self.container.with_user(self.env.user) + template_id = self.env.ref('test_mail.mail_test_container_tpl').id + + with self.assertQueryCount(__system__=73, emp=75): + record.message_post_with_template(template_id, message_type='comment', composition_mode='comment') + + self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name) + self.assertEqual(record.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id | self.customer) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_message_subscribe(self): + pids = self.partners.ids + cids = self.channel.ids + subtypes = self.env.ref('mail.mt_comment') | self.env.ref('test_mail.st_mail_test_ticket_container_upd') + subtype_ids = subtypes.ids + rec = self.env['mail.test.ticket'].create({ + 'name': 'Test', + 'container_id': False, + 'customer_id': False, + 'user_id': self.user_portal.id, + }) + rec1 = rec.with_context(active_test=False) # to see inactive records + + self.assertEqual(rec1.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id) + self.assertEqual(rec1.message_channel_ids, self.env['mail.channel']) + + # subscribe new followers with forced given subtypes + with self.assertQueryCount(__system__=8, emp=8): + rec.message_subscribe( + partner_ids=pids[:4], + channel_ids=cids, + subtype_ids=subtype_ids + ) + + self.assertEqual(rec1.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners[:4]) + self.assertEqual(rec1.message_channel_ids, self.channel) + + # subscribe existing and new followers with force=False, meaning only some new followers will be added + with self.assertQueryCount(__system__=6, emp=6): + rec.message_subscribe( + partner_ids=pids[:6], + channel_ids=cids, + subtype_ids=None + ) + + self.assertEqual(rec1.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners[:6]) + self.assertEqual(rec1.message_channel_ids, self.channel) + + # subscribe existing and new followers with force=True, meaning all will have the same subtypes + with self.assertQueryCount(__system__=7, emp=7): + rec.message_subscribe( + partner_ids=pids, + channel_ids=cids, + subtype_ids=subtype_ids + ) + + self.assertEqual(rec1.message_partner_ids, self.env.user.partner_id | self.user_portal.partner_id | self.partners) + self.assertEqual(rec1.message_channel_ids, self.channel) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_tracking_assignation(self): + """ Assignation performance test on already-created record """ + rec = self.env['mail.test.ticket'].create({ + 'name': 'Test', + 'container_id': self.container.id, + 'customer_id': self.customer.id, + 'user_id': self.env.uid, + }) + 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__=37, emp=38): + 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 + self.assertEqual(rec1.message_ids[0].subtype_id, self.env.ref('mail.mt_note')) + self.assertEqual(rec1.message_ids[0].notified_partner_ids, self.env['res.partner']) + # creation message + self.assertEqual(rec1.message_ids[1].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd')) + self.assertEqual(rec1.message_ids[1].notified_partner_ids, self.partners) + self.assertEqual(len(rec1.message_ids), 2) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_tracking_subscription_create(self): + """ Creation performance test involving auto subscription, assignation, tracking with subtype and template send. """ + container_id = self.container.id + customer_id = self.customer.id + user_id = self.user_portal.id + + with self.assertQueryCount(__system__=108, emp=109): + rec = self.env['mail.test.ticket'].create({ + 'name': 'Test', + 'container_id': container_id, + 'customer_id': customer_id, + 'user_id': user_id, + }) + + 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) + # creation message + self.assertEqual(rec1.message_ids[0].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd')) + self.assertEqual(rec1.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id) + self.assertEqual(len(rec1.message_ids), 1) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_tracking_subscription_subtype(self): + """ Write performance test involving auto subscription, tracking with subtype """ + rec = self.env['mail.test.ticket'].create({ + 'name': 'Test', + 'container_id': False, + 'customer_id': False, + 'user_id': self.user_portal.id, + }) + 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__=77, emp=77): + rec.write({ + 'name': 'Test2', + 'container_id': self.container.id, + }) + + self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) + # write tracking message + self.assertEqual(rec1.message_ids[0].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd')) + self.assertEqual(rec1.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id) + # creation message + self.assertEqual(rec1.message_ids[1].subtype_id, self.env.ref('mail.mt_note')) + self.assertEqual(rec1.message_ids[1].notified_partner_ids, self.env['res.partner']) + self.assertEqual(len(rec1.message_ids), 2) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_tracking_subscription_write(self): + """ Write performance test involving auto subscription, tracking with subtype and template send """ + container_id = self.container.id + customer_id = self.customer.id + container2 = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({ + 'name': 'Test Container 2', + 'customer_id': False, + 'alias_name': False, + }) + + rec = self.env['mail.test.ticket'].create({ + 'name': 'Test', + 'container_id': container2.id, + 'customer_id': False, + 'user_id': self.user_portal.id, + }) + 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__=85, emp=85): + rec.write({ + 'name': 'Test2', + 'container_id': container_id, + 'customer_id': customer_id, + }) + + self.assertEqual(rec1.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) + # write tracking message + self.assertEqual(rec1.message_ids[0].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd')) + self.assertEqual(rec1.message_ids[0].notified_partner_ids, self.partners | self.user_portal.partner_id) + # creation message + self.assertEqual(rec1.message_ids[1].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd')) + self.assertEqual(rec1.message_ids[1].notified_partner_ids, self.user_portal.partner_id) + self.assertEqual(len(rec1.message_ids), 2) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('__system__', 'emp') + @warmup + def test_complex_tracking_template(self): + """ Write performance test involving assignation, tracking with template """ + customer_id = self.customer.id + self.assertTrue(self.env.registry.ready, "We need to simulate that registery is ready") + rec = self.env['mail.test.ticket'].create({ + 'name': 'Test', + 'container_id': self.container.id, + 'customer_id': False, + 'user_id': self.user_portal.id, + 'mail_template': self.env.ref('test_mail.mail_test_ticket_tracking_tpl').id, + }) + 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__=29, emp=30): + rec.write({ + 'name': 'Test2', + 'customer_id': customer_id, + 'user_id': self.env.uid, + }) + + # write template message (sent to customer, mass mailing kept for history) + self.assertEqual(rec1.message_ids[0].subtype_id, self.env['mail.message.subtype']) + self.assertEqual(rec1.message_ids[0].subject, 'Test Template') + # write tracking message + self.assertEqual(rec1.message_ids[1].subtype_id, self.env.ref('mail.mt_note')) + self.assertEqual(rec1.message_ids[1].notified_partner_ids, self.env['res.partner']) + # creation message + self.assertEqual(rec1.message_ids[2].subtype_id, self.env.ref('test_mail.st_mail_test_ticket_container_upd')) + self.assertEqual(rec1.message_ids[2].notified_partner_ids, self.partners | self.user_portal.partner_id) + self.assertEqual(len(rec1.message_ids), 3) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('emp') + @warmup + def test_message_format(self): + """Test performance of `_message_format` and of `message_format` with + multiple messages with multiple attachments, different authors, various + notifications, and different tracking values. + Those messages might not make sense functionally but they are crafted to + cover as much of the code as possible in regard to number of queries. + """ + name_field = self.env['ir.model.fields']._get(self.container._name, 'name') + customer_id_field = self.env['ir.model.fields']._get(self.container._name, 'customer_id') + + messages = self.env['mail.message'].sudo().create([{ + 'subject': 'Test 0', + 'body': '<p>Test 0</p>', + 'author_id': self.partners[0].id, + 'email_from': self.partners[0].email, + 'model': 'mail.test.container', + 'res_id': self.container.id, + 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment'), + 'attachment_ids': [ + (0, 0, { + 'name': 'test file 0 - %d' % j, + 'datas': 'data', + }) for j in range(2) + ], + 'notification_ids': [ + (0, 0, { + 'res_partner_id': self.partners[3].id, + 'notification_type': 'inbox', + }), + (0, 0, { + 'res_partner_id': self.partners[4].id, + 'notification_type': 'email', + 'notification_status': 'exception', + }), + (0, 0, { + 'res_partner_id': self.partners[6].id, + 'notification_type': 'email', + 'notification_status': 'exception', + }), + ], + 'tracking_value_ids': [ + (0, 0, { + 'field': name_field.id, + 'field_desc': 'Name', + 'old_value_char': 'old 0', + 'new_value_char': 'new 0', + }), + (0, 0, { + 'field': customer_id_field.id, + 'field_desc': 'Customer', + 'old_value_integer': self.partners[7].id, + 'new_value_integer': self.partners[8].id, + }), + ] + }, { + 'subject': 'Test 1', + 'body': '<p>Test 1</p>', + 'author_id': self.partners[1].id, + 'email_from': self.partners[1].email, + 'model': 'mail.test.container', + 'res_id': self.container.id, + 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), + 'attachment_ids': [ + (0, 0, { + 'name': 'test file 1 - %d' % j, + 'datas': 'data', + }) for j in range(2) + ], + 'notification_ids': [ + (0, 0, { + 'res_partner_id': self.partners[5].id, + 'notification_type': 'inbox', + }), + (0, 0, { + 'res_partner_id': self.partners[6].id, + 'notification_type': 'email', + 'notification_status': 'exception', + }), + ], + 'tracking_value_ids': [ + (0, 0, { + 'field': name_field.id, + 'field_desc': 'Name', + 'old_value_char': 'old 1', + 'new_value_char': 'new 1', + }), + (0, 0, { + 'field': customer_id_field.id, + 'field_desc': 'Customer', + 'old_value_integer': self.partners[7].id, + 'new_value_integer': self.partners[8].id, + }), + ] + }]) + + with self.assertQueryCount(emp=5): + res = messages.message_format() + self.assertEqual(len(res), 2) + for message in res: + self.assertEqual(len(message['attachment_ids']), 2) + + messages.flush() + messages.invalidate_cache() + + with self.assertQueryCount(emp=17): + res = messages.message_format() + self.assertEqual(len(res), 2) + for message in res: + self.assertEqual(len(message['attachment_ids']), 2) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('emp') + @warmup + def test_message_format_group_thread_name_by_model(self): + """Ensures the fetch of multiple thread names is grouped by model.""" + records = [] + for i in range(5): + records.append(self.env['mail.test.simple'].create({'name': 'Test'})) + records.append(self.env['mail.test.track'].create({'name': 'Test'})) + + messages = self.env['mail.message'].create([{ + 'model': record._name, + 'res_id': record.id + } for record in records]) + + with self.assertQueryCount(emp=4): + res = messages.message_format() + self.assertEqual(len(res), 6) + + messages.flush() + messages.invalidate_cache() + + with self.assertQueryCount(emp=14): + res = messages.message_format() + self.assertEqual(len(res), 6) + + +@tagged('mail_performance') +class TestMailHeavyPerformancePost(BaseMailPerformance): + + def setUp(self): + super(TestMailHeavyPerformancePost, self).setUp() + + # record + self.customer = self.env['res.partner'].with_context(self._quick_create_ctx).create({ + 'name': 'customer', + 'email': 'customer@example.com', + }) + self.record = self.env['mail.test.container'].with_context(mail_create_nosubscribe=True).create({ + 'name': 'Test record', + 'customer_id': self.customer.id, + 'alias_name': 'test-alias', + }) + # followers + self.user_follower_email = self.env['res.users'].with_context(self._quick_create_ctx).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({ + '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({ + 'name': 'partner_follower', + 'email': 'partner_follower@example.com', + }) + self.record.message_subscribe([ + self.partner_follower.id, + self.user_follower_inbox.partner_id.id, + self.user_follower_email.partner_id.id + ]) + + # partner_ids + self.user_inbox = self.env['res.users'].with_context(self._quick_create_ctx).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({ + '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({ + 'name': 'partner', + 'email': 'partner@example.com', + }) + # channels user/partner + self.partner_channel_inbox = self.env['res.partner'].with_context(self._quick_create_ctx).create({ + 'name': 'partner_channel_inbox', + 'email': 'partner_channel_inbox@example.com', + }) + self.partner_channel_email = self.env['res.partner'].with_context(self._quick_create_ctx).create({ + 'name': 'partner_channel_email', + 'email': 'partner_channel_email@example.com', + }) + self.user_channel_inbox = self.env['res.users'].with_context(self._quick_create_ctx).create({ + 'name': 'user_channel_inbox', + 'login': 'user_channel_inbox', + 'email': 'user_channel_inbox@example.com', + 'notification_type': 'inbox', + 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])], + }) + self.user_channel_email = self.env['res.users'].with_context(self._quick_create_ctx).create({ + 'name': 'user_channel_email', + 'login': 'user_channel_email', + 'email': 'user_channel_email@example.com', + 'notification_type': 'inbox', + 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])], + }) + # channels + self.channel_inbox = self.env['mail.channel'].with_context(self._quick_create_ctx).create({ + 'name': 'channel_inbox', + 'channel_partner_ids': [(4, self.partner_channel_inbox.id), (4, self.user_channel_inbox.partner_id.id)] + }) + self.channel_email = self.env['mail.channel'].with_context(self._quick_create_ctx).create({ + 'name': 'channel_email', + 'email_send': True, + 'channel_partner_ids': [(4, self.partner_channel_email.id), (4, self.user_channel_email.partner_id.id)] + }) + 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)] + + self.patch(self.env.registry, 'ready', True) + + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @users('emp') + @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] + channel_ids = [self.channel_inbox.id, self.channel_email.id] + record = self.record.with_user(self.env.user) + attachements = [ # not linear on number of attachements + ('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 + with self.assertQueryCount(emp=80): + self.cr.sql_log = self.warm and self.cr.sql_log_count + record.with_context({}).message_post( + body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>', + subject='Test Subject', + message_type='notification', + subtype_xmlid=None, + partner_ids=partner_ids, + channel_ids=channel_ids, + parent_id=False, + attachments=attachements, + attachment_ids=attachement_ids, + add_sign=True, + model_description=False, + mail_auto_delete=True + ) + self.cr.sql_log = False + self.assertTrue(record.message_ids[0].body.startswith('<p>Test body <img src="/web/image/')) + self.assertEqual(self.attachements.mapped('res_model'), [record._name for i in range(3)]) + self.assertEqual(self.attachements.mapped('res_id'), [record.id for i in range(3)]) + # self.assertEqual(record.message_ids[0].notified_partner_ids, []) diff --git a/addons/test_mail/tests/test_ui.py b/addons/test_mail/tests/test_ui.py new file mode 100644 index 00000000..82858b52 --- /dev/null +++ b/addons/test_mail/tests/test_ui.py @@ -0,0 +1,10 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo.tests + + +@odoo.tests.tagged('post_install', '-at_install') +class TestUi(odoo.tests.HttpCase): + + def test_01_mail_tour(self): + self.start_tour("/web", 'mail_tour', login="admin") |
