Initial commit: Test packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:52 +02:00
commit 080accd21c
338 changed files with 32413 additions and 0 deletions

View file

@ -0,0 +1,24 @@
# -*- 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_composer_mixin
from . import test_mail_followers
from . import test_mail_message
from . import test_mail_message_security
from . import test_mail_mail
from . import test_mail_gateway
from . import test_mail_multicompany
from . import test_mail_thread_internals
from . import test_mail_thread_mixins
from . import test_mail_template
from . import test_mail_template_preview
from . import test_message_management
from . import test_message_post
from . import test_message_track
from . import test_performance
from . import test_ui
from . import test_mail_management
from . import test_mail_security

View file

@ -0,0 +1,37 @@
# -*- 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 TransactionCase
class TestMailCommon(MailCommon):
""" Main entry point for functional tests. Kept to ease backward
compatibility. """
class TestRecipients(TransactionCase):
@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',
'phone': False,
})
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',
'phone': False,
})

View file

@ -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.tests import tagged
from odoo.tools import mute_logger
@tagged('mail_followers')
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.assertSentEmail(self.partner_employee, [test_partner])
self.assertSentEmail(self.partner_employee, [self.partner_admin])
self.assertEqual(len(self._mails), 2)

View file

@ -0,0 +1,134 @@
# -*- 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
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('ir_actions')
class TestServerActionsEmail(TestMailCommon, TestServerActionsBase):
def setUp(self):
super(TestServerActionsEmail, self).setUp()
self.template = self._create_template(
'res.partner',
{'email_from': '{{ object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted }}',
'partner_to': '%s' % self.test_partner.id,
}
)
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
def test_action_email(self):
# initial state
self.assertEqual(len(self.test_partner.message_ids), 1,
'Contains Contact created message')
self.assertFalse(self.test_partner.message_partner_ids)
# update action: send an email
self.action.write({
'mail_post_method': 'email',
'state': 'mail_post',
'template_id': self.template.id,
})
self.assertFalse(self.action.mail_post_autofollow, 'Email action does not support autofollow')
with self.mock_mail_app():
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)
self.assertTrue(mail.auto_delete)
self.assertEqual(mail.body_html, '<p>Hello TestingPartner</p>')
self.assertFalse(mail.is_notification)
with self.mock_mail_gateway(mail_unlink_sent=True):
mail.send()
# no archive (message)
self.assertEqual(len(self.test_partner.message_ids), 1,
'Contains Contact created message')
self.assertFalse(self.test_partner.message_partner_ids)
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)],
})
self.action.with_context(self.context).run()
self.assertEqual(self.test_partner.message_partner_ids, self.env.ref('base.partner_admin') | random_partner)
def test_action_message_post(self):
# initial state
self.assertEqual(len(self.test_partner.message_ids), 1,
'Contains Contact created message')
self.assertFalse(self.test_partner.message_partner_ids)
# test without autofollow and comment
self.action.write({
'mail_post_autofollow': False,
'mail_post_method': 'comment',
'state': 'mail_post',
'template_id': self.template.id
})
with self.assertSinglePostNotifications(
[{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}],
message_info={'content': 'Hello %s' % self.test_partner.name,
'message_type': 'notification',
'subtype': 'mail.mt_comment',
}
):
self.action.with_context(self.context).run()
# NOTE: template using current user will have funny email_from
self.assertEqual(self.test_partner.message_ids[0].email_from, self.partner_root.email_formatted)
self.assertFalse(self.test_partner.message_partner_ids)
# test with autofollow and note
self.action.write({
'mail_post_autofollow': True,
'mail_post_method': 'note'
})
with self.assertSinglePostNotifications(
[{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}],
message_info={'content': 'Hello %s' % self.test_partner.name,
'message_type': 'notification',
'subtype': 'mail.mt_note',
}
):
self.action.with_context(self.context).run()
self.assertEqual(len(self.test_partner.message_ids), 3,
'2 new messages produced')
self.assertEqual(self.test_partner.message_partner_ids, self.test_partner)
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)
def test_action_next_activity_due_date(self):
""" Make sure we don't crash if a due date is set without a type. """
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',
'activity_date_deadline_range': 1,
'activity_date_deadline_range_type': False,
})
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)

View file

@ -0,0 +1,762 @@
# -*- 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 freezegun import freeze_time
from psycopg2 import IntegrityError
from unittest.mock import patch
from unittest.mock import DEFAULT
import pytz
from odoo import fields, exceptions, tests
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 MailTestActivity
from odoo.tools import mute_logger
from odoo.tests.common import Form, users
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 they have 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 they have 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
test_activity = self.env['mail.activity'].with_user(self.user_admin).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_admin.id,
'summary': 'Summary',
})
test_activity.flush_recordset()
# can _search activities if access to the document
self.env['mail.activity'].with_user(self.user_employee)._search(
[('id', '=', test_activity.id)], count=False)
# cannot _search 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):
searched_activity = self.env['mail.activity'].with_user(self.user_employee)._search(
[('id', '=', test_activity.id)], count=False)
# can read_group activities if access to the document
read_group_result = self.env['mail.activity'].with_user(self.user_employee).read_group(
[('id', '=', test_activity.id)],
['summary'],
['summary'],
)
self.assertEqual(1, read_group_result[0]['summary_count'])
self.assertEqual('Summary', read_group_result[0]['summary'])
# cannot read_group 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):
self.env['mail.activity'].with_user(self.user_employee).read_group(
[('id', '=', test_activity.id)],
['summary'],
['summary'],
)
# cannot read 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):
searched_activity = self.env['mail.activity'].with_user(self.user_employee).search(
[('id', '=', test_activity.id)])
searched_activity.read(['summary'])
# cannot search_read 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):
self.env['mail.activity'].with_user(self.user_employee).search_read(
[('id', '=', test_activity.id)],
['summary'])
# 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)})
self.assertEqual(test_record.activity_state, 'overdue')
test_record.activity_ids.write({'date_deadline': date.today()})
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 the following 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)
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['ir.model']._get_id('mail.test.activity'), default_res_id=self.test_record.id)) as ActivityForm:
# `res_model_id` and `res_id` are invisible, see view `mail.mail_activity_view_form_popup`
# they must be set using defaults, see `action_feedback_schedule_next`
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)
@mute_logger('odoo.sql_db')
def test_activity_values(self):
""" Test activities are created with right model / res_id values linking
to records without void values. 0 as res_id especially is not wanted. """
# creating activities on a temporary record generates activities with res_id
# being 0, which is annoying -> never create activities in transient mode
temp_record = self.env['mail.test.activity'].new({'name': 'Test'})
with self.assertRaises(IntegrityError):
activity = temp_record.activity_schedule('test_mail.mail_act_test_todo', user_id=self.user_employee.id)
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
with self.assertRaises(IntegrityError):
self.env['mail.activity'].create({
'res_model_id': self.env['ir.model']._get_id(test_record._name),
})
with self.assertRaises(IntegrityError):
self.env['mail.activity'].create({
'res_model_id': self.env['ir.model']._get_id(test_record._name),
'res_id': False,
})
with self.assertRaises(IntegrityError):
self.env['mail.activity'].create({
'res_id': test_record.id,
})
activity = self.env['mail.activity'].create({
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id(test_record._name),
})
with self.assertRaises(IntegrityError):
activity.write({'res_model_id': False})
self.env.flush_all()
with self.assertRaises(IntegrityError):
activity.write({'res_id': False})
self.env.flush_all()
with self.assertRaises(IntegrityError):
activity.write({'res_id': 0})
self.env.flush_all()
@tests.tagged('mail_activity')
class TestActivityMixin(TestActivityCommon):
@classmethod
def setUpClass(cls):
super(TestActivityMixin, cls).setUpClass()
cls.user_utc = mail_new_test_user(
cls.env,
name='User UTC',
login='User UTC',
)
cls.user_utc.tz = 'UTC'
cls.user_australia = mail_new_test_user(
cls.env,
name='user Australia',
login='user Australia',
)
cls.user_australia.tz = 'Australia/Sydney'
@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_recordset(['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_recordset(['activity_ids'])
self.assertEqual(self.test_record.activity_user_id, self.user_employee)
self.test_record.invalidate_recordset()
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)
@users('employee')
def test_feedback_w_attachments(self):
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
activity = self.env['mail.activity'].create({
'activity_type_id': 1,
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
attachments = self.env['ir.attachment'].create([{
'name': 'test',
'res_name': 'test',
'res_model': 'mail.activity',
'res_id': activity.id,
'datas': 'test',
}, {
'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 = test_record.message_ids[-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)
@users('employee')
def test_feedback_chained_current_date(self):
frozen_now = datetime(2021, 10, 10, 14, 30, 15)
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
first_activity = self.env['mail.activity'].create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_chained_1').id,
'date_deadline': frozen_now + relativedelta(days=-2),
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
first_activity_id = first_activity.id
with freeze_time(frozen_now):
first_activity.action_feedback(feedback='Done')
self.assertFalse(first_activity.exists())
# check chained activity
new_activity = test_record.activity_ids
self.assertNotEqual(new_activity.id, first_activity_id)
self.assertEqual(new_activity.summary, 'Take the second step.')
self.assertEqual(new_activity.date_deadline, frozen_now.date() + relativedelta(days=10))
@users('employee')
def test_feedback_chained_previous(self):
self.env.ref('test_mail.mail_act_test_chained_2').sudo().write({'delay_from': 'previous_activity'})
frozen_now = datetime(2021, 10, 10, 14, 30, 15)
test_record = self.env['mail.test.activity'].browse(self.test_record.ids)
first_activity = self.env['mail.activity'].create({
'activity_type_id': self.env.ref('test_mail.mail_act_test_chained_1').id,
'date_deadline': frozen_now + relativedelta(days=-2),
'res_id': test_record.id,
'res_model_id': self.env['ir.model']._get_id('mail.test.activity'),
'summary': 'Test',
})
first_activity_id = first_activity.id
with freeze_time(frozen_now):
first_activity.action_feedback(feedback='Done')
self.assertFalse(first_activity.exists())
# check chained activity
new_activity = test_record.activity_ids
self.assertNotEqual(new_activity.id, first_activity_id)
self.assertEqual(new_activity.summary, 'Take the second step.')
self.assertEqual(new_activity.date_deadline, frozen_now.date() + relativedelta(days=8),
'New deadline should take into account original activity deadline, not current date')
def test_mail_activity_state(self):
"""Create 3 activity for 2 different users in 2 different timezones.
User UTC (+0h)
User Australia (+11h)
Today datetime: 1/1/2020 16h
Activity 1 & User UTC
1/1/2020 - 16h UTC -> The state is today
Activity 2 & User Australia
1/1/2020 - 16h UTC
2/1/2020 - 1h Australia -> State is overdue
Activity 3 & User UTC
1/1/2020 - 23h UTC -> The state is today
"""
today_utc = datetime(2020, 1, 1, 16, 0, 0)
class MockedDatetime(datetime):
@classmethod
def utcnow(cls):
return today_utc
record = self.env['mail.test.activity'].create({'name': 'Record'})
with patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime):
activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': 1,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': record.id,
'date_deadline': today_utc,
'user_id': self.user_utc.id,
})
activity_2 = activity_1.copy()
activity_2.user_id = self.user_australia
activity_3 = activity_1.copy()
activity_3.date_deadline += relativedelta(hours=7)
self.assertEqual(activity_1.state, 'today')
self.assertEqual(activity_2.state, 'overdue')
self.assertEqual(activity_3.state, 'today')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
def test_mail_activity_mixin_search_state_basic(self):
"""Test the search method on the "activity_state".
Test all the operators and also test the case where the "activity_state" is
different because of the timezone. There's also a tricky case for which we
"reverse" the domain for performance purpose.
"""
today_utc = datetime(2020, 1, 1, 16, 0, 0)
class MockedDatetime(datetime):
@classmethod
def utcnow(cls):
return today_utc
# Create some records without activity schedule on it for testing
self.env['mail.test.activity'].create([
{'name': 'Record %i' % record_i}
for record_i in range(5)
])
origin_1, origin_2 = self.env['mail.test.activity'].search([], limit=2)
with patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime), \
patch('odoo.addons.mail.models.mail_activity_mixin.datetime', MockedDatetime):
origin_1_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': 1,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_1.id,
'date_deadline': today_utc,
'user_id': self.user_utc.id,
})
origin_1_activity_2 = origin_1_activity_1.copy()
origin_1_activity_2.user_id = self.user_australia
origin_1_activity_3 = origin_1_activity_1.copy()
origin_1_activity_3.date_deadline += relativedelta(hours=8)
self.assertEqual(origin_1_activity_1.state, 'today')
self.assertEqual(origin_1_activity_2.state, 'overdue')
self.assertEqual(origin_1_activity_3.state, 'today')
origin_2_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': 1,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_2.id,
'date_deadline': today_utc + relativedelta(hours=8),
'user_id': self.user_utc.id,
})
origin_2_activity_2 = origin_2_activity_1.copy()
origin_2_activity_2.user_id = self.user_australia
origin_2_activity_3 = origin_2_activity_1.copy()
origin_2_activity_3.date_deadline -= relativedelta(hours=8)
origin_2_activity_4 = origin_2_activity_1.copy()
origin_2_activity_4.date_deadline = datetime(2020, 1, 2, 0, 0, 0)
self.assertEqual(origin_2_activity_1.state, 'planned')
self.assertEqual(origin_2_activity_2.state, 'today')
self.assertEqual(origin_2_activity_3.state, 'today')
self.assertEqual(origin_2_activity_4.state, 'planned')
all_activity_mixin_record = self.env['mail.test.activity'].search([])
result = self.env['mail.test.activity'].search([('activity_state', '=', 'today')])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state == 'today'))
result = self.env['mail.test.activity'].search([('activity_state', 'in', ('today', 'overdue'))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state in ('today', 'overdue')))
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('today',))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state not in ('today',)))
result = self.env['mail.test.activity'].search([('activity_state', '=', False)])
self.assertTrue(len(result) >= 3, "There is more than 3 records without an activity schedule on it")
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: not p.activity_state))
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('planned', 'overdue', 'today'))])
self.assertTrue(len(result) >= 3, "There is more than 3 records without an activity schedule on it")
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: not p.activity_state))
# test tricky case when the domain will be reversed in the search method
# because of falsy value
result = self.env['mail.test.activity'].search([('activity_state', 'not in', ('today', False))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state not in ('today', False)))
result = self.env['mail.test.activity'].search([('activity_state', 'in', ('today', False))])
self.assertTrue(len(result) > 0)
self.assertEqual(result, all_activity_mixin_record.filtered(lambda p: p.activity_state in ('today', False)))
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
def test_mail_activity_mixin_search_state_different_day_but_close_time(self):
"""Test the case where there's less than 24 hours between the deadline and now_tz,
but one day of difference (e.g. 23h 01/01/2020 & 1h 02/02/2020). So the state
should be "planned" and not "today". This case was tricky to implement in SQL
that's why it has its own test.
"""
today_utc = datetime(2020, 1, 1, 23, 0, 0)
class MockedDatetime(datetime):
@classmethod
def utcnow(cls):
return today_utc
# Create some records without activity schedule on it for testing
self.env['mail.test.activity'].create([
{'name': 'Record %i' % record_i}
for record_i in range(5)
])
origin_1 = self.env['mail.test.activity'].search([], limit=1)
with patch('odoo.addons.mail.models.mail_activity.datetime', MockedDatetime):
origin_1_activity_1 = self.env['mail.activity'].create({
'summary': 'Test',
'activity_type_id': 1,
'res_model_id': self.env.ref('test_mail.model_mail_test_activity').id,
'res_id': origin_1.id,
'date_deadline': today_utc + relativedelta(hours=2),
'user_id': self.user_utc.id,
})
self.assertEqual(origin_1_activity_1.state, 'planned')
result = self.env['mail.test.activity'].search([('activity_state', '=', 'today')])
self.assertNotIn(origin_1, result, 'The activity state miss calculated during the search')
@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.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.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.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)
@tests.tagged('mail_activity')
class TestORM(TestActivityCommon):
"""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
"""
MailTestActivityCtx = 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
self.env['mail.test.activity'].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(MailTestActivityCtx) - timedelta(days=7),
)
self.env['mail.test.activity'].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(MailTestActivityCtx),
)
self.env['mail.test.activity'].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(MailTestActivityCtx) + 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 = MailTestActivityCtx.read_group(domain, fields=['date'], groupby=[groupby])
progressbars = MailTestActivityCtx.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"])

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,113 @@
# -*- 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
from odoo.tests.common import users
@tagged('mail_composer_mixin')
class TestMailComposerMixin(TestMailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestMailComposerMixin, 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)],
})
cls.mail_template = cls.env['mail.template'].create({
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
'model_id': cls.env['ir.model']._get('mail.test.composer.source').id,
'name': 'Test Template for mail.test.composer.source',
'lang': '{{ object.customer_id.lang }}',
'subject': 'EnglishSubject for {{ object.name }}',
})
cls.test_record = cls.env['mail.test.composer.source'].create({
'name': cls.partner_1.name,
'customer_id': cls.partner_1.id,
})
cls._activate_multi_lang(
layout_arch_db='<body><t t-out="message.body"/> English Layout for <t t-esc="model_description"/></body>',
lang_code='es_ES',
test_record=cls.test_record,
test_template=cls.mail_template,
)
@users("employee")
def test_content_sync(self):
""" Test updating template updates the dynamic fields accordingly. """
source = self.test_record.with_env(self.env)
composer = self.env['mail.test.composer.mixin'].create({
'name': 'Invite',
'template_id': self.mail_template.id,
'source_ids': [(4, source.id)],
})
self.assertEqual(composer.body, self.mail_template.body_html)
self.assertEqual(composer.subject, self.mail_template.subject)
self.assertFalse(composer.lang, 'Fixme: lang is not propagated currently')
subject = composer._render_field('subject', source.ids)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}')
body = composer._render_field('body', source.ids)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
@users("employee")
def test_rendering_custom(self):
""" Test rendering with custom strings (not coming from template) """
source = self.test_record.with_env(self.env)
composer = self.env['mail.test.composer.mixin'].create({
'description': '<p>Description for <t t-esc="object.name"/></p>',
'body': '<p>SpecificBody from <t t-out="user.name"/></p>',
'name': 'Invite',
'subject': 'SpecificSubject for {{ object.name }}',
})
self.assertEqual(composer.body, '<p>SpecificBody from <t t-out="user.name"/></p>')
self.assertEqual(composer.subject, 'SpecificSubject for {{ object.name }}')
subject = composer._render_field('subject', source.ids)[source.id]
self.assertEqual(subject, f'SpecificSubject for {source.name}')
body = composer._render_field('body', source.ids)[source.id]
self.assertEqual(body, f'<p>SpecificBody from {self.env.user.name}</p>')
description = composer._render_field('description', source.ids)[source.id]
self.assertEqual(description, f'<p>Description for {source.name}</p>')
@users("employee")
def test_rendering_lang(self):
""" Test rendering with language involved """
template = self.mail_template.with_env(self.env)
customer = self.partner_1.with_env(self.env)
customer.lang = 'es_ES'
source = self.test_record.with_env(self.env)
composer = self.env['mail.test.composer.mixin'].create({
'description': '<p>Description for <t t-esc="object.name"/></p>',
'lang': '{{ object.customer_id.lang }}',
'name': 'Invite',
'template_id': self.mail_template.id,
'source_ids': [(4, source.id)],
})
self.assertEqual(composer.body, template.body_html)
self.assertEqual(composer.subject, template.subject)
self.assertEqual(composer.lang, '{{ object.customer_id.lang }}')
# do not specifically ask for language computation
subject = composer._render_field('subject', source.ids, compute_lang=False)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}')
body = composer._render_field('body', source.ids, compute_lang=False)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
description = composer._render_field('description', source.ids)[source.id]
self.assertEqual(description, f'<p>Description for {source.name}</p>')
# ask for dynamic language computation
subject = composer._render_field('subject', source.ids, compute_lang=True)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}',
'Fixme: translations are not done, as taking composer translations and not template one')
body = composer._render_field('body', source.ids, compute_lang=True)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>',
'Fixme: translations are not done, as taking composer translations and not template one'
)
description = composer._render_field('description', source.ids)[source.id]
self.assertEqual(description, f'<p>Description for {source.name}</p>')

View file

@ -0,0 +1,790 @@
# -*- 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.exceptions import AccessError
from odoo.tests import tagged, users
from odoo.tools 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()
# 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_message_partner_ids(self):
test_record = self.test_record.with_user(self.user_employee)
partner = self.user_employee.partner_id
followed_before = self.env['mail.test.simple'].search([('message_partner_ids', 'in', partner.ids)])
self.assertFalse(partner in test_record.message_partner_ids)
self.assertNotIn(test_record, followed_before)
test_record.message_subscribe(partner_ids=[partner.id])
followed_after = self.env['mail.test.simple'].search([('message_partner_ids', 'in', partner.ids)])
self.assertTrue(partner in test_record.message_partner_ids)
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])
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)
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_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_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_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_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)
@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')
@mute_logger('odoo.models.unlink')
def test_followers_inverse_message_partner(self):
test_record = self.test_record.with_env(self.env)
partner0, partner1, partner2, partner3 = self.env['res.partner'].create(
[{'email': f'partner.{n}@test.lan', 'name': f'partner{n}'} for n in range(4)]
)
self.assertFalse(test_record.message_follower_ids)
self.assertFalse(test_record.message_partner_ids)
# fillup with API
test_record.message_subscribe(partner_ids=partner3.ids)
self.assertEqual(test_record.message_follower_ids.partner_id, partner3)
# set empty
test_record.message_partner_ids = None
self.assertFalse(test_record.message_follower_ids.partner_id)
# set 1
test_record.message_partner_ids = partner0
self.assertEqual(test_record.message_follower_ids.partner_id, partner0)
# set multiple when non-empty
test_record.message_partner_ids = partner1 + partner2
self.assertEqual(test_record.message_follower_ids.partner_id, partner1 + partner2)
# remove 1
test_record.message_partner_ids -= partner1
self.assertEqual(test_record.message_follower_ids.partner_id, partner2)
# add multiple with one already set
test_record.message_partner_ids += partner1 + partner2
self.assertEqual(test_record.message_follower_ids.partner_id, partner1 + partner2)
# remove outside of existing
test_record.message_partner_ids -= partner3
self.assertEqual(test_record.message_follower_ids.partner_id, partner1 + partner2)
# reset
test_record.message_partner_ids = False
self.assertFalse(test_record.message_follower_ids.partner_id)
# test with inactive and commands
partner0.write({'active': False})
test_record.write({'message_partner_ids': [(4, partner0.id), (4, partner1.id)]})
self.assertEqual(test_record.message_follower_ids.partner_id, partner1)
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_followers_inverse_message_partner_access_rights(self):
""" Make sure we're not bypassing security checks by setting a partner
instead of a follower """
test_record = self.test_record.with_user(self.user_portal)
partner0 = self.env['res.partner'].create({
'email': 'partner1@test.lan',
'name': 'partner1',
})
_name = test_record.name # check portal user can read
# set empty
with self.assertRaises(AccessError):
test_record.message_partner_ids = None
# set 1
with self.assertRaises(AccessError):
test_record.message_partner_ids = partner0
# remove 1
with self.assertRaises(AccessError):
test_record.message_partner_ids -= partner0
@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)
@users('employee')
def test_create_multi_followers(self):
documents = self.env['mail.test.simple'].create([{'name': 'ninja'}] * 5)
for document in documents:
self.assertEqual(document.message_follower_ids.partner_id, self.env.user.partner_id)
self.assertEqual(document.message_follower_ids.subtype_ids, self.default_group_subtypes)
@users('employee')
def test_subscriptions_data_fetch(self):
""" Test that _get_subscription_data gives correct values when modifying followers manually."""
test_record = self.test_record
test_record_copy = self.test_record.copy()
test_records = test_record + test_record_copy
test_record.message_subscribe([self.user_employee.partner_id.id])
subscription_data = self.env['mail.followers']._get_subscription_data([(test_records._name, test_records.ids)], None)
self.assertEqual(len(subscription_data), 1)
self.assertEqual(subscription_data[0][1], test_record.id)
self.env['mail.followers'].browse(subscription_data[0][0]).sudo().res_id = test_record_copy
subscription_data = self.env['mail.followers']._get_subscription_data([(test_records._name, test_records.ids)], None)
self.assertEqual(len(subscription_data), 1)
self.assertEqual(subscription_data[0][1], test_record_copy.id)
@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)
@mute_logger('odoo.models.unlink')
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_recordset()
self.partner_admin.active = False
self.partner_admin.flush_recordset()
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)
new_record = self.env['mail.test.track'].with_user(self.user_admin).create({
'name': 'Test',
})
self.assertFalse(new_record.message_partner_ids,
'Filters out inactive partners')
self.assertFalse(new_record.message_follower_ids.partner_id,
'Does not subscribe inactive partner')
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))
@mute_logger('odoo.models.unlink')
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_mail_id))
self.assertEqual(mail_notification.mail_mail_id.state, 'outgoing')
@tagged('mail_followers', 'post_install', '-at_install')
class RecipientsNotificationTest(TestMailCommon):
""" Test advanced and complex recipients computation / notification, such
as multiple users, batch computation, ... Post install because we need the
registry to be ready to send notifications."""
@classmethod
def setUpClass(cls):
super(RecipientsNotificationTest, cls).setUpClass()
# portal user for testing share status / internal subtypes
cls.user_portal = cls._create_portal_user()
cls.partner_portal = cls.user_portal.partner_id
# simple customer
cls.customer = cls.env['res.partner'].create({
'email': 'customer@test.customer.com',
'name': 'Customer',
'phone': '+32455778899',
})
# Simulate case of 2 users that got their partner merged
cls.common_partner = cls.env['res.partner'].create({
'email': 'common.partner@test.customer.com',
'name': 'Common Partner',
'phone': '+32455998877',
})
cls.user_1, cls.user_2 = cls.env['res.users'].with_context(no_reset_password=True).create([
{'groups_id': [(4, cls.env.ref('base.group_portal').id)],
'login': '_login_portal',
'notification_type': 'email',
'partner_id': cls.common_partner.id,
},
{'groups_id': [(4, cls.env.ref('base.group_user').id)],
'login': '_login_internal',
'notification_type': 'inbox',
'partner_id': cls.common_partner.id,
}
])
cls.env.flush_all()
def assertRecipientsData(self, recipients_data, records, partners, partner_to_users=None):
""" Custom assert as recipients structure is custom and may change due
to some implementation choice. """
if records:
self.assertEqual(set(recipients_data.keys()), set(records.ids))
record_ids = records.ids
else:
records, record_ids = [False], [0]
for record, record_id in zip(records, record_ids):
record_data = recipients_data[record_id]
self.assertEqual(set(record_data.keys()), set(partners.ids))
for partner in partners:
partner_data = record_data[partner.id]
if partner_to_users and partner_to_users.get(partner.id): #helps making test explicit
user = partner_to_users[partner.id]
else:
user = next((user for user in partner.user_ids if not user.share), self.env['res.users'])
if not user:
user = next((user for user in partner.user_ids), self.env['res.users'])
self.assertEqual(partner_data['active'], partner.active)
if user:
self.assertEqual(partner_data['groups'], set(user.groups_id.ids))
self.assertEqual(partner_data['notif'], user.notification_type)
self.assertEqual(partner_data['uid'], user.id)
else:
self.assertEqual(partner_data['groups'], set())
self.assertEqual(partner_data['notif'], 'email')
self.assertFalse(partner_data['uid'])
if record:
self.assertEqual(partner_data['is_follower'], partner in record.message_partner_ids)
else:
self.assertFalse(partner_data['is_follower'])
self.assertEqual(partner_data['share'], partner.partner_share)
self.assertEqual(partner_data['ushare'], user.share)
@users('employee')
def test_notification_nodupe(self):
""" Check that we only create one mail.notification per partner. """
# Trigger auto subscribe notification
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": self.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', '=', self.common_partner.id)
])
self.assertEqual(len(notif), 1)
self.assertEqual(notif.notification_type, 'inbox', 'Multi users should take internal users if possible')
recipients_data = self.env['mail.followers']._get_recipient_data(
test, 'comment', self.env.ref('mail.mt_comment').id,
pids=self.common_partner.ids)
self.assertRecipientsData(recipients_data, test, self.common_partner + self.partner_employee,
partner_to_users={self.common_partner.id: self.user_2})
@users('employee')
@mute_logger('odoo.models.unlink')
def test_notification_unlink(self):
""" Check that we unlink the created user_notification after unlinked the
related document. """
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": self.user_1.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()
self.assertEqual(
self.env['mail.message'].search_count([
('res_id', '=', test.id),
('model', '=', 'mail.test.track'),
('message_type', '=', 'user_notification')
]), 0
)
@users('employee')
def test_notification_user_choice(self):
""" Check fetching user information when notifying someone with multiple
users (more complex use case). """
company_other = self.env['res.company'].sudo().create({
'currency_id': self.env.ref('base.CAD').id,
'email': 'company_other@test.example.com',
'name': 'Company Other',
})
shared_partner = self.env['res.partner'].sudo().create({
'email': 'common.partner@test.customer.com',
'name': 'Common Partner',
'phone': '+32455998877',
})
cids = (company_other + self.company_admin).ids
user_2_1, user_2_2, user_2_3 = self.env['res.users'].sudo().with_context(no_reset_password=True).create([
{'company_ids': [(6, 0, cids)],
'company_id': self.company_admin.id,
'groups_id': [(4, self.env.ref('base.group_portal').id)],
'login': '_login2_portal',
'notification_type': 'email',
'partner_id': shared_partner.id,
},
{'company_ids': [(6, 0, cids)],
'company_id': self.company_admin.id,
'groups_id': [(4, self.env.ref('base.group_user').id)],
'login': '_login2_internal',
'notification_type': 'inbox',
'partner_id': shared_partner.id,
},
{'company_ids': [(6, 0, cids)],
'company_id': company_other.id,
'groups_id': [(4, self.env.ref('base.group_user').id), (4, self.env.ref('base.group_partner_manager').id)],
'login': '_login2_manager',
'notification_type': 'inbox',
'partner_id': shared_partner.id,
}
])
(user_2_1 + user_2_2 + user_2_3).flush_recordset()
# just ensure current share status
self.assertFalse(shared_partner.partner_share)
self.assertTrue(user_2_1.share)
self.assertFalse(user_2_2.share or user_2_3.share)
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": False})
self.assertEqual(test.message_partner_ids, self.partner_employee)
with self.assertSinglePostNotifications(
[{'group': 'customer', 'partner': shared_partner,
'status': 'sent', 'type': 'inbox'}],
message_info={'content': 'User Choice Notification'}):
test.message_post(
body='<p>User Choice Notification</p>',
message_type='comment',
partner_ids=shared_partner.ids,
subtype_xmlid='mail.mt_comment',
)
recipients_data = self.env['mail.followers']._get_recipient_data(
test, 'comment', self.env.ref('mail.mt_comment').id,
pids=shared_partner.ids)
self.assertRecipientsData(recipients_data, test, self.partner_employee + shared_partner,
partner_to_users={shared_partner.id: user_2_2})
@users('employee')
def test_recipients_fetch(self):
test_records = self.env['mail.test.simple'].create([
{'email_from': 'ignasse@example.com',
'name': 'Test %s' % idx,
} for idx in range(5)
])
# make followers listen to notes to use it and check portal will never be notified of it (internal)
test_records.message_follower_ids.sudo().write({'subtype_ids': [(4, self.env.ref('mail.mt_note').id)]})
for test_record in test_records:
self.assertEqual(test_record.message_partner_ids, self.env.user.partner_id)
test_records[0].message_subscribe(self.partner_portal.ids)
self.assertNotIn(
self.env.ref('mail.mt_note'),
test_records[0].message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal).subtype_ids,
'Portal user should not follow notes by default')
# just fetch followers
recipients_data = self.env['mail.followers']._get_recipient_data(
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
pids=None
)
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.partner_portal)
# followers + additional recipients
recipients_data = self.env['mail.followers']._get_recipient_data(
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
pids=(self.customer + self.common_partner + self.partner_admin).ids
)
self.assertRecipientsData(recipients_data, test_records[0],
self.env.user.partner_id + self.partner_portal + self.customer + self.common_partner + self.partner_admin)
# ensure filtering on internal: should exclude Portal even if misconfiguration
follower_portal = test_records[0].message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal).sudo()
follower_portal.write({'subtype_ids': [(4, self.env.ref('mail.mt_note').id)]})
follower_portal.flush_recordset()
recipients_data = self.env['mail.followers']._get_recipient_data(
test_records[0], 'comment', self.env.ref('mail.mt_note').id,
pids=(self.common_partner + self.partner_admin).ids
)
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.common_partner + self.partner_admin)
# ensure filtering on subtype: should exclude Portal as it does not follow comment anymore
follower_portal.write({'subtype_ids': [(3, self.env.ref('mail.mt_comment').id)]})
recipients_data = self.env['mail.followers']._get_recipient_data(
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
pids=(self.common_partner + self.partner_admin).ids
)
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.common_partner + self.partner_admin)
# check without subtype
recipients_data = self.env['mail.followers']._get_recipient_data(
test_records[0], 'comment', False,
pids=(self.common_partner + self.partner_admin).ids
)
self.assertRecipientsData(recipients_data, test_records[0], self.common_partner + self.partner_admin)
# multi mode
test_records[1].message_subscribe(self.partner_portal.ids)
test_records[0:4].message_subscribe(self.common_partner.ids)
recipients_data = self.env['mail.followers']._get_recipient_data(
test_records, 'comment', self.env.ref('mail.mt_comment').id,
pids=self.partner_admin.ids
)
# 0: portal is follower but does not follow comment + common partner (+ admin as pid)
recipients_data_1 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[0:1].ids)
self.assertRecipientsData(recipients_data_1, test_records[0:1], self.env.user.partner_id + self.common_partner + self.partner_admin)
# 1: portal is follower with comment + common partner (+ admin as pid)
recipients_data_1 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[1:2].ids)
self.assertRecipientsData(recipients_data_1, test_records[1:2], self.env.user.partner_id + self.common_partner + self.partner_portal + self.partner_admin)
# 2-3: common partner (+ admin as pid)
recipients_data_2 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[2:4].ids)
self.assertRecipientsData(recipients_data_2, test_records[2:4], self.env.user.partner_id + self.common_partner + self.partner_admin)
# 4+: env user partner (+ admin as pid)
recipients_data_3 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[4:].ids)
self.assertRecipientsData(recipients_data_3, test_records[4:], self.env.user.partner_id + self.partner_admin)
# multi mode, pids only
recipients_data = self.env['mail.followers']._get_recipient_data(
test_records, 'comment', False,
pids=(self.env.user.partner_id + self.partner_admin).ids
)
self.assertRecipientsData(recipients_data, test_records, self.env.user.partner_id + self.partner_admin)
# on mail.thread, False everywhere: pathologic case
test_partners = self.partner_admin + self.partner_employee + self.common_partner
recipients_data = self.env['mail.followers']._get_recipient_data(
self.env['mail.thread'], False, False,
pids=test_partners.ids
)
self.assertRecipientsData(recipients_data, False, test_partners)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,907 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import psycopg2
import pytz
import smtplib
from datetime import datetime, timedelta
from freezegun import freeze_time
from OpenSSL.SSL import Error as SSLError
from socket import gaierror, timeout
from unittest.mock import call, patch
from odoo import api, Command, tools
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
from odoo.addons.test_mail.tests.common import TestMailCommon
from odoo.exceptions import AccessError
from odoo.tests import common, tagged, users
from odoo.tools import mute_logger, DEFAULT_SERVER_DATETIME_FORMAT
@tagged('mail_mail')
class TestMailMail(TestMailCommon):
@classmethod
def setUpClass(cls):
super(TestMailMail, cls).setUpClass()
cls._init_mail_servers()
cls.server_domain_2 = cls.env['ir.mail_server'].create({
'name': 'Server 2',
'smtp_host': 'test_2.com',
'from_filter': 'test_2.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.test_message = cls.test_record.message_post(body='<p>Message</p>', subject='Subject')
cls.test_mail = cls.env['mail.mail'].create([{
'body': '<p>Body</p>',
'email_from': False,
'email_to': 'test@example.com',
'is_notification': True,
'subject': 'Subject',
}])
cls.test_notification = cls.env['mail.notification'].create({
'is_read': False,
'mail_mail_id': cls.test_mail.id,
'mail_message_id': cls.test_message.id,
'notification_type': 'email',
'res_partner_id': cls.partner_employee.id, # not really used for matching except multi-recipients
})
cls.emails_falsy = [False, '', ' ']
cls.emails_invalid = ['buggy', 'buggy, wrong']
cls.emails_invalid_ascii = ['raoul@example¢¡.com']
cls.emails_valid = ['raoul¢¡@example.com', 'raoul@example.com']
def _reset_data(self):
self._init_mail_mock()
self.test_mail.write({'failure_reason': False, 'failure_type': False, 'state': 'outgoing'})
self.test_notification.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'})
@users('admin')
def test_mail_mail_attachment_access(self):
mail = self.env['mail.mail'].create({
'body_html': 'Test',
'email_to': 'test@example.com',
'partner_ids': [(4, self.user_employee.partner_id.id)],
'attachment_ids': [
(0, 0, {'name': 'file 1', 'datas': 'c2VjcmV0'}),
(0, 0, {'name': 'file 2', 'datas': 'c2VjcmV0'}),
(0, 0, {'name': 'file 3', 'datas': 'c2VjcmV0'}),
(0, 0, {'name': 'file 4', 'datas': 'c2VjcmV0'}),
],
})
def _patched_check(self, *args, **kwargs):
if self.env.is_superuser():
return
if any(attachment.name in ('file 2', 'file 4') for attachment in self):
raise AccessError('No access')
mail.invalidate_recordset()
new_attachment = self.env['ir.attachment'].create({
'name': 'new file',
'datas': 'c2VjcmV0',
})
with patch.object(type(self.env['ir.attachment']), 'check', _patched_check):
# Sanity check
self.assertEqual(mail.restricted_attachment_count, 2)
self.assertEqual(len(mail.unrestricted_attachment_ids), 2)
self.assertEqual(mail.unrestricted_attachment_ids.mapped('name'), ['file 1', 'file 3'])
# Add a new attachment
mail.write({
'unrestricted_attachment_ids': [Command.link(new_attachment.id)],
})
self.assertEqual(mail.restricted_attachment_count, 2)
self.assertEqual(len(mail.unrestricted_attachment_ids), 3)
self.assertEqual(mail.unrestricted_attachment_ids.mapped('name'), ['file 1', 'file 3', 'new file'])
self.assertEqual(len(mail.attachment_ids), 5)
# Remove an attachment
mail.write({
'unrestricted_attachment_ids': [Command.unlink(new_attachment.id)],
})
self.assertEqual(mail.restricted_attachment_count, 2)
self.assertEqual(len(mail.unrestricted_attachment_ids), 2)
self.assertEqual(mail.unrestricted_attachment_ids.mapped('name'), ['file 1', 'file 3'])
self.assertEqual(len(mail.attachment_ids), 4)
# Reset command
mail.invalidate_recordset()
mail.write({'unrestricted_attachment_ids': [Command.clear()]})
self.assertEqual(len(mail.unrestricted_attachment_ids), 0)
self.assertEqual(len(mail.attachment_ids), 2)
# Read in SUDO
mail.invalidate_recordset()
self.assertEqual(mail.sudo().restricted_attachment_count, 2)
self.assertEqual(len(mail.sudo().unrestricted_attachment_ids), 0)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_recipients(self):
""" Partner_ids is a field used from mail_message, but not from mail_mail. """
mail = self.env['mail.mail'].sudo().create({
'body_html': '<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)
mail = self.env['mail.mail'].sudo().create({
'body_html': '<p>Test</p>',
'email_to': 'test@example.com',
'recipient_ids': [(4, self.user_employee.partner_id.id)],
})
with self.mock_mail_gateway():
mail.send()
self.assertSentEmail(mail.env.user.partner_id, ['test@example.com'])
self.assertSentEmail(mail.env.user.partner_id, [self.user_employee.email_formatted])
self.assertEqual(len(self._mails), 2)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_recipients_cc(self):
""" Partner_ids is a field used from mail_message, but not from mail_mail. """
mail = self.env['mail.mail'].sudo().create({
'body_html': '<p>Test</p>',
'email_cc': 'test.cc.1@example.com, "Herbert" <test.cc.2@example.com>',
'email_to': 'test.rec.1@example.com, "Raoul" <test.rec.2@example.com>',
'recipient_ids': [(4, self.user_employee.partner_id.id)],
})
with self.mock_mail_gateway():
mail.send()
# note that formatting is lost for cc
self.assertSentEmail(mail.env.user.partner_id,
['test.rec.1@example.com', '"Raoul" <test.rec.2@example.com>'],
email_cc=['test.cc.1@example.com', '"Herbert" <test.cc.2@example.com>'])
# Mail: currently cc are put as copy of all sent emails (aka spam)
self.assertSentEmail(mail.env.user.partner_id, [self.user_employee.email_formatted],
email_cc=['test.cc.1@example.com', '"Herbert" <test.cc.2@example.com>'])
self.assertEqual(len(self._mails), 2)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_recipients_formatting(self):
""" Check support of email / formatted email """
mail = self.env['mail.mail'].sudo().create({
'author_id': False,
'body_html': '<p>Test</p>',
'email_cc': 'test.cc.1@example.com, "Herbert" <test.cc.2@example.com>',
'email_from': '"Ignasse" <test.from@example.com>',
'email_to': 'test.rec.1@example.com, "Raoul" <test.rec.2@example.com>',
})
with self.mock_mail_gateway():
mail.send()
# note that formatting is lost for cc
self.assertSentEmail('"Ignasse" <test.from@example.com>',
['test.rec.1@example.com', '"Raoul" <test.rec.2@example.com>'],
email_cc=['test.cc.1@example.com', '"Herbert" <test.cc.2@example.com>'])
self.assertEqual(len(self._mails), 1)
@mute_logger('odoo.addons.mail.models.mail_mail')
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@%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))
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
def test_mail_mail_schedule(self):
"""Test that a mail scheduled in the past/future are sent or not"""
now = datetime(2022, 6, 28, 14, 0, 0)
scheduled_datetimes = [
# falsy values
False, '', 'This is not a date format',
# datetimes (UTC/GMT +10 hours for Australia/Brisbane)
now, pytz.timezone('Australia/Brisbane').localize(now),
# string
(now - timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
(now + timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
(now + timedelta(days=1)).strftime("%H:%M:%S %d-%m-%Y"),
# tz: is actually 1 hour before now in UTC
(now + timedelta(hours=3)).strftime("%H:%M:%S %d-%m-%Y") + " +0400",
# tz: is actually 1 hour after now in UTC
(now + timedelta(hours=-3)).strftime("%H:%M:%S %d-%m-%Y") + " -0400",
]
expected_datetimes = [
False, False, False,
now, now - pytz.timezone('Australia/Brisbane').utcoffset(now),
now - timedelta(days=1), now + timedelta(days=1), now + timedelta(days=1),
now + timedelta(hours=-1),
now + timedelta(hours=1),
]
expected_states = [
# falsy values = send now
'sent', 'sent', 'sent',
'sent', 'sent',
'sent', 'outgoing', 'outgoing',
'sent', 'outgoing'
]
mails = self.env['mail.mail'].create([
{'body_html': '<p>Test</p>',
'email_to': 'test@example.com',
'scheduled_date': scheduled_datetime,
} for scheduled_datetime in scheduled_datetimes
])
for mail, expected_datetime, scheduled_datetime in zip(mails, expected_datetimes, scheduled_datetimes):
self.assertEqual(mail.scheduled_date, expected_datetime,
'Scheduled date: %s should be stored as %s, received %s' % (scheduled_datetime, expected_datetime, mail.scheduled_date))
self.assertEqual(mail.state, 'outgoing')
with freeze_time(now):
self.env['mail.mail'].process_email_queue()
for mail, expected_state in zip(mails, expected_states):
self.assertEqual(mail.state, expected_state)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_send_exceptions_origin(self):
""" Test various use case with exceptions and errors and see how they are
managed and stored at mail and notification level. """
mail, notification = self.test_mail, self.test_notification
# MailServer.build_email(): invalid from
self.env['ir.config_parameter'].set_param('mail.default.from', '')
self._reset_data()
with self.mock_mail_gateway(), mute_logger('odoo.addons.mail.models.mail_mail'):
mail.send(raise_exception=False)
self.assertFalse(self._mails[0]['email_from'])
self.assertEqual(
mail.failure_reason,
'You must either provide a sender address explicitly or configure using the combination of `mail.catchall.domain` and `mail.default.from` ICPs, in the server configuration file or with the --email-from startup parameter.')
self.assertFalse(mail.failure_type, 'Mail: void from: no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertEqual(
notification.failure_reason,
'You must either provide a sender address explicitly or configure using the combination of `mail.catchall.domain` and `mail.default.from` ICPs, in the server configuration file or with the --email-from startup parameter.')
self.assertEqual(notification.failure_type, 'unknown', 'Mail: void from: unknown failure type, should be updated')
self.assertEqual(notification.notification_status, 'exception')
# MailServer.send_email(): _prepare_email_message: unexpected ASCII
# Force catchall domain to void otherwise bounce is set to postmaster-odoo@domain
self.env['ir.config_parameter'].set_param('mail.catchall.domain', '')
self._reset_data()
mail.write({'email_from': 'strange@example¢¡.com'})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(self._mails[0]['email_from'], 'strange@example¢¡.com')
self.assertEqual(mail.failure_reason, "Malformed 'Return-Path' or 'From' address: strange@example¢¡.com - It should contain one valid plain ASCII email")
self.assertFalse(mail.failure_type, 'Mail: bugged from (ascii): no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertEqual(notification.failure_reason, "Malformed 'Return-Path' or 'From' address: strange@example¢¡.com - It should contain one valid plain ASCII email")
self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged from (ascii): unknown failure type, should be updated')
self.assertEqual(notification.notification_status, 'exception')
# MailServer.send_email(): _prepare_email_message: unexpected ASCII based on catchall domain
self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'domain¢¡.com')
self._reset_data()
mail.write({'email_from': 'test.user@example.com'})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(self._mails[0]['email_from'], 'test.user@example.com')
self.assertIn("Malformed 'Return-Path' or 'From' address: bounce.test@domain¢¡.com", mail.failure_reason)
self.assertFalse(mail.failure_type, 'Mail: bugged catchall domain (ascii): no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertEqual(notification.failure_reason, "Malformed 'Return-Path' or 'From' address: bounce.test@domain¢¡.com - It should contain one valid plain ASCII email")
self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged catchall domain (ascii): unknown failure type, should be updated')
self.assertEqual(notification.notification_status, 'exception')
# MailServer.send_email(): _prepare_email_message: Malformed 'Return-Path' or 'From' address
self.env['ir.config_parameter'].set_param('mail.catchall.domain', '')
self._reset_data()
mail.write({'email_from': 'robert'})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(self._mails[0]['email_from'], 'robert')
self.assertEqual(mail.failure_reason, "Malformed 'Return-Path' or 'From' address: robert - It should contain one valid plain ASCII email")
self.assertFalse(mail.failure_type, 'Mail: bugged from (ascii): no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertEqual(notification.failure_reason, "Malformed 'Return-Path' or 'From' address: robert - It should contain one valid plain ASCII email")
self.assertEqual(notification.failure_type, 'unknown', 'Mail: bugged from (ascii): unknown failure type, should be updated')
self.assertEqual(notification.notification_status, 'exception')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_send_exceptions_recipients_emails(self):
""" Test various use case with exceptions and errors and see how they are
managed and stored at mail and notification level. """
mail, notification = self.test_mail, self.test_notification
self.env['ir.config_parameter'].set_param('mail.catchall.domain', self.alias_domain)
self.env['ir.config_parameter'].set_param('mail.default.from', self.default_from)
# MailServer.send_email(): _prepare_email_message: missing To
for email_to in self.emails_falsy:
self._reset_data()
mail.write({'email_to': email_to})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
self.assertFalse(mail.failure_type, 'Mail: missing email_to: no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
if email_to == ' ':
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
self.assertEqual(notification.failure_type, 'mail_email_missing')
self.assertEqual(notification.notification_status, 'exception')
else:
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
self.assertEqual(notification.failure_type, False, 'Mail: missing email_to: notification is wrongly set as sent')
self.assertEqual(notification.notification_status, 'sent', 'Mail: missing email_to: notification is wrongly set as sent')
# MailServer.send_email(): _prepare_email_message: invalid To
for email_to, failure_type in zip(self.emails_invalid,
['mail_email_missing', 'mail_email_missing']):
self._reset_data()
mail.write({'email_to': email_to})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
self.assertFalse(mail.failure_type, 'Mail: invalid email_to: no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
self.assertEqual(notification.failure_type, failure_type, 'Mail: invalid email_to: missing instead of invalid')
self.assertEqual(notification.notification_status, 'exception')
# MailServer.send_email(): _prepare_email_message: invalid To (ascii)
for email_to in self.emails_invalid_ascii:
self._reset_data()
mail.write({'email_to': email_to})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
self.assertFalse(mail.failure_type, 'Mail: invalid (ascii) recipient partner: no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertEqual(notification.failure_type, 'mail_email_invalid')
self.assertEqual(notification.notification_status, 'exception')
# MailServer.send_email(): _prepare_email_message: ok To (ascii or just ok)
for email_to in self.emails_valid:
self._reset_data()
mail.write({'email_to': email_to})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertFalse(mail.failure_reason)
self.assertFalse(mail.failure_type)
self.assertEqual(mail.state, 'sent')
self.assertFalse(notification.failure_reason)
self.assertFalse(notification.failure_type)
self.assertEqual(notification.notification_status, 'sent')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_send_exceptions_recipients_partners(self):
""" Test various use case with exceptions and errors and see how they are
managed and stored at mail and notification level. """
mail, notification = self.test_mail, self.test_notification
mail.write({'email_from': 'test.user@test.example.com', 'email_to': False})
partners_falsy = self.env['res.partner'].create([
{'name': 'Name %s' % email, 'email': email}
for email in self.emails_falsy
])
partners_invalid = self.env['res.partner'].create([
{'name': 'Name %s' % email, 'email': email}
for email in self.emails_invalid
])
partners_invalid_ascii = self.env['res.partner'].create([
{'name': 'Name %s' % email, 'email': email}
for email in self.emails_invalid_ascii
])
partners_valid = self.env['res.partner'].create([
{'name': 'Name %s' % email, 'email': email}
for email in self.emails_valid
])
# void values
for partner in partners_falsy:
self._reset_data()
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
notification.write({'res_partner_id': partner.id})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
self.assertFalse(mail.failure_type, 'Mail: void recipient partner: no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
self.assertEqual(notification.failure_type, 'mail_email_invalid', 'Mail: void recipient partner: should be missing, not invalid')
self.assertEqual(notification.notification_status, 'exception')
# wrong values
for partner in partners_invalid:
self._reset_data()
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
notification.write({'res_partner_id': partner.id})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
self.assertFalse(mail.failure_type, 'Mail: invalid recipient partner: no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
self.assertEqual(notification.failure_type, 'mail_email_invalid')
self.assertEqual(notification.notification_status, 'exception')
# ascii ko
for partner in partners_invalid_ascii:
self._reset_data()
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
notification.write({'res_partner_id': partner.id})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertEqual(mail.failure_reason, 'Error without exception. Probably due to sending an email without computed recipients.')
self.assertFalse(mail.failure_type, 'Mail: invalid (ascii) recipient partner: no failure type, should be updated')
self.assertEqual(mail.state, 'exception')
self.assertEqual(notification.failure_type, 'mail_email_invalid')
self.assertEqual(notification.notification_status, 'exception')
# ascii ok or just ok
for partner in partners_valid:
self._reset_data()
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
notification.write({'res_partner_id': partner.id})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertFalse(mail.failure_reason)
self.assertFalse(mail.failure_type)
self.assertEqual(mail.state, 'sent')
self.assertFalse(notification.failure_reason)
self.assertFalse(notification.failure_type)
self.assertEqual(notification.notification_status, 'sent')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_send_exceptions_recipients_partners_mixed(self):
""" Test various use case with exceptions and errors and see how they are
managed and stored at mail and notification level. """
mail, notification = self.test_mail, self.test_notification
mail.write({'email_to': 'test@example.com'})
partners_falsy = self.env['res.partner'].create([
{'name': 'Name %s' % email, 'email': email}
for email in self.emails_falsy
])
partners_invalid = self.env['res.partner'].create([
{'name': 'Name %s' % email, 'email': email}
for email in self.emails_invalid
])
partners_valid = self.env['res.partner'].create([
{'name': 'Name %s' % email, 'email': email}
for email in self.emails_valid
])
# valid to, missing email for recipient or wrong email for recipient
for partner in partners_falsy + partners_invalid:
self._reset_data()
mail.write({'recipient_ids': [(5, 0), (4, partner.id)]})
notification.write({'res_partner_id': partner.id})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertFalse(mail.failure_reason, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
self.assertEqual(mail.state, 'sent', 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
self.assertFalse(notification.failure_reason, 'Mail: void email considered as invalid')
self.assertEqual(notification.failure_type, 'mail_email_invalid', 'Mail: void email considered as invalid')
self.assertEqual(notification.notification_status, 'exception')
# update to have valid partner and invalid partner
mail.write({'recipient_ids': [(5, 0), (4, partners_valid[1].id), (4, partners_falsy[0].id)]})
notification.write({'res_partner_id': partners_valid[1].id})
notification2 = notification.create({
'is_read': False,
'mail_mail_id': mail.id,
'mail_message_id': self.test_message.id,
'notification_type': 'email',
'res_partner_id': partners_falsy[0].id,
})
# missing to / invalid to
for email_to in self.emails_falsy + self.emails_invalid:
self._reset_data()
notification2.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'})
mail.write({'email_to': email_to})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertFalse(mail.failure_reason, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
self.assertEqual(mail.state, 'sent', 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
self.assertFalse(notification.failure_reason)
self.assertFalse(notification.failure_type)
self.assertEqual(notification.notification_status, 'sent')
self.assertFalse(notification2.failure_reason)
self.assertEqual(notification2.failure_type, 'mail_email_invalid')
self.assertEqual(notification2.notification_status, 'exception')
# buggy to (ascii)
for email_to in self.emails_invalid_ascii:
self._reset_data()
notification2.write({'failure_reason': False, 'failure_type': False, 'notification_status': 'ready'})
mail.write({'email_to': email_to})
with self.mock_mail_gateway():
mail.send(raise_exception=False)
self.assertFalse(mail.failure_type, 'Mail: at least one valid recipient, mail is sent to avoid send loops and spam')
self.assertEqual(mail.state, 'sent')
self.assertFalse(notification.failure_type)
self.assertEqual(notification.notification_status, 'sent')
self.assertEqual(notification2.failure_type, 'mail_email_invalid')
self.assertEqual(notification2.notification_status, 'exception')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_send_exceptions_raise_management(self):
""" Test various use case with exceptions and errors and see how they are
managed and stored at mail and notification level. """
mail, notification = self.test_mail, self.test_notification
mail.write({'email_from': 'test.user@test.example.com', 'email_to': 'test@example.com'})
# SMTP connecting issues
with self.mock_mail_gateway():
_connect_current = self.connect_mocked.side_effect
# classic errors that may be raised during sending, just to test their current support
for error, msg in [
(smtplib.SMTPServerDisconnected('SMTPServerDisconnected'), 'SMTPServerDisconnected'),
(smtplib.SMTPResponseException('code', 'SMTPResponseException'), 'code\nSMTPResponseException'),
(smtplib.SMTPNotSupportedError('SMTPNotSupportedError'), 'SMTPNotSupportedError'),
(smtplib.SMTPException('SMTPException'), 'SMTPException'),
(SSLError('SSLError'), 'SSLError'),
(gaierror('gaierror'), 'gaierror'),
(timeout('timeout'), 'timeout')]:
def _connect(*args, **kwargs):
raise error
self.connect_mocked.side_effect = _connect
mail.send(raise_exception=False)
self.assertEqual(mail.failure_reason, msg)
self.assertFalse(mail.failure_type)
self.assertEqual(mail.state, 'exception')
self.assertFalse(notification.failure_reason, 'Mail: failure reason not propagated')
self.assertEqual(notification.failure_type, 'mail_smtp')
self.assertEqual(notification.notification_status, 'exception')
self._reset_data()
self.connect_mocked.side_effect = _connect_current
# SMTP sending issues
with self.mock_mail_gateway():
_send_current = self.send_email_mocked.side_effect
self._reset_data()
mail.write({'email_to': 'test@example.com'})
# should always raise for those errors, even with raise_exception=False
for error, error_class in [
(smtplib.SMTPServerDisconnected("Some exception"), smtplib.SMTPServerDisconnected),
(MemoryError("Some exception"), MemoryError)]:
def _send_email(*args, **kwargs):
raise error
self.send_email_mocked.side_effect = _send_email
with self.assertRaises(error_class):
mail.send(raise_exception=False)
self.assertFalse(mail.failure_reason, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
self.assertFalse(mail.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
self.assertEqual(mail.state, 'outgoing', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
self.assertFalse(notification.failure_reason, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
self.assertFalse(notification.failure_type, 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
self.assertEqual(notification.notification_status, 'ready', 'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback')
# MailDeliveryException: should be catched; other issues are sub-catched under
# a MailDeliveryException and are catched
for error, msg in [
(MailDeliveryException("Some exception"), 'Some exception'),
(ValueError("Unexpected issue"), 'Unexpected issue')]:
def _send_email(*args, **kwargs):
raise error
self.send_email_mocked.side_effect = _send_email
self._reset_data()
mail.send(raise_exception=False)
self.assertEqual(mail.failure_reason, msg)
self.assertFalse(mail.failure_type, 'Mail: unlogged failure type to fix')
self.assertEqual(mail.state, 'exception')
self.assertEqual(notification.failure_reason, msg)
self.assertEqual(notification.failure_type, 'unknown', 'Mail: generic failure type')
self.assertEqual(notification.notification_status, 'exception')
self.send_email_mocked.side_effect = _send_current
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_send_server(self):
"""Test that the mails are send in batch.
Batch are defined by the mail server and the email from field.
"""
self.assertEqual(self.env['ir.mail_server']._get_default_from_address(), 'notifications@test.com')
mail_values = {
'body_html': '<p>Test</p>',
'email_to': 'user@example.com',
}
# Should be encapsulated in the notification email
mails = self.env['mail.mail'].create([{
**mail_values,
'email_from': 'test@unknown_domain.com',
} for _ in range(5)]) | self.env['mail.mail'].create([{
**mail_values,
'email_from': 'test_2@unknown_domain.com',
} for _ in range(5)])
# Should use the test_2 mail server
# Once with "user_1@test_2.com" as login
# Once with "user_2@test_2.com" as login
mails |= self.env['mail.mail'].create([{
**mail_values,
'email_from': 'user_1@test_2.com',
} for _ in range(5)]) | self.env['mail.mail'].create([{
**mail_values,
'email_from': 'user_2@test_2.com',
} for _ in range(5)])
# Mail server is forced
mails |= self.env['mail.mail'].create([{
**mail_values,
'email_from': 'user_1@test_2.com',
'mail_server_id': self.server_domain.id,
} for _ in range(5)])
with self.mock_smtplib_connection():
mails.send()
self.assertEqual(self.find_mail_server_mocked.call_count, 4, 'Must be called only once per "mail from" when the mail server is not forced')
self.assertEqual(len(self.emails), 25)
# Check call to the connect method to ensure that we authenticate
# to the right mail server with the right login
self.assertEqual(self.connect_mocked.call_count, 4, 'Must be called once per batch which share the same mail server and the same smtp from')
self.connect_mocked.assert_has_calls(
calls=[
call(smtp_from='notifications@test.com', mail_server_id=self.server_notification.id),
call(smtp_from='user_1@test_2.com', mail_server_id=self.server_domain_2.id),
call(smtp_from='user_2@test_2.com', mail_server_id=self.server_domain_2.id),
call(smtp_from='user_1@test_2.com', mail_server_id=self.server_domain.id),
],
any_order=True,
)
self.assert_email_sent_smtp(message_from='"test" <notifications@test.com>',
emails_count=5, from_filter=self.server_notification.from_filter)
self.assert_email_sent_smtp(message_from='"test_2" <notifications@test.com>',
emails_count=5, from_filter=self.server_notification.from_filter)
self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter)
self.assert_email_sent_smtp(message_from='user_2@test_2.com', emails_count=5, from_filter=self.server_domain_2.from_filter)
self.assert_email_sent_smtp(message_from='user_1@test_2.com', emails_count=5, from_filter=self.server_domain.from_filter)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_values_email_formatted(self):
""" Test outgoing email values, with formatting """
customer = self.env['res.partner'].create({
'name': 'Tony Customer',
'email': '"Formatted Emails" <tony.customer@test.example.com>',
})
mail = self.env['mail.mail'].create({
'body_html': '<p>Test</p>',
'email_cc': '"Ignasse, le Poilu" <test.cc.1@test.example.com>',
'email_to': '"Raoul, le Grand" <test.email.1@test.example.com>, "Micheline, l\'immense" <test.email.2@test.example.com>',
'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)]
})
with self.mock_mail_gateway():
mail.send()
self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient')
self.assertEqual(
sorted(sorted(_mail['email_to']) for _mail in self._mails),
sorted([sorted(['"Raoul, le Grand" <test.email.1@test.example.com>', '"Micheline, l\'immense" <test.email.2@test.example.com>']),
[tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))],
[tools.formataddr(("Tony Customer", 'tony.customer@test.example.com'))]
]),
'Mail: formatting issues should have been removed as much as possible'
)
# Currently broken: CC are added to ALL emails (spammy)
self.assertEqual(
[_mail['email_cc'] for _mail in self._mails],
[['"Ignasse, le Poilu" <test.cc.1@test.example.com>']] * 3,
'Mail: currently always removing formatting in email_cc'
)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_values_email_multi(self):
""" Test outgoing email values, with email field holding multi emails """
# Multi
customer = self.env['res.partner'].create({
'name': 'Tony Customer',
'email': 'tony.customer@test.example.com, norbert.customer@test.example.com',
})
mail = self.env['mail.mail'].create({
'body_html': '<p>Test</p>',
'email_cc': 'test.cc.1@test.example.com, test.cc.2@test.example.com',
'email_to': 'test.email.1@test.example.com, test.email.2@test.example.com',
'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)]
})
with self.mock_mail_gateway():
mail.send()
self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient')
self.assertEqual(
sorted(sorted(_mail['email_to']) for _mail in self._mails),
sorted([sorted(['test.email.1@test.example.com', 'test.email.2@test.example.com']),
[tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))],
sorted([tools.formataddr(("Tony Customer", 'tony.customer@test.example.com')),
tools.formataddr(("Tony Customer", 'norbert.customer@test.example.com'))]),
]),
'Mail: formatting issues should have been removed as much as possible (multi emails in a single address are managed '
'like separate emails when sending with recipient_ids'
)
# Currently broken: CC are added to ALL emails (spammy)
self.assertEqual(
[_mail['email_cc'] for _mail in self._mails],
[['test.cc.1@test.example.com', 'test.cc.2@test.example.com']] * 3,
)
# Multi + formatting
customer = self.env['res.partner'].create({
'name': 'Tony Customer',
'email': 'tony.customer@test.example.com, "Norbert Customer" <norbert.customer@test.example.com>',
})
mail = self.env['mail.mail'].create({
'body_html': '<p>Test</p>',
'email_cc': 'test.cc.1@test.example.com, test.cc.2@test.example.com',
'email_to': 'test.email.1@test.example.com, test.email.2@test.example.com',
'recipient_ids': [(4, self.user_employee.partner_id.id), (4, customer.id)]
})
with self.mock_mail_gateway():
mail.send()
self.assertEqual(len(self._mails), 3, 'Mail: sent 3 emails: 1 for email_to, 1 / recipient')
self.assertEqual(
sorted(sorted(_mail['email_to']) for _mail in self._mails),
sorted([sorted(['test.email.1@test.example.com', 'test.email.2@test.example.com']),
[tools.formataddr((self.user_employee.name, self.user_employee.email_normalized))],
sorted([tools.formataddr(("Tony Customer", 'tony.customer@test.example.com')),
tools.formataddr(("Tony Customer", 'norbert.customer@test.example.com'))]),
]),
'Mail: formatting issues should have been removed as much as possible (multi emails in a single address are managed '
'like separate emails when sending with recipient_ids (and partner name is always used as name part)'
)
# Currently broken: CC are added to ALL emails (spammy)
self.assertEqual(
[_mail['email_cc'] for _mail in self._mails],
[['test.cc.1@test.example.com', 'test.cc.2@test.example.com']] * 3,
)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mail_mail_values_email_unicode(self):
""" Unicode should be fine. """
mail = self.env['mail.mail'].create({
'body_html': '<p>Test</p>',
'email_cc': 'test.😊.cc@example.com',
'email_to': 'test.😊@example.com',
})
with self.mock_mail_gateway():
mail.send()
self.assertEqual(len(self._mails), 1)
self.assertEqual(self._mails[0]['email_cc'], ['test.😊.cc@example.com'])
self.assertEqual(self._mails[0]['email_to'], ['test.😊@example.com'])
@users('admin')
def test_mail_mail_values_email_uppercase(self):
""" Test uppercase support when comparing emails, notably due to
'send_validated_to' introduction that checks emails before sending them. """
customer = self.env['res.partner'].create({
'name': 'Uppercase Partner',
'email': 'Uppercase.Partner.youpie@example.gov.uni',
})
for recipient_values, (exp_to, exp_cc) in zip(
[
{'email_to': 'Uppercase.Customer.to@example.gov.uni'},
{'email_to': '"Formatted Customer" <Uppercase.Customer.to@example.gov.uni>'},
{'recipient_ids': [(4, customer.id)], 'email_cc': 'Uppercase.Customer.cc@example.gov.uni'},
], [
(['uppercase.customer.to@example.gov.uni'], []),
(['"Formatted Customer" <uppercase.customer.to@example.gov.uni>'], []),
(['"Uppercase Partner" <uppercase.partner.youpie@example.gov.uni>'], ['uppercase.customer.cc@example.gov.uni']),
]
):
with self.subTest(values=recipient_values):
mail = self.env['mail.mail'].create({
'body_html': '<p>Test</p>',
'email_from': '"Forced From" <Forced.From@test.example.com>',
**recipient_values,
})
with self.mock_mail_gateway():
mail.send()
self.assertSentEmail('"Forced From" <forced.from@test.example.com>', exp_to, email_cc=exp_cc)
@tagged('mail_mail')
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>',
'is_notification': True,
'state': 'outgoing',
'recipient_ids': [(4, self.partner.id)]
})
mail_message = mail.mail_message_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_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_notification 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_notification 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()
mail.unlink()
(mail_message | message).unlink()
self.partner.unlink()
self.env.cr.commit()
# because we committed the cursor, the savepoint of the test method is
# gone, and this would break TransactionCase cleanups
self.cr.execute('SAVEPOINT test_%d' % self._savepoint_id)

View file

@ -0,0 +1,53 @@
# -*- 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
@tagged('mail_management')
class TestMailManagement(TestMailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestMailManagement, cls).setUpClass()
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test'})
cls._reset_mail_context(cls.test_record)
cls.msg = cls.test_record.message_post(body='TEST BODY', author_id=cls.partner_employee.id)
cls.notif_p1 = cls.env['mail.notification'].create({
'author_id': cls.msg.author_id.id,
'mail_message_id': cls.msg.id,
'res_partner_id': cls.partner_1.id,
'notification_type': 'email',
'notification_status': 'exception',
'failure_type': 'mail_smtp',
})
cls.notif_p2 = cls.env['mail.notification'].create({
'author_id': cls.msg.author_id.id,
'mail_message_id': cls.msg.id,
'res_partner_id': cls.partner_2.id,
'notification_type': 'email',
'notification_status': 'bounce',
'failure_type': 'unknown',
})
cls.partner_3 = cls.env['res.partner'].create({
'name': 'Partner3',
'email': 'partner3@example.com',
})
cls.notif_p3 = cls.env['mail.notification'].create({
'author_id': cls.msg.author_id.id,
'mail_message_id': cls.msg.id,
'res_partner_id': cls.partner_3.id,
'notification_type': 'email',
'notification_status': 'sent',
'failure_type': None,
})
def test_mail_notify_cancel(self):
self._reset_bus()
self.test_record.with_user(self.user_employee).notify_cancel_by_type('email')
self.assertEqual((self.notif_p1 | self.notif_p2 | self.notif_p3).mapped('notification_status'),
['canceled', 'canceled', 'sent'])
self.assertMessageBusNotifications(self.msg)

View file

@ -0,0 +1,307 @@
# -*- 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.exceptions import UserError
from odoo.tools import is_html_empty, mute_logger, formataddr
from odoo.tests import tagged, users
@tagged('mail_message')
class TestMessageValues(TestMailCommon):
@classmethod
def setUpClass(cls):
super(TestMessageValues, cls).setUpClass()
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)
@users('employee')
def test_empty_message(self):
""" Test that message is correctly considered as empty (see `_filter_empty()`).
Message considered as empty if:
- no body or empty body
- AND no subtype or no subtype description
- AND no tracking values
- AND no attachment
Check _update_content behavior when voiding messages (cleanup side
records: stars, notifications).
"""
note_subtype = self.env.ref('mail.mt_note')
_attach_1 = self.env['ir.attachment'].with_user(self.user_employee).create({
'name': 'Attach1',
'datas': 'bWlncmF0aW9uIHRlc3Q=',
'res_id': 0,
'res_model': 'mail.compose.message',
})
record = self.env['mail.test.track'].create({'name': 'EmptyTesting'})
self.flush_tracking()
record.message_subscribe(partner_ids=self.partner_admin.ids, subtype_ids=note_subtype.ids)
message = record.message_post(
attachment_ids=_attach_1.ids,
body='Test',
message_type='comment',
subtype_id=note_subtype.id,
)
message.write({'starred_partner_ids': [(4, self.partner_admin.id)]})
# check content
self.assertEqual(len(message.attachment_ids), 1)
self.assertFalse(is_html_empty(message.body))
self.assertEqual(len(message.sudo().notification_ids), 1)
self.assertEqual(message.notified_partner_ids, self.partner_admin)
self.assertEqual(message.starred_partner_ids, self.partner_admin)
self.assertFalse(message.sudo().tracking_value_ids)
# Reset body case
record._message_update_content(message, '<p><br /></p>', attachment_ids=message.attachment_ids.ids)
self.assertTrue(is_html_empty(message.body))
self.assertFalse(message.sudo()._filter_empty(), 'Still having attachments')
# Subtype content
note_subtype.sudo().write({'description': 'Very important discussions'})
record._message_update_content(message, '', [])
self.assertFalse(message.attachment_ids)
self.assertEqual(message.notified_partner_ids, self.partner_admin)
self.assertEqual(message.starred_partner_ids, self.partner_admin)
self.assertFalse(message.sudo()._filter_empty(), 'Subtype with description')
# Completely void now
note_subtype.sudo().write({'description': ''})
self.assertEqual(message.sudo()._filter_empty(), message)
record._message_update_content(message, '', [])
self.assertFalse(message.notified_partner_ids)
self.assertFalse(message.starred_partner_ids)
# test tracking values
record.write({'user_id': self.user_admin.id})
self.flush_tracking()
tracking_message = record.message_ids[0]
self.assertFalse(tracking_message.attachment_ids)
self.assertTrue(is_html_empty(tracking_message.body))
self.assertFalse(tracking_message.subtype_id.description)
self.assertFalse(tracking_message.sudo()._filter_empty(), 'Has tracking values')
with self.assertRaises(UserError, msg='Tracking values prevent from updating content'):
record._message_update_content(tracking_message, '', [])
@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.
self.env.flush_all()
self.env.invalidate_all()
res = message.with_user(self.user_employee).message_format()
self.assertEqual(res[0].get('record_name'), 'Test1')
record1.write({"name": "Test2"})
res = message.with_user(self.user_employee).message_format()
self.assertEqual(res[0].get('record_name'), 'Test2')
def test_mail_message_values_body_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)
self.assertEqual(
msg.body,
'<p>taratata <img src="/web/image/{attachment.id}?access_token={attachment.access_token}" alt="image0" width="2"> '
'<img src="/web/image/{attachment.id}?access_token={attachment.access_token}" alt="image0" width="2"></p>'.format(attachment=msg.attachment_ids[0])
)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.models')
@users('employee')
def test_mail_message_values_fromto_long_name(self):
""" Long headers may break in python if above 68 chars for certain
DKIM verification stacks as folding is not done correctly
(see ``_notify_get_reply_to_formatted_email`` docstring
+ commit linked to this test). """
# name would make it blow up: keep only email
test_record = self.env['mail.test.container'].browse(self.alias_record.ids)
test_record.write({
'name': 'Super Long Name That People May Enter "Even with an internal quoting of stuff"'
})
msg = self.env['mail.message'].create({
'model': test_record._name,
'res_id': test_record.id
})
reply_to_email = f"{test_record.alias_name}@{self.alias_domain}"
self.assertEqual(msg.reply_to, reply_to_email,
'Reply-To: use only email when formataddr > 68 chars')
# name + company_name would make it blow up: keep record_name in formatting
self.company_admin.name = "Company name being about 33 chars"
test_record.write({'name': 'Name that would be more than 68 with company name'})
msg = self.env['mail.message'].create({
'model': test_record._name,
'res_id': test_record.id
})
self.assertEqual(msg.reply_to, formataddr((test_record.name, reply_to_email)),
'Reply-To: use recordname as name in format if recordname + company > 68 chars')
# no record_name: keep company_name in formatting if ok
test_record.write({'name': ''})
msg = self.env['mail.message'].create({
'model': test_record._name,
'res_id': test_record.id
})
self.assertEqual(msg.reply_to, formataddr((self.env.user.company_id.name, reply_to_email)),
'Reply-To: use company as name in format when no record name and still < 68 chars')
# no record_name and company_name make it blow up: keep only email
self.env.user.company_id.write({'name': 'Super Long Name That People May Enter "Even with an internal quoting of stuff"'})
msg = self.env['mail.message'].create({
'model': test_record._name,
'res_id': test_record.id
})
self.assertEqual(msg.reply_to, reply_to_email,
'Reply-To: use only email when formataddr > 68 chars')
# whatever the record and company names, email is too long: keep only email
test_record.write({
'alias_name': 'Waaaay too long alias name that should make any reply-to blow the 68 characters limit',
'name': 'Short',
})
self.env.user.company_id.write({'name': 'Comp'})
sanitized_alias_name = 'waaaay-too-long-alias-name-that-should-make-any-reply-to-blow-the-68-characters-limit'
msg = self.env['mail.message'].create({
'model': test_record._name,
'res_id': test_record.id
})
self.assertEqual(msg.reply_to, f"{sanitized_alias_name}@{self.alias_domain}",
'Reply-To: even a long email is ok as only formataddr is problematic')
@mute_logger('odoo.models.unlink')
def test_mail_message_values_fromto_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_fromto_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_fromto_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_fromto_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_fromto_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_fromto_reply_to_force_new(self):
msg = self.Message.create({
'model': 'mail.test.container',
'res_id': self.alias_record.id,
'reply_to_force_new': 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])

View file

@ -0,0 +1,778 @@
import base64
from unittest.mock import patch
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_mail.models.mail_test_access import MailTestAccess
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
from odoo.tests import tagged
class MessageAccessCommon(TestMailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_public = mail_new_test_user(
cls.env,
groups='base.group_public',
login='bert',
name='Bert Tartignole',
)
cls.user_portal = mail_new_test_user(
cls.env,
groups='base.group_portal',
login='chell',
name='Chell Gladys',
)
cls.user_portal_2 = mail_new_test_user(
cls.env,
groups='base.group_portal',
login='portal2',
name='Chell Gladys',
)
(
cls.record_public, cls.record_portal, cls.record_portal_ro,
cls.record_followers,
cls.record_internal, cls.record_internal_ro,
cls.record_admin
) = cls.env['mail.test.access'].create([
{'access': 'public', 'name': 'Public Record'},
{'access': 'logged', 'name': 'Portal Record'},
{'access': 'logged_ro', 'name': 'Portal RO Record'},
{'access': 'followers', 'name': 'Followers Record'},
{'access': 'internal', 'name': 'Internal Record'},
{'access': 'internal_ro', 'name': 'Internal Readonly Record'},
{'access': 'admin', 'name': 'Admin Record'},
])
for record in (cls.record_public + cls.record_portal + cls.record_portal_ro + cls.record_followers +
cls.record_internal + cls.record_internal_ro + cls.record_admin):
record.message_post(
body='Test Comment',
message_type='comment',
subtype_id=cls.env.ref('mail.mt_comment').id,
)
record.message_post(
body='Test Answer',
message_type='comment',
subtype_id=cls.env.ref('mail.mt_comment').id,
)
@tagged('mail_message', 'security', 'post_install', '-at_install')
class TestMailMessageAccess(MessageAccessCommon):
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
def test_assert_initial_values(self):
""" Just ensure tests data """
for record in (
self.record_public + self.record_portal + self.record_portal_ro + self.record_followers +
self.record_internal + self.record_internal_ro + self.record_admin):
self.assertFalse(record.message_follower_ids)
self.assertEqual(len(record.message_ids), 3)
for index, msg in enumerate(record.message_ids):
body = ['<p>Test Answer</p>', '<p>Test Comment</p>', '<p>Mail Access Test created</p>'][index]
message_type = ['comment', 'comment', 'notification'][index]
subtype_id = [self.env.ref('mail.mt_comment'), self.env.ref('mail.mt_comment'), self.env.ref('mail.mt_note')][index]
self.assertEqual(msg.author_id, self.partner_root)
self.assertEqual(msg.body, body)
self.assertEqual(msg.message_type, message_type)
self.assertFalse(msg.notified_partner_ids)
self.assertFalse(msg.partner_ids)
self.assertEqual(msg.subtype_id, subtype_id)
# public user access check
for allowed in self.record_public:
allowed.with_user(self.user_public).read(['name'])
for forbidden in self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro + self.record_admin:
with self.assertRaises(AccessError):
forbidden.with_user(self.user_public).read(['name'])
for forbidden in self.record_public + self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro + self.record_admin:
with self.assertRaises(AccessError):
forbidden.with_user(self.user_public).write({'name': 'Update'})
# portal user access check
for allowed in self.record_public + self.record_portal + self.record_portal_ro:
allowed.with_user(self.user_portal).read(['name'])
for forbidden in self.record_internal + self.record_internal_ro + self.record_admin:
with self.assertRaises(AccessError):
forbidden.with_user(self.user_portal).read(['name'])
for allowed in self.record_portal:
allowed.with_user(self.user_portal).write({'name': 'Update'})
for forbidden in self.record_public + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro + self.record_admin:
with self.assertRaises(AccessError):
forbidden.with_user(self.user_portal).write({'name': 'Update'})
self.record_followers.message_subscribe(self.user_portal.partner_id.ids)
self.record_followers.with_user(self.user_portal).read(['name'])
self.record_followers.with_user(self.user_portal).write({'name': 'Update'})
# internal user access check
for allowed in self.record_public + self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro:
allowed.with_user(self.user_employee).read(['name'])
for forbidden in self.record_admin:
with self.assertRaises(AccessError):
forbidden.with_user(self.user_employee).read(['name'])
for allowed in self.record_public + self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal:
allowed.with_user(self.user_employee).write({'name': 'Update'})
for forbidden in self.record_internal_ro + self.record_admin:
with self.assertRaises(AccessError):
forbidden.with_user(self.user_employee).write({'name': 'Update'})
# elevated user access check
for allowed in self.record_public + self.record_portal + self.record_portal_ro + self.record_followers + self.record_internal + self.record_internal_ro + self.record_admin:
allowed.with_user(self.user_admin).read(['name'])
# ------------------------------------------------------------
# CREATE
# - Criterions
# - "private message" (no model, no res_id) -> deprecated
# - follower of document
# - document-based (write or create, using '_get_mail_message_access'
# hence '_mail_post_access' by default)
# - notified of parent message
# ------------------------------------------------------------
@mute_logger('odoo.addons.base.models.ir_rule')
def test_access_create(self):
""" Test 'group_user' creation rules """
# prepare 'notified of parent' condition
admin_msg = self.record_admin.message_ids[0]
admin_msg.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
# prepare 'followers' condition
record_admin_fol = self.env['mail.test.access'].create({
'access': 'admin',
'name': 'Admin Record Follower',
})
record_admin_fol.message_subscribe(self.user_employee.partner_id.ids)
for record, msg_vals, should_crash, reason in [
# private-like
(self.env["mail.test.access"], {}, False, 'Private message like is ok'),
# document based
(self.record_internal, {}, False, 'W Access on record'),
(self.record_internal_ro, {}, True, 'No W Access on record'),
(self.record_admin, {}, True, 'No access on record (and not notified on first message)'),
(record_admin_fol, {
'reply_to': 'avoid.catchall@my.test.com', # otherwise crashes
}, False, 'Followers > no access on record'),
# parent based
(self.record_admin, { # note: force reply_to normally computed by message_post avoiding ACLs issues
'parent_id': admin_msg.id,
}, False, 'No access on record but reply to notified parent'),
]:
with self.subTest(record=record, msg_vals=msg_vals, reason=reason):
if should_crash:
with self.assertRaises(AccessError):
self.env['mail.message'].with_user(self.user_employee).create({
'model': record._name if record else False,
'res_id': record.id if record else False,
'body': 'Test',
**msg_vals,
})
if record:
with self.assertRaises(AccessError):
record.with_user(self.user_employee).message_post(
body='Test',
subtype_id=self.env.ref('mail.mt_comment').id,
)
else:
_message = self.env['mail.message'].with_user(self.user_employee).create({
'model': record._name if record else False,
'res_id': record.id if record else False,
'body': 'Test',
**msg_vals,
})
if record:
# TDE note: due to parent_id flattening, doing message_post
# with parent_id which should allow posting crashes, as
# parent_id is changed to an older message the employee cannot
# access. Won't fix that in stable.
if record == self.record_admin and 'parent_id' in msg_vals:
continue
record.with_user(self.user_employee).message_post(
body='Test',
subtype_id=self.env.ref('mail.mt_comment').id,
**msg_vals,
)
def test_access_create_customized(self):
""" Test '_get_mail_message_access' support """
record = self.env['mail.test.access.custo'].with_user(self.user_employee).create({'name': 'Open'})
for user in self.user_employee + self.user_portal:
_message = record.message_post(
body='A message',
subtype_id=self.env.ref('mail.mt_comment').id,
)
# lock -> see '_get_mail_message_access'
record.write({'is_locked': True})
for user in self.user_employee + self.user_portal:
with self.assertRaises(AccessError):
_message_portal = record.with_user(self.user_portal).message_post(
body='Another portal message',
subtype_id=self.env.ref('mail.mt_comment').id,
)
def test_access_create_mail_post_access(self):
""" Test 'mail_post_access' support that allows creating a message with
other rights than 'write' access on document """
for post_value, should_crash in [
('read', False),
('write', True),
]:
with self.subTest(post_value=post_value):
with patch.object(MailTestAccess, '_mail_post_access', post_value):
if should_crash:
with self.assertRaises(AccessError):
self.env['mail.message'].with_user(self.user_employee).create({
'model': self.record_internal_ro._name,
'res_id': self.record_internal_ro.id,
'body': 'Test',
})
else:
self.env['mail.message'].with_user(self.user_employee).create({
'model': self.record_internal_ro._name,
'res_id': self.record_internal_ro.id,
'body': 'Test',
})
@mute_logger('odoo.addons.base.models.ir_rule')
def test_access_create_portal(self):
""" Test group_portal creation rules """
# prepare 'notified of parent' condition
admin_msg = self.record_admin.message_ids[-1]
admin_msg.write({'partner_ids': [(4, self.user_portal.partner_id.id)]})
# prepare 'followers' condition
record_admin_fol = self.env['mail.test.access'].create({
'access': 'admin',
'name': 'Admin Record',
})
record_admin_fol.message_subscribe(self.user_portal.partner_id.ids)
for record, msg_vals, should_crash, reason in [
# private-like
(self.env["mail.test.access"], {}, False, 'Private message like is ok'),
# document based
(self.record_portal, {}, False, 'W Access on record'),
(self.record_portal_ro, {}, True, 'No W Access on record'),
(self.record_internal, {}, True, 'No R/W Access on record'),
(record_admin_fol, {
'reply_to': 'avoid.catchall@my.test.com', # otherwise crashes
}, False, 'Followers > no access on record'),
# parent based
(self.record_admin, {
'parent_id': admin_msg.id,
}, False, 'No access on record but reply to notified parent'),
]:
with self.subTest(record=record, msg_vals=msg_vals, reason=reason):
if should_crash:
with self.assertRaises(AccessError):
self.env['mail.message'].with_user(self.user_portal).create({
'model': record._name if record else False,
'res_id': record.id if record else False,
'body': 'Test',
**msg_vals,
})
else:
_message = self.env['mail.message'].with_user(self.user_portal).create({
'model': record._name if record else False,
'res_id': record.id if record else False,
'body': 'Test',
**msg_vals,
})
# check '_mail_post_access', reducing W to R
with patch.object(MailTestAccess, '_mail_post_access', 'read'):
_message = self.env['mail.message'].with_user(self.user_portal).create({
'model': self.record_portal._name,
'res_id': self.record_portal.id,
'body': 'Test',
})
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
def test_access_create_public(self):
""" Public can never create messages """
for record in [
self.env['mail.test.access'], # old private message: no document
self.record_public, # read access
self.record_portal, # read access
]:
with self.subTest(record=record):
# can never create message, simple
with self.assertRaises(AccessError):
self.env['mail.message'].with_user(self.user_public).create({
'model': record._name if record else False,
'res_id': record.id if record else False,
'body': 'Test',
})
@mute_logger('odoo.tests')
def test_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. This cause issues notably when computing
references, checking ancestors message_ids. """
test_record = self.env['mail.test.simple'].with_context(self._test_context).create({
'email_from': 'ignasse@example.com',
'name': 'Test',
})
partner_1 = self.env['res.partner'].create({
'name': 'Not Jitendra Prajapati',
'email': 'not.jitendra@mycompany.example.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>',
message_type='comment',
subject='Subject',
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
with self.mock_mail_gateway():
new_msg = test_record.with_user(self.user_portal).message_post(
body='<p>This is Second Message</p>',
subject='Subject',
parent_id=message.id,
mail_auto_delete=False,
message_type='comment',
subtype_xmlid='mail.mt_comment',
)
self.assertEqual(new_msg.sudo().parent_id, message)
new_mail = self.env['mail.mail'].sudo().search([
('mail_message_id', '=', new_msg.id),
])
self.assertEqual(
new_mail.references, f'{message.message_id} {new_msg.message_id}',
'References should not include message parent message_id, even if internal note, to help thread formation')
self.assertTrue(new_mail)
self.assertEqual(new_msg.parent_id, message)
# ------------------------------------------------------------
# READ
# - Criterions
# - author
# - recipients / notified
# - document-based: read, using '_get_mail_message_access'
# - share users: limited to 'not internal' (flag or subtype)
# ------------------------------------------------------------
def test_access_read(self):
""" Read access check for internal users. """
for msg, msg_vals, should_crash, reason in [
# document based
(self.record_internal.message_ids[0], {}, False, 'R Access on record'),
(self.record_internal_ro.message_ids[0], {}, False, 'R Access on record'),
(self.record_admin.message_ids[0], {}, True, 'No access on record'),
# author
(self.record_admin.message_ids[0], {
'author_id': self.user_employee.partner_id.id,
}, False, 'Author > no access on record'),
# notified
(self.record_admin.message_ids[0], {
'notification_ids': [(0, 0, {
'res_partner_id': self.user_employee.partner_id.id,
})],
}, False, 'Notified > no access on record'),
(self.record_admin.message_ids[0], {
'partner_ids': [(4, self.user_employee.partner_id.id)],
}, False, 'Recipients > no access on record'),
]:
original_vals = {
'author_id': msg.author_id.id,
'notification_ids': [(6, 0, {})],
'parent_id': msg.parent_id.id,
}
with self.subTest(msg=msg, reason=reason):
if msg_vals:
msg.write(msg_vals)
if should_crash:
with self.assertRaises(AccessError):
msg.with_user(self.user_employee).read(['body'])
else:
msg.with_user(self.user_employee).read(['body'])
if msg_vals:
msg.write(original_vals)
def test_access_read_portal(self):
""" Read access check for portal users """
for msg, msg_vals, should_crash, reason in [
# document based
(self.record_portal.message_ids[0], {}, False, 'Access on record'),
(self.record_internal.message_ids[0], {}, True, 'No access on record'),
# author
(self.record_internal.message_ids[0], {
'author_id': self.user_portal.partner_id.id,
}, False, 'Author > no access on record'),
# notified
(self.record_admin.message_ids[0], {
'notification_ids': [(0, 0, {
'res_partner_id': self.user_portal.partner_id.id,
})],
}, False, 'Notified > no access on record'),
# forbidden
(self.record_portal.message_ids[0], {
'subtype_id': self.env.ref('mail.mt_note').id,
}, True, 'Note cannot be read by portal users'),
(self.record_portal.message_ids[0], {
'is_internal': True,
}, True, 'Internal message cannot be read by portal users'),
]:
original_vals = {
'author_id': msg.author_id.id,
'is_internal': False,
'notification_ids': [(6, 0, {})],
'parent_id': msg.parent_id.id,
'subtype_id': self.env.ref('mail.mt_comment').id,
}
with self.subTest(msg=msg, reason=reason):
if msg_vals:
msg.write(msg_vals)
if should_crash:
with self.assertRaises(AccessError):
msg.with_user(self.user_portal).read(['body'])
else:
msg.with_user(self.user_portal).read(['body'])
if msg_vals:
msg.write(original_vals)
def test_access_read_public(self):
""" Read access check for public users """
for msg, msg_vals, should_crash, reason in [
# document based
(self.record_public.message_ids[0], {}, False, 'Access on record'),
(self.record_portal.message_ids[0], {}, True, 'No access on record'),
# author
(self.record_internal.message_ids[0], {
'author_id': self.user_public.partner_id.id,
}, False, 'Author > no access on record'),
# notified
(self.record_admin.message_ids[0], {
'notification_ids': [(0, 0, {
'res_partner_id': self.user_public.partner_id.id,
})],
}, False, 'Notified > no access on record'),
# forbidden
(self.record_public.message_ids[0], {
'subtype_id': self.env.ref('mail.mt_note').id,
}, True, 'Note cannot be read by public users'),
(self.record_public.message_ids[0], {
'is_internal': True,
}, True, 'Internal message cannot be read by public users'),
]:
original_vals = {
'author_id': msg.author_id.id,
'is_internal': False,
'notification_ids': [(6, 0, {})],
'parent_id': msg.parent_id.id,
'subtype_id': self.env.ref('mail.mt_comment').id,
}
with self.subTest(msg=msg, reason=reason):
if msg_vals:
msg.write(msg_vals)
if should_crash:
with self.assertRaises(AccessError):
msg.with_user(self.user_public).read(['body'])
else:
msg.with_user(self.user_public).read(['body'])
if msg_vals:
msg.write(original_vals)
# ------------------------------------------------------------
# UNLINK
# - Criterion: document-based (write or create), using '_get_mail_message_access'
# ------------------------------------------------------------
def test_access_unlink(self):
""" Unlink access check for internal users """
for msg, msg_vals, should_crash, reason in [
# document based
(self.record_portal.message_ids[0], {}, False, 'W Access on record'),
(self.record_internal_ro.message_ids[0], {}, True, 'R Access on record'),
# notified
(self.record_admin.message_ids[0], {
'notification_ids': [(0, 0, {
'res_partner_id': self.user_portal.partner_id.id,
})],
}, True, 'Even notified, cannot remove'),
]:
with self.subTest(msg=msg, reason=reason):
if msg_vals:
msg.write(msg_vals)
if should_crash:
with self.assertRaises(AccessError):
msg.with_user(self.user_portal).unlink()
else:
msg.with_user(self.user_portal).unlink()
def test_access_unlink_portal(self):
""" Unlink access check for portal users. """
for msg, msg_vals, should_crash, reason in [
# document based
(self.record_portal.message_ids[0], {}, False, 'W Access on record but unlink limited'),
(self.record_portal_ro.message_ids[0], {}, True, 'R Access on record'),
# notified
(self.record_admin.message_ids[0], {
'notification_ids': [(0, 0, {
'res_partner_id': self.user_portal.partner_id.id,
})],
}, True, 'Even notified, cannot remove'),
]:
with self.subTest(msg=msg, reason=reason):
if msg_vals:
msg.write(msg_vals)
if should_crash:
with self.assertRaises(AccessError):
msg.with_user(self.user_portal).unlink()
else:
msg.with_user(self.user_portal).unlink()
# ------------------------------------------------------------
# WRITE
# - Criterions
# - author
# - recipients / notified
# - document-based (write or create), using '_get_mail_message_access'
# ------------------------------------------------------------
def test_access_write(self):
""" Test updating message content as internal user """
for msg, msg_vals, should_crash, reason in [
# document based
(self.record_internal.message_ids[0], {}, False, 'W Access on record'),
(self.record_internal_ro.message_ids[0], {}, True, 'No W Access on record'),
(self.record_admin.message_ids[0], {}, True, 'No access on record'),
# author
(self.record_admin.message_ids[0], {
'author_id': self.user_employee.partner_id.id,
}, False, 'Author > no access on record'),
# notified
(self.record_admin.message_ids[0], {
'notification_ids': [(0, 0, {
'res_partner_id': self.user_employee.partner_id.id,
})],
}, False, 'Notified > no access on record'),
]:
original_vals = {
'author_id': msg.author_id.id,
'notification_ids': [(6, 0, {})],
'parent_id': msg.parent_id.id,
}
with self.subTest(msg=msg, reason=reason):
if msg_vals:
msg.write(msg_vals)
if should_crash:
with self.assertRaises(AccessError):
msg.with_user(self.user_employee).write({'body': 'Update'})
else:
msg.with_user(self.user_employee).write({'body': 'Update'})
if msg_vals:
msg.write(original_vals)
@mute_logger('odoo.addons.base.models.ir_rule')
def test_access_write_envelope(self):
""" Test updating message envelope require some privileges """
message = self.record_internal.with_user(self.user_employee).message_ids[0]
message.write({'body': 'Update Me'})
# To change in 18+
message.write({'model': 'res.partner'})
message.sudo().write({'model': self.record_internal._name}) # back to original model
# To change in 18+
message.write({'partner_ids': [(4, self.user_portal_2.partner_id.id)]})
# To change in 18+
message.write({'res_id': self.record_public.id})
# To change in 18+
message.write({'notification_ids': [
(0, 0, {'res_partner_id': self.user_portal_2.partner_id.id})
]})
@mute_logger('odoo.addons.base.models.ir_rule')
def test_access_write_portal_notification(self):
""" Test updating message notification content as portal user """
self.record_followers.message_subscribe(self.user_portal.partner_id.ids)
test_record = self.record_followers.with_user(self.user_portal)
test_record.read(['name'])
with self.assertRaises(AccessError):
test_record.with_user(self.user_portal_2).read(['name'])
message = test_record.message_ids[0].with_user(self.user_portal)
message.write({'body': 'Updated'})
with self.assertRaises(AccessError):
message.with_user(self.user_portal_2).read(['subject'])
# ------------------------------------------------------------
# SEARCH
# ------------------------------------------------------------
def test_search(self):
""" Test custom 'search' implemented on 'mail.message' that replicates
custom rules defined on 'read' override """
base_msg_vals = {
'message_type': 'comment',
'model': self.record_internal._name,
'res_id': self.record_internal.id,
'subject': '_ZTest',
}
msgs = self.env['mail.message'].create([
dict(base_msg_vals,
body='Private Comment (mention portal)',
model=False,
partner_ids=[(4, self.user_portal.partner_id.id)],
res_id=False,
subtype_id=self.ref('mail.mt_comment'),
),
dict(base_msg_vals,
body='Internal Log',
subtype_id=False,
),
dict(base_msg_vals,
body='Internal Note',
subtype_id=self.ref('mail.mt_note'),
),
dict(base_msg_vals,
body='Internal Comment (mention portal)',
partner_ids=[(4, self.user_portal.partner_id.id)],
subtype_id=self.ref('mail.mt_comment'),
),
dict(base_msg_vals,
body='Internal Comment (mention employee)',
partner_ids=[(4, self.user_employee.partner_id.id)],
subtype_id=self.ref('mail.mt_comment'),
),
dict(base_msg_vals,
body='Internal Comment',
subtype_id=self.ref('mail.mt_comment'),
),
])
msg_record_admin = self.env['mail.message'].create(dict(base_msg_vals,
body='Admin Comment',
model=self.record_admin._name,
res_id=self.record_admin.id,
subtype_id=self.ref('mail.mt_comment'),
))
msg_record_portal = self.env['mail.message'].create(dict(base_msg_vals,
body='Portal Comment',
model=self.record_portal._name,
res_id=self.record_portal.id,
subtype_id=self.ref('mail.mt_comment'),
))
msg_record_public = self.env['mail.message'].create(dict(base_msg_vals,
body='Public Comment',
model=self.record_public._name,
res_id=self.record_public.id,
subtype_id=self.ref('mail.mt_comment'),
))
for (test_user, add_domain), exp_messages in zip([
(self.user_public, []),
(self.user_portal, []),
(self.user_employee, []),
(self.user_employee, [('body', 'ilike', 'Internal')]),
(self.user_admin, []),
], [
msg_record_public,
msgs[0] + msgs[3] + msg_record_portal + msg_record_public,
msgs[1:6] + msg_record_portal + msg_record_public,
msgs[1:6],
msgs[1:] + msg_record_admin + msg_record_portal + msg_record_public
]):
with self.subTest(test_user=test_user.name, add_domain=add_domain):
domain = [('subject', 'like', '_ZTest')] + add_domain
self.assertEqual(self.env['mail.message'].with_user(test_user).search(domain), exp_messages)
@tagged('mail_message', 'security', 'post_install', '-at_install')
class TestMessageSubModelAccess(MessageAccessCommon):
def test_ir_attachment_read_message_notification(self):
message = self.record_admin.message_ids[0]
attachment = self.env['ir.attachment'].create({
'datas': base64.b64encode(b'My attachment'),
'name': 'doc.txt',
'res_model': message._name,
'res_id': message.id})
# attach the attachment to the message
message.write({'attachment_ids': [(4, attachment.id)]})
message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
message.with_user(self.user_employee).read()
# Test: Employee has access to attachment, ok because they can read message
attachment.with_user(self.user_employee).read(['name', 'datas'])
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.addons.base.models.ir_rule')
def test_mail_follower(self):
""" Read access check on sub entities of mail.message """
internal_record = self.record_internal.with_user(self.user_employee)
internal_record.message_subscribe(
partner_ids=self.user_portal.partner_id.ids
)
# employee can access
follower = internal_record.message_follower_ids.filtered(
lambda f: f.partner_id == self.user_portal.partner_id
)
self.assertTrue(follower)
with self.assertRaises(AccessError):
follower.with_user(self.user_portal).read(['partner_id'])
# employee cannot update
with self.assertRaises(AccessError):
follower.write({'partner_id': self.user_admin.partner_id.id})
follower.with_user(self.user_admin).write({'partner_id': self.user_admin.partner_id.id})
@mute_logger('odoo.addons.base.models.ir_rule')
def test_mail_notification(self):
""" Limit update of notifications for internal users """
internal_record = self.record_internal.with_user(self.user_admin)
message = internal_record.message_post(
body='Hello People',
message_type='comment',
partner_ids=(self.user_portal.partner_id + self.user_employee.partner_id).ids,
subtype_id=self.env.ref('mail.mt_comment').id,
)
notifications = message.with_user(self.user_employee).notification_ids
self.assertEqual(len(notifications), 2)
self.assertTrue(bool(notifications.read(['is_read'])), 'Internal can read')
notif_other = notifications.filtered(lambda n: n.res_partner_id == self.user_portal.partner_id)
with self.assertRaises(AccessError):
notif_other.write({'is_read': True})
notif_own = notifications.filtered(lambda n: n.res_partner_id == self.user_employee.partner_id)
notif_own.write({'is_read': True})
# with self.assertRaises(AccessError):
# notif_own.write({'author_id': self.user_portal.partner_id.id})
with self.assertRaises(AccessError):
notif_own.write({'mail_message_id': self.record_internal.message_ids[0]})
with self.assertRaises(AccessError):
notif_own.write({'res_partner_id': self.user_admin.partner_id.id})
def test_mail_notification_portal(self):
""" In any case, portal should not modify notifications """
self.assertFalse(self.env['mail.notification'].with_user(self.user_portal).check_access_rights('write', raise_exception=False))
portal_record = self.record_portal.with_user(self.user_portal)
message = portal_record.message_post(
body='Hello People',
message_type='comment',
partner_ids=(self.user_portal_2.partner_id + self.user_employee.partner_id).ids,
subtype_id=self.env.ref('mail.mt_comment').id,
)
notifications = message.notification_ids
self.assertEqual(len(notifications), 2)
self.assertTrue(bool(notifications.read(['is_read'])), 'Portal can read')
self.assertEqual(notifications.res_partner_id, self.user_portal_2.partner_id + self.user_employee.partner_id)

View file

@ -0,0 +1,436 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import json
import socket
from itertools import product
from unittest.mock import patch
from werkzeug.urls import url_parse, url_decode
from odoo.addons.mail.models.mail_message import Message
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
from odoo.exceptions import AccessError, UserError
from odoo.tests import tagged, users, HttpCase
from odoo.tools import formataddr, mute_logger
@tagged('multi_company')
class TestMultiCompanySetup(TestMailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestMultiCompanySetup, cls).setUpClass()
cls._activate_multi_company()
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.test_records_mc = cls.env['mail.test.multi.company'].create([
{'name': 'Test Company1',
'company_id': cls.user_employee.company_id.id},
{'name': 'Test Company2',
'company_id': cls.user_employee_c2.company_id.id},
])
cls.company_3 = cls.env['res.company'].create({'name': 'ELIT'})
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()),
})
def setUp(self):
super(TestMultiCompanySetup, self).setUp()
# patch registry to simulate a ready environment
self.patch(self.env.registry, 'ready', True)
self.flush_tracking()
@users('employee_c2')
@mute_logger('odoo.addons.base.models.ir_rule')
def test_post_with_read_access(self):
""" Check that with readonly access, a message with attachment can be
posted on a model with the attribute _mail_post_access = 'read'. """
test_record_c1_su = self.env['mail.test.multi.company.read'].sudo().create([
{
'company_id': self.user_employee.company_id.id,
'name': 'MC Readonly',
}
])
test_record_c1 = test_record_c1_su.with_env(self.env)
self.assertFalse(test_record_c1.message_main_attachment_id)
self.assertEqual(test_record_c1.name, 'MC Readonly')
with self.assertRaises(AccessError):
test_record_c1.write({'name': 'Cannot Write'})
message = test_record_c1.message_post(
attachments=[('testAttachment', b'Test attachment')],
body='My Body',
message_type='comment',
subtype_xmlid='mail.mt_comment',
)
self.assertEqual(message.attachment_ids.mapped('name'), ['testAttachment'])
first_attachment = message.attachment_ids
self.assertEqual(test_record_c1.message_main_attachment_id, first_attachment)
new_attach = self.env['ir.attachment'].create({
'company_id': self.user_employee_c2.company_id.id,
'datas': base64.b64encode(b'Test attachment'),
'mimetype': 'text/plain',
'name': 'TestAttachmentIDS.txt',
'res_model': 'mail.compose.message',
'res_id': 0,
})
message = test_record_c1.message_post(
attachments=[('testAttachment', b'Test attachment')],
attachment_ids=new_attach.ids,
body='My Body',
message_type='comment',
subtype_xmlid='mail.mt_comment',
)
self.assertEqual(
sorted(message.attachment_ids.mapped('name')),
['TestAttachmentIDS.txt', 'testAttachment'],
)
self.assertEqual(test_record_c1.message_main_attachment_id, first_attachment)
@users('employee_c2')
@mute_logger('odoo.addons.base.models.ir_rule')
def test_post_wo_access(self):
test_records_mc_c1, test_records_mc_c2 = self.test_records_mc.with_env(self.env)
attachments_data = [
('ReportLike1', 'AttContent1'),
('ReportLike2', 'AttContent2'),
]
# ------------------------------------------------------------
# Other company (no access)
# ------------------------------------------------------------
_original_car = Message.check_access_rule
with patch.object(Message, 'check_access_rule',
autospec=True, side_effect=_original_car) as mock_msg_car:
with self.assertRaises(AccessError):
test_records_mc_c1.message_post(
body='<p>Hello</p>',
message_type='comment',
record_name='CustomName', # avoid ACL on display_name
reply_to='custom.reply.to@test.example.com', # avoid ACL in notify_get_reply_to
subtype_xmlid='mail.mt_comment',
)
self.assertEqual(mock_msg_car.call_count, 1,
'Purpose is to raise at msg check access level')
with self.assertRaises(AccessError):
_name = test_records_mc_c1.name
# no access to company1, access to post through being notified of parent
with self.assertRaises(AccessError):
_subject = test_records_mc_c1.message_ids.subject
self.assertEqual(len(self.test_records_mc[0].message_ids), 1)
initial_message = self.test_records_mc[0].message_ids
self.env['mail.notification'].sudo().create({
'mail_message_id': initial_message.id,
'notification_status': 'sent',
'res_partner_id': self.user_employee_c2.partner_id.id,
})
# additional: works only if in partner_ids, not notified via followers
initial_message.write({
'partner_ids': [(4, self.user_employee_c2.partner_id.id)],
})
# now able to post as was notified of parent message
test_records_mc_c1.message_post(
body='<p>Hello</p>',
message_type='comment',
parent_id=initial_message.id,
record_name='CustomName', # avoid ACL on display_name
reply_to='custom.reply.to@test.example.com', # avoid ACL in notify_get_reply_to
subtype_xmlid='mail.mt_comment',
)
# now able to post as was notified of parent message
attachments = self.env['ir.attachment'].create(
self._generate_attachments_data(
2, 'mail.compose.message', 0,
prefix='Other'
)
)
# record_name and reply_to may generate ACLs issues when computed by
# 'message_post' but should not, hence not specifying them to be sure
# testing the complete flow
test_records_mc_c1.message_post(
attachments=attachments_data,
attachment_ids=attachments.ids,
body='<p>Hello</p>',
message_type='comment',
parent_id=initial_message.id,
subtype_xmlid='mail.mt_comment',
)
# ------------------------------------------------------------
# User company (access granted)
# ------------------------------------------------------------
# can effectively link attachments with message to record of writable record
attachments = self.env['ir.attachment'].create(
self._generate_attachments_data(
2, 'mail.compose.message', 0,
prefix='Same'
)
)
message = test_records_mc_c2.message_post(
attachments=attachments_data,
attachment_ids=attachments.ids,
body='<p>Hello</p>',
message_type='comment',
record_name='CustomName', # avoid ACL on display_name
reply_to='custom.reply.to@test.example.com', # avoid ACL in notify_get_reply_to
subtype_xmlid='mail.mt_comment',
)
self.assertTrue(attachments < message.attachment_ids)
self.assertEqual(
sorted(message.attachment_ids.mapped('name')),
['ReportLike1', 'ReportLike2', 'SameAttFileName_00.txt', 'SameAttFileName_01.txt'],
)
self.assertEqual(
message.attachment_ids.mapped('res_id'),
[test_records_mc_c2.id] * 4,
)
self.assertEqual(
message.attachment_ids.mapped('res_model'),
[test_records_mc_c2._name] * 4,
)
# cannot link attachments of unreachable records when posting on a document
# they can access (aka no access delegation through posting message)
attachments = self.env['ir.attachment'].sudo().create(
self._generate_attachments_data(
1,
test_records_mc_c1._name,
test_records_mc_c1.id,
prefix='NoAccessMC'
)
)
with self.assertRaises(AccessError):
message = test_records_mc_c2.message_post(
attachments=attachments_data,
attachment_ids=attachments.ids,
body='<p>Hello</p>',
message_type='comment',
record_name='CustomName', # avoid ACL on display_name
reply_to='custom.reply.to@test.example.com', # avoid ACL in notify_get_reply_to
subtype_xmlid='mail.mt_comment',
)
def test_systray_get_activities(self):
self.env["mail.activity"].search([]).unlink()
user_admin = self.user_admin.with_user(self.user_admin)
test_records = self.env["mail.test.multi.company.with.activity"].create(
[
{"name": "Test1", "company_id": user_admin.company_id.id},
{"name": "Test2", "company_id": self.company_2.id},
]
)
test_records[0].activity_schedule("test_mail.mail_act_test_todo", user_id=user_admin.id)
test_records[1].activity_schedule("test_mail.mail_act_test_todo", user_id=user_admin.id)
test_activity = next(
a for a in user_admin.systray_get_activities()
if a['model'] == 'mail.test.multi.company.with.activity'
)
self.assertEqual(
test_activity,
{
"actions": [{"icon": "fa-clock-o", "name": "Summary"}],
"icon": "/base/static/description/icon.png",
"id": self.env["ir.model"]._get_id("mail.test.multi.company.with.activity"),
"model": "mail.test.multi.company.with.activity",
"name": "Test Multi Company Mail With Activity",
"overdue_count": 0,
"planned_count": 0,
"today_count": 2,
"total_count": 2,
"type": "activity",
}
)
test_activity = next(
a for a in user_admin.with_context(allowed_company_ids=[self.company_2.id]).systray_get_activities()
if a['model'] == 'mail.test.multi.company.with.activity'
)
self.assertEqual(
test_activity,
{
"actions": [{"icon": "fa-clock-o", "name": "Summary"}],
"icon": "/base/static/description/icon.png",
"id": self.env["ir.model"]._get_id("mail.test.multi.company.with.activity"),
"model": "mail.test.multi.company.with.activity",
"name": "Test Multi Company Mail With Activity",
"overdue_count": 0,
"planned_count": 0,
"today_count": 1,
"total_count": 1,
"type": "activity",
}
)
@tagged('-at_install', 'post_install', 'multi_company', 'mail_controller')
class TestMultiCompanyRedirect(TestMailCommon, HttpCase):
@classmethod
def setUpClass(cls):
super(TestMultiCompanyRedirect, cls).setUpClass()
cls._activate_multi_company()
def test_redirect_to_records(self):
""" Test mail/view redirection in MC environment, notably cids being
added in redirect if user has access to the record. """
mc_record_c1, mc_record_c2 = self.env['mail.test.multi.company'].create([
{
'company_id': self.user_employee.company_id.id,
'name': 'Multi Company Record',
},
{
'company_id': self.user_employee_c2.company_id.id,
'name': 'Multi Company Record',
}
])
for (login, password), mc_record in product(
((None, None), # not logged: redirect to web/login
('employee', 'employee'), # access only main company
('admin', 'admin'), # access both companies
),
(mc_record_c1, mc_record_c2),
):
with self.subTest(login=login, mc_record=mc_record):
self.authenticate(login, password)
response = self.url_open(
f'/mail/view?model={mc_record._name}&res_id={mc_record.id}',
timeout=15
)
self.assertEqual(response.status_code, 200)
if not login:
path = url_parse(response.url).path
self.assertEqual(path, '/web/login')
decoded_fragment = url_decode(url_parse(response.url).fragment)
self.assertNotIn("cids", decoded_fragment)
else:
user = self.env['res.users'].browse(self.session.uid)
self.assertEqual(user.login, login)
mc_error = login == 'employee' and mc_record == mc_record_c2
if mc_error:
# Logged into company main, try accessing record in other
# company -> _redirect_to_record should redirect to
# messaging as the user doesn't have any access
fragment = url_parse(response.url).fragment
action = url_decode(fragment)['action']
self.assertEqual(action, 'mail.action_discuss')
else:
# Logged into company main, try accessing record in same
# company -> _redirect_to_record should add company in
# allowed_company_ids
fragment = url_parse(response.url).fragment
cids = url_decode(fragment)['cids']
if mc_record.company_id == user.company_id:
self.assertEqual(cids, f'{mc_record.company_id.id}')
else:
self.assertEqual(cids, f'{user.company_id.id},{mc_record.company_id.id}')
def test_redirect_to_records_nothread(self):
""" Test no thread models and redirection """
nothreads = self.env['mail.test.nothread'].create([
{
'company_id': company.id,
'name': f'Test with {company.name}',
}
for company in (self.company_admin, self.company_2, self.env['res.company'])
])
# when being logged, cids should be based on current user's company unless
# there is an access issue (not tested here, see 'test_redirect_to_records')
self.authenticate(self.user_admin.login, self.user_admin.login)
for test_record in nothreads:
for user_company in self.company_admin, self.company_2:
with self.subTest(record_name=test_record.name, user_company=user_company):
self.user_admin.write({'company_id': user_company.id})
response = self.url_open(
f'/mail/view?model={test_record._name}&res_id={test_record.id}',
timeout=15
)
self.assertEqual(response.status_code, 200)
decoded_fragment = url_decode(url_parse(response.url).fragment)
self.assertTrue("cids" in decoded_fragment)
self.assertEqual(decoded_fragment['cids'], str(user_company.id))
# when being not logged, cids should not be added as redirection after
# logging will be 'mail/view' again
for test_record in nothreads:
with self.subTest(record_name=test_record.name):
self.authenticate(None, None)
response = self.url_open(
f'/mail/view?model={test_record._name}&res_id={test_record.id}',
timeout=15
)
self.assertEqual(response.status_code, 200)
decoded_fragment = url_decode(url_parse(response.url).fragment)
self.assertNotIn('cids', decoded_fragment)
@tagged("-at_install", "post_install", "multi_company", "mail_controller")
class TestMultiCompanyThreadData(TestMailCommon, HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._activate_multi_company()
def test_mail_thread_data_follower(self):
partner_portal = self.env["res.partner"].create(
{"company_id": self.company_3.id, "name": "portal partner"}
)
record = self.env["mail.test.multi.company"].create({"name": "Multi Company Record"})
record.message_subscribe(partner_ids=partner_portal.ids)
with self.assertRaises(UserError):
partner_portal.with_user(self.user_employee_c2).check_access_rule("read")
self.authenticate(self.user_employee_c2.login, self.user_employee_c2.login)
response = self.url_open(
url="/mail/thread/data",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"params": {
"thread_id": record.id,
"thread_model": "mail.test.multi.company",
"request_list": ["followers"],
}
},
),
)
self.assertEqual(response.status_code, 200)
followers = json.loads(response.content)["result"]["followers"]
self.assertEqual(len(followers), 1)
self.assertEqual(followers[0]["partner"]["id"], partner_portal.id)

View file

@ -0,0 +1,29 @@
# -*- 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.exceptions import AccessError
class TestSubtypeAccess(TestMailCommon):
def test_subtype_access(self):
"""
The function aims to formally verify the access restrictions on mail.message.subtype for
normal and admin users. It ensures that normal users are unable to modify it,
while admin users possess the necessary privileges to modify it successfully.
"""
test_subtype = self.env['mail.message.subtype'].create({
'name': 'Test',
'description': 'only description',
})
user = mail_new_test_user(self.env, 'Internal user', groups='base.group_user')
with self.assertRaises(AccessError):
test_subtype.with_user(user).write({'description': 'changing description'})
test_subtype.with_user(self.user_admin).write({'description': 'testing'})
self.assertEqual(test_subtype.description, 'testing')

View file

@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import datetime
from freezegun import freeze_time
from unittest.mock import patch
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
from odoo.tests import tagged, users
from odoo.tools import mute_logger, safe_eval
class TestMailTemplateCommon(TestMailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestMailTemplateCommon, cls).setUpClass()
cls.test_record = cls.env['mail.test.lang'].with_context(cls._test_context).create({
'email_from': 'ignasse@example.com',
'name': 'Test',
})
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
# create a complete test template
cls.test_template = cls._create_template('mail.test.lang', {
'attachment_ids': [(0, 0, cls._attachments[0]), (0, 0, cls._attachments[1])],
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
'lang': '{{ object.customer_id.lang or object.lang }}',
'email_to': '%s, %s' % (cls.email_1, cls.email_2),
'email_cc': '%s' % cls.email_3,
'partner_to': '%s,%s' % (cls.partner_2.id, cls.user_admin.partner_id.id),
'subject': 'EnglishSubject for {{ object.name }}',
})
# activate translations
cls._activate_multi_lang(
layout_arch_db='<body><t t-out="message.body"/> English Layout for <t t-esc="model_description"/></body>',
test_record=cls.test_record, test_template=cls.test_template
)
# admin should receive emails
cls.user_admin.write({'notification_type': 'email'})
# Force the attachments of the template to be in the natural order.
cls.test_template.invalidate_recordset(['attachment_ids'])
@tagged('mail_template')
class TestMailTemplate(TestMailTemplateCommon):
def test_template_add_context_action(self):
self.test_template.create_action()
# check template act_window has been updated
self.assertTrue(bool(self.test_template.ref_ir_act_window))
# check those records
action = self.test_template.ref_ir_act_window
self.assertEqual(action.name, 'Send Mail (%s)' % self.test_template.name)
self.assertEqual(action.binding_model_id.model, 'mail.test.lang')
@mute_logger('odoo.addons.mail.models.mail_mail')
@users('employee')
def test_template_schedule_email(self):
""" Test scheduling email sending from template. """
now = datetime.datetime(2024, 4, 29, 10, 49, 59)
test_template = self.test_template.with_env(self.env)
# schedule the mail in 3 days -> patch safe_eval.datetime access
safe_eval_orig = safe_eval.safe_eval
def _safe_eval_hacked(*args, **kwargs):
""" safe_eval wraps 'datetime' and freeze_time does not mock it;
simplest solution found so far is to directly hack safe_eval just
for this test """
if args[0] == "datetime.datetime.now() + datetime.timedelta(days=3)":
return now + datetime.timedelta(days=3)
return safe_eval_orig(*args, **kwargs)
# patch datetime and safe_eval.datetime, as otherwise using standard 'now'
# might lead to errors due to test running right before minute switch it
# sometimes ends at minute+1 and assert fails - see runbot-54946
with patch.object(safe_eval, "safe_eval", autospec=True, side_effect=_safe_eval_hacked):
test_template.scheduled_date = '{{datetime.datetime.now() + datetime.timedelta(days=3)}}'
with freeze_time(now):
mail_id = test_template.send_mail(self.test_record.id)
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(
mail.scheduled_date.replace(second=0, microsecond=0),
(now + datetime.timedelta(days=3)).replace(second=0, microsecond=0),
)
self.assertEqual(mail.state, 'outgoing')
# check a wrong format
test_template.scheduled_date = '{{"test " * 5}}'
with freeze_time(now):
mail_id = test_template.send_mail(self.test_record.id)
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertFalse(mail.scheduled_date)
self.assertEqual(mail.state, 'outgoing')
@tagged('mail_template', 'multi_lang')
class TestMailTemplateLanguages(TestMailTemplateCommon):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_template_send_email(self):
mail_id = self.test_template.send_mail(self.test_record.id)
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(mail.email_cc, self.test_template.email_cc)
self.assertEqual(mail.email_to, self.test_template.email_to)
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
self.assertEqual(mail.subject, 'EnglishSubject for %s' % self.test_record.name)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_template_translation_lang(self):
test_record = self.env['mail.test.lang'].browse(self.test_record.ids)
test_record.write({
'lang': 'es_ES',
})
test_template = self.env['mail.template'].browse(self.test_template.ids)
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(mail.body_html,
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish Model Description</body>' % self.test_record.name)
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_template_translation_partner_lang(self):
test_record = self.env['mail.test.lang'].browse(self.test_record.ids)
customer = self.env['res.partner'].create({
'email': 'robert.carlos@test.example.com',
'lang': 'es_ES',
'name': 'Roberto Carlos',
})
test_record.write({
'customer_id': customer.id,
})
test_template = self.env['mail.template'].browse(self.test_template.ids)
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(mail.body_html,
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish Model Description</body>' % self.test_record.name)
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)

View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mail.tests.test_mail_template import TestMailTemplateCommon
from odoo.tests import tagged, users
from odoo.tests.common import Form
@tagged('mail_template', 'multi_lang')
class TestMailTemplateTools(TestMailTemplateCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_template_preview = cls.env['mail.template.preview'].create({
'mail_template_id': cls.test_template.id,
})
def test_initial_values(self):
self.assertTrue(self.test_template.email_to)
self.assertTrue(self.test_template.email_cc)
self.assertEqual(len(self.test_template.partner_to.split(',')), 2)
self.assertTrue(self.test_record.email_from)
def test_mail_template_preview_force_lang(self):
test_record = self.env['mail.test.lang'].browse(self.test_record.ids)
test_record.write({
'lang': 'es_ES',
})
test_template = self.env['mail.template'].browse(self.test_template.ids)
preview = self.env['mail.template.preview'].create({
'mail_template_id': test_template.id,
'resource_ref': test_record,
'lang': 'es_ES',
})
self.assertEqual(preview.body_html, '<p>SpanishBody for %s</p>' % test_record.name)
preview.write({'lang': 'en_US'})
self.assertEqual(preview.body_html, '<p>EnglishBody for %s</p>' % test_record.name)
@users('employee')
def test_mail_template_preview_recipients(self):
form = Form(self.test_template_preview)
form.resource_ref = self.test_record
self.assertEqual(form.email_to, self.test_template.email_to)
self.assertEqual(form.email_cc, self.test_template.email_cc)
self.assertEqual(set(record.id for record in form.partner_ids),
{int(pid) for pid in self.test_template.partner_to.split(',') if pid})
@users('employee')
def test_mail_template_preview_recipients_use_default_to(self):
self.test_template.use_default_to = True
form = Form(self.test_template_preview)
form.resource_ref = self.test_record
self.assertEqual(form.email_to, self.test_record.email_from)
self.assertFalse(form.email_cc)
self.assertFalse(form.partner_ids)

View file

@ -0,0 +1,409 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from unittest.mock import DEFAULT
from odoo import exceptions
from odoo.addons.test_mail.models.test_mail_models import MailTestSimple
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
from odoo.tests.common import tagged, users
from odoo.tools import mute_logger
@tagged('mail_thread')
class TestAPI(TestMailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestAPI, cls).setUpClass()
cls.ticket_record = cls.env['mail.test.ticket'].with_context(cls._test_context).create({
'email_from': '"Paulette Vachette" <paulette@test.example.com>',
'name': 'Test',
'user_id': cls.user_employee.id,
})
@mute_logger('openerp.addons.mail.models.mail_mail')
@users('employee')
def test_message_update_content(self):
""" Test updating message content. """
ticket_record = self.ticket_record.with_env(self.env)
attachments = self.env['ir.attachment'].create(
self._generate_attachments_data(2, 'mail.compose.message', 0)
)
# post a note
message = ticket_record.message_post(
attachment_ids=attachments.ids,
body="<p>Initial Body</p>",
message_type="comment",
partner_ids=self.partner_1.ids,
)
self.assertEqual(message.attachment_ids, attachments)
self.assertEqual(set(message.mapped('attachment_ids.res_id')), set(ticket_record.ids))
self.assertEqual(set(message.mapped('attachment_ids.res_model')), set([ticket_record._name]))
self.assertEqual(message.body, "<p>Initial Body</p>")
self.assertEqual(message.subtype_id, self.env.ref('mail.mt_note'))
# update the content with new attachments
new_attachments = self.env['ir.attachment'].create(
self._generate_attachments_data(2, 'mail.compose.message', 0)
)
ticket_record._message_update_content(
message, "<p>New Body</p>",
attachment_ids=new_attachments.ids
)
self.assertEqual(message.attachment_ids, attachments + new_attachments)
self.assertEqual(set(message.mapped('attachment_ids.res_id')), set(ticket_record.ids))
self.assertEqual(set(message.mapped('attachment_ids.res_model')), set([ticket_record._name]))
self.assertEqual(message.body, "<p>New Body</p>")
# void attachments
ticket_record._message_update_content(
message, "<p>Another Body, void attachments</p>",
attachment_ids=[]
)
self.assertFalse(message.attachment_ids)
self.assertFalse((attachments + new_attachments).exists())
self.assertEqual(message.body, "<p>Another Body, void attachments</p>")
@mute_logger('openerp.addons.mail.models.mail_mail')
@users('employee')
def test_message_update_content_check(self):
""" Test cases where updating content should be prevented """
ticket_record = self.ticket_record.with_env(self.env)
# cannot edit user comments (subtype)
message = ticket_record.message_post(
body="<p>Initial Body</p>",
message_type="comment",
subtype_id=self.env.ref('mail.mt_comment').id,
)
with self.assertRaises(exceptions.UserError):
ticket_record._message_update_content(
message, "<p>New Body</p>"
)
message.sudo().write({'subtype_id': self.env.ref('mail.mt_note')})
ticket_record._message_update_content(
message, "<p>New Body</p>"
)
# cannot edit notifications
for message_type in ['notification', 'user_notification', 'email']:
message.sudo().write({'message_type': message_type})
with self.assertRaises(exceptions.UserError):
ticket_record._message_update_content(
message, "<p>New Body</p>"
)
@tagged('mail_thread')
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'))
@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'))
@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)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_chatter_context_cleaning(self):
""" Test default keys are not propagated to message creation as it may
induce wrong values for some fields, like parent_id. """
parent = self.env['res.partner'].create({'name': 'Parent'})
partner = self.env['res.partner'].with_context(default_parent_id=parent.id).create({'name': 'Contact'})
self.assertFalse(partner.message_ids[-1].parent_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)
@tagged('mail_thread')
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'
})
@mute_logger('openerp.addons.mail.models.mail_mail')
def test_mark_all_as_read(self):
def _employee_crash(*args, **kwargs):
""" If employee is test employee, consider they have 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(MailTestSimple, 'check_access_rights', autospec=True, side_effect=_employee_crash):
with self.assertRaises(exceptions.AccessError):
self.env['mail.test.simple'].with_user(self.user_employee).browse(self.test_record.ids).read(['name'])
employee_partner = self.env['res.partner'].with_user(self.user_employee).browse(self.partner_employee.ids)
# mark all as read clear needactions
msg1 = self.test_record.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[employee_partner.id])
self._reset_bus()
with self.assertBus(
[(self.cr.dbname, 'res.partner', employee_partner.id)],
message_items=[{
'type': 'mail.message/mark_as_read',
'payload': {
'message_ids': [msg1.id],
'needaction_inbox_counter': 0,
},
}]):
employee_partner.env['mail.message'].mark_all_as_read(domain=[])
na_count = employee_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 = self.test_record.message_post(body='Zest', message_type='comment', subtype_xmlid='mail.mt_comment', partner_ids=[employee_partner.id])
needaction_accessible = len(employee_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']
employee_partner.env['mail.message'].search([['needaction', '=', True]])
needaction_length = len(employee_partner.env['mail.message'].search([['needaction', '=', True]]))
self.assertEqual(needaction_length, 1, "message should still be readable when notified")
na_count = employee_partner._get_needaction_count()
self.assertEqual(na_count, 1, "message not accessible is currently still counted")
self._reset_bus()
with self.assertBus(
[(self.cr.dbname, 'res.partner', employee_partner.id)],
message_items=[{
'type': 'mail.message/mark_as_read',
'payload': {
'message_ids': [msg2.id],
'needaction_inbox_counter': 0,
},
}]):
employee_partner.env['mail.message'].mark_all_as_read(domain=[])
na_count = employee_partner._get_needaction_count()
self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
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>', None, 'CC Email'),
(False, 'cc1@example.com', None, 'CC Email'),
(False, 'cc2@example.com', None, '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].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():
def _connect(*args, **kwargs):
raise Exception("Some exception")
self.connect_mocked.side_effect = _connect
self.user_admin.notification_type = 'email'
message2 = self.test_record.with_user(self.user_employee).message_post(
body='Test', message_type='comment', subtype_xmlid='mail.mt_comment',
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)
@users("employee")
def test_unlink_notification_message(self):
channel = self.env['mail.channel'].create({'name': 'testChannel'})
notification_msg = channel.with_user(self.user_admin).message_notify(
body='test',
message_type='user_notification',
partner_ids=[self.partner_2.id],
)
with self.assertRaises(exceptions.AccessError):
notification_msg.with_env(self.env)._message_format(['id', 'body', 'date', 'author_id', 'email_from'])
channel_message = self.env['mail.message'].sudo().search([('model', '=', 'mail.channel'), ('res_id', 'in', channel.ids)])
self.assertEqual(len(channel_message), 1, "Test message should have been posted")
channel.sudo().unlink()
remaining_message = channel_message.exists()
self.assertEqual(len(remaining_message), 0, "Test message should have been deleted")
@tagged('mail_thread')
class TestNoThread(TestMailCommon, TestRecipients):
""" Specific tests for cross models thread features """
@users('employee')
def test_message_notify(self):
test_record = self.env['mail.test.nothread'].create({
'customer_id': self.partner_1.id,
'name': 'Not A Thread',
})
with self.assertPostNotifications([{
'content': 'Hello Paulo',
'email_values': {
'reply_to': self.company_admin.catchall_formatted,
},
'message_type': 'user_notification',
'notif': [{
'check_send': True,
'is_read': True,
'partner': self.partner_2,
'status': 'sent',
'type': 'email',
}],
'subtype': 'mail.mt_note',
}]):
_message = self.env['mail.thread'].message_notify(
body='<p>Hello Paulo</p>',
model=test_record._name,
res_id=test_record.id,
subject='Test Notify',
partner_ids=self.partner_2.ids
)

View file

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import exceptions, tools
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
from odoo.tests.common import tagged
from odoo.tools import mute_logger
@tagged('mail_thread', 'mail_blacklist')
class TestMailThread(TestMailCommon, TestRecipients):
@mute_logger('odoo.models.unlink')
def test_blacklist_mixin_email_normalized(self):
""" Test email_normalized and is_blacklisted fields behavior, notably
when dealing with encapsulated email fields and multi-email input. """
base_email = 'test.email@test.example.com'
# test data: source email, expected email normalized
valid_pairs = [
(base_email, base_email),
(tools.formataddr(('Another Name', base_email)), base_email),
(f'Name That Should Be Escaped <{base_email}>', base_email),
('test.😊@example.com', 'test.😊@example.com'),
('"Name 😊" <test.😊@example.com>', 'test.😊@example.com'),
]
void_pairs = [(False, False),
('', False),
(' ', False)]
multi_pairs = [
(f'{base_email}, other.email@test.example.com',
base_email), # multi supports first found
(f'{tools.formataddr(("Another Name", base_email))}, other.email@test.example.com',
base_email), # multi supports first found
]
for email_from, exp_email_normalized in valid_pairs + void_pairs + multi_pairs:
with self.subTest(email_from=email_from, exp_email_normalized=exp_email_normalized):
new_record = self.env['mail.test.gateway'].create({
'email_from': email_from,
'name': 'BL Test',
})
self.assertEqual(new_record.email_normalized, exp_email_normalized)
self.assertFalse(new_record.is_blacklisted)
# blacklist email should fail as void
if email_from in [pair[0] for pair in void_pairs]:
with self.assertRaises(exceptions.UserError):
bl_record = self.env['mail.blacklist']._add(email_from)
# blacklist email currently fails but could not
elif email_from in [pair[0] for pair in multi_pairs]:
with self.assertRaises(exceptions.UserError):
bl_record = self.env['mail.blacklist']._add(email_from)
# blacklist email ok
else:
bl_record = self.env['mail.blacklist']._add(email_from)
self.assertEqual(bl_record.email, exp_email_normalized)
new_record.invalidate_recordset(fnames=['is_blacklisted'])
self.assertTrue(new_record.is_blacklisted)
bl_record.unlink()

View file

@ -0,0 +1,130 @@
# -*- 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_user', 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'}):
def _connect(*args, **kwargs):
raise Exception("Some exception")
self.connect_mocked.side_effect = _connect
message = self.test_record.with_user(self.user_admin).message_post(partner_ids=self.partners.ids, subtype_xmlid='mail.mt_comment', message_type='notification')
wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
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()
expected_bus_notifications = [
(self.cr.dbname, 'res.partner', self.partner_admin.id),
(self.cr.dbname, 'res.partner', self.env.user.partner_id.id),
]
with self.mock_mail_gateway(), self.assertBus(expected_bus_notifications * 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(expected_bus_notifications * 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(expected_bus_notifications):
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()
expected_bus_notifications = [
(self.cr.dbname, 'res.partner', self.partner_admin.id),
(self.cr.dbname, 'res.partner', self.env.user.partner_id.id),
]
with self.mock_mail_gateway(), self.assertBus(expected_bus_notifications):
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]}]
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,484 @@
# -*- 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_message_type(self):
"""Check that the right message type is applied for track templates."""
self.record.message_subscribe(
partner_ids=[self.user_admin.partner_id.id],
subtype_ids=[self.env.ref('mail.mt_comment').id]
)
mail_templates = self.env['mail.template'].create([{
'name': f'Template {n}',
'subject': f'Template {n}',
'model_id': self.env.ref('test_mail.model_mail_test_ticket').id,
'body_html': f'<p>Template {n}</p>',
} for n in range(2)])
def _track_subtype(self, init_values):
return self.env.ref('mail.mt_note')
self.patch(self.registry('mail.test.ticket'), '_track_subtype', _track_subtype)
def _track_template(self, changes):
if 'email_from' in changes:
return {'email_from': (mail_templates[0], {})}
elif 'container_id' in changes:
return {'container_id': (mail_templates[1], {'message_type': 'notification'})}
return {}
self.patch(self.registry('mail.test.ticket'), '_track_template', _track_template)
container = self.env['mail.test.container'].create({'name': 'Container'})
# default is auto_comment
with self.mock_mail_gateway():
self.record.email_from = 'test@test.lan'
self.flush_tracking()
first_message = self.record.message_ids.filtered(lambda message: message.subject == 'Template 0')
self.assertEqual(len(self.record.message_ids), 2, 'Should be one change message and one automated template')
self.assertEqual(first_message.message_type, 'auto_comment')
# auto_comment can be overriden by _track_template
with self.mock_mail_gateway(mail_unlink_sent=False):
self.record.container_id = container
self.flush_tracking()
second_message = self.record.message_ids.filtered(lambda message: message.subject == 'Template 1')
self.assertEqual(len(self.record.message_ids), 4, 'Should have added one change message and one automated template')
self.assertEqual(second_message.message_type, 'notification')
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_invalid_selection(self):
# Test: Check that initial invalid selection values are allowed when tracking
# Create a record with an initially invalid selection value
invalid_value = 'I love writing tests!'
record = self.env['mail.test.track.selection'].create({
'name': 'Test Invalid Selection Values',
'selection_type': 'first',
})
self.flush_tracking()
self.env.cr.execute(
"""
UPDATE mail_test_track_selection
SET selection_type = %s
WHERE id = %s
""",
[invalid_value, record.id]
)
record.invalidate_recordset()
self.assertEqual(record.selection_type, invalid_value)
# Write a valid selection value
record.selection_type = "second"
self.flush_tracking()
self.assertTracking(record.message_ids, [
('selection_type', 'char', invalid_value, 'Second'),
])
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 TestTrackingMonetary(TestMailCommon):
def setUp(self):
super(TestTrackingMonetary, self).setUp()
self._activate_multi_company()
record = self.env['mail.test.track.monetary'].with_user(self.user_employee).with_context(self._test_context).create({
'company_id': self.user_employee.company_id.id,
})
self.flush_tracking()
self.record = record.with_context(mail_notrack=False)
def test_message_track_monetary(self):
""" Update a record with a tracked monetary field """
# Check if the tracking value have the correct currency and values
self.record.write({
'revenue': 100,
})
self.flush_tracking()
self.assertEqual(len(self.record.message_ids), 1)
self.assertTracking(self.record.message_ids[0], [
('revenue', 'monetary', 0, 100),
])
# Check if the tracking value have the correct currency and values after changing the value and the company
self.record.write({
'revenue': 200,
'company_id': self.company_2.id,
})
self.flush_tracking()
self.assertEqual(len(self.record.message_ids), 2)
self.assertTracking(self.record.message_ids[0], [
('revenue', 'monetary', 100, 200),
('company_currency', 'many2one', self.user_employee.company_id.currency_id, self.company_2.currency_id)
])
@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()
tracking_values = self.env['mail.tracking.value'].search([('mail_message_id', '=', self.record.message_ids.id)])
formattedTrackingValues = [{
'changedField': 'Email From',
'id': tracking_values[0]['id'],
'newValue': {
'currencyId': False,
'fieldType': 'char',
'value': 'X',
},
'oldValue': {
'currencyId': False,
'fieldType': 'char',
'value': False,
},
}]
self.assertEqual(msg_emp[0].get('trackingValues'), [], "should not have protected tracking values")
self.assertEqual(msg_sudo[0].get('trackingValues'), formattedTrackingValues, "should have protected tracking values")
msg_emp = self.record._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
msg_sudo = self.record.sudo()._notify_by_email_prepare_rendering_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.env.invalidate_all()
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 they 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)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo import Command
@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")
def test_02_mail_create_channel_no_mail_tour(self):
self.env['res.users'].create({
'email': '', # User should be able to create a channel even if no email is defined
'groups_id': [Command.set([self.ref('base.group_user')])],
'name': 'Test User',
'login': 'testuser',
'password': 'testuser',
})
self.start_tour("/web", 'mail_tour', login='testuser')