19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:39 +01:00
parent 5df8c07b59
commit daa394e8b0
2114 changed files with 564841 additions and 299642 deletions

View file

@ -8,7 +8,7 @@ import werkzeug
from unittest.mock import patch
from odoo import tools
from odoo.tools import email_normalize, mail
from odoo.addons.link_tracker.tests.common import MockLinkTracker
from odoo.addons.mail.tests.common import MailCase, MailCommon, mail_new_test_user
from odoo.sql_db import Cursor
@ -36,7 +36,7 @@ class MassMailCase(MailCase, MockLinkTracker):
)
def assertMailTraces(self, recipients_info, mailing, records,
check_mail=True, sent_unlink=False,
check_mail=True, is_cancel_not_sent=True, sent_unlink=False,
author=None, mail_links_info=None):
""" Check content of traces. Traces are fetched based on a given mailing
and records. Their content is compared to recipients_info structure that
@ -46,16 +46,22 @@ class MassMailCase(MailCase, MockLinkTracker):
:param recipients_info: list[{
# TRACE
'email': (normalized) email used when sending email and stored on
trace. May be empty, computed based on partner;
'failure_type': optional failure type;
'failure_reason': optional failure reason;
'partner': res.partner record (may be empty),
'email': email used when sending email (may be empty, computed based on partner),
'trace_status': outgoing / sent / open / reply / bounce / error / cancel (sent by default),
'record: linked record,
'trace_status': outgoing / sent / open / reply / bounce / error / cancel (sent by default),
# MAIL.MAIL
'content': optional content that should be present in mail.mail body_html;
'email_to_mail': optional email used for the mail, when different from the
one stored on the trace itself;
'email_to_recipients': optional, see '_assertMailMail';
'failure_type': optional failure reason;
one stored on the trace itself (see 'email_to' in assertMailMail);
'email_to_recipients': optional email used ofr the outgoing email,
see 'assertSentEmail';
'failure_type': propagated from trace;
'failure_reason': propagated from trace;
'mail_values': other mail.mail values for assertMailMail;
}, { ... }]
:param mailing: a mailing.mailing record from which traces have been
@ -63,12 +69,15 @@ class MassMailCase(MailCase, MockLinkTracker):
:param records: records given to mailing that generated traces. It is
used notably to find traces using their IDs;
:param check_mail: if True, also check mail.mail records that should be
linked to traces;
linked to traces unless not sent (trace_status == 'cancel');
:param is_cancel_not_sent: if True, also check that no mail.mail/mail.message
related to "cancel trace" have been created and disable check_mail for those.
:param sent_unlink: it True, sent mail.mail are deleted and we check gateway
output result instead of actual mail.mail records;
:param mail_links_info: if given, should follow order of ``recipients_info``
and give details about links. See ``assertLinkShortenedHtml`` helper for
more details about content to give;
more details about content to give.
Not tested for mail with trace status == 'cancel' if is_cancel_not_sent;
:param author: author of sent mail.mail;
"""
# map trace state to email state
@ -86,10 +95,8 @@ class MassMailCase(MailCase, MockLinkTracker):
('res_id', 'in', records.ids)
])
debug_info = '\n'.join(
(
f'Trace: to {t.email} - state {t.trace_status} - res_id {t.res_id}'
for t in traces
)
f'Trace: to {t.email} - state {t.trace_status} - res_id {t.res_id}'
for t in traces
)
# ensure trace coherency
@ -100,15 +107,36 @@ class MassMailCase(MailCase, MockLinkTracker):
if not mail_links_info:
mail_links_info = [None] * len(recipients_info)
for recipient_info, link_info, record in zip(recipients_info, mail_links_info, records):
# check input
invalid = set(recipient_info.keys()) - {
'content',
# email_to
'email', 'email_to_mail', 'email_to_recipients',
# mail.mail
'mail_values',
# email
'email_values',
# trace
'partner', 'record', 'trace_status',
'failure_type', 'failure_reason',
}
if invalid:
raise AssertionError(f"assertMailTraces: invalid input {invalid}")
# recipients
partner = recipient_info.get('partner', self.env['res.partner'])
email = recipient_info.get('email')
email_to_mail = recipient_info.get('email_to_mail') or email
email_to_recipients = recipient_info.get('email_to_recipients')
status = recipient_info.get('trace_status', 'sent')
record = record or recipient_info.get('record')
content = recipient_info.get('content')
if email is None and partner:
email = partner.email_normalized
email_to_mail = recipient_info.get('email_to_mail') or email
email_to_recipients = recipient_info.get('email_to_recipients')
# trace
failure_type = recipient_info.get('failure_type')
failure_reason = recipient_info.get('failure_reason')
status = recipient_info.get('trace_status', 'sent')
# content
content = recipient_info.get('content')
record = record or recipient_info.get('record')
recipient_trace = traces.filtered(
lambda t: (t.email == email or (not email and not t.email)) and \
@ -121,22 +149,38 @@ class MassMailCase(MailCase, MockLinkTracker):
email, partner, status, record,
len(recipient_trace), debug_info)
)
self.assertTrue(bool(recipient_trace.mail_mail_id_int))
mail_not_created = is_cancel_not_sent and recipient_trace.trace_status == 'cancel'
self.assertTrue(mail_not_created or bool(recipient_trace.mail_mail_id_int))
if 'failure_type' in recipient_info or status in ('error', 'cancel', 'bounce'):
self.assertEqual(recipient_trace.failure_type, recipient_info['failure_type'])
self.assertEqual(recipient_trace.failure_type, failure_type)
if 'failure_reason' in recipient_info:
self.assertEqual(recipient_trace.failure_reason, failure_reason)
if mail_not_created:
self.assertFalse(recipient_trace.mail_mail_id_int)
self.assertFalse(self.env['mail.mail'].sudo().search(
[('model', '=', record._name), ('res_id', '=', record.id),
('id', 'in', self._new_mails.ids)]))
self.assertFalse(self.env['mail.message'].sudo().search(
[('model', '=', record._name), ('res_id', '=', record.id),
('id', 'in', self._new_mails.mail_message_id.ids)]))
if check_mail:
if check_mail and not mail_not_created:
if author is None:
author = self.env.user.partner_id
# mail.mail specific values to check
email_values = recipient_info.get('email_values', {})
fields_values = {'mailing_id': mailing}
if recipient_info.get('mail_values'):
fields_values.update(recipient_info['mail_values'])
if 'failure_type' in recipient_info:
fields_values['failure_type'] = failure_type
if 'failure_reason' in recipient_info:
fields_values['failure_reason'] = recipient_info['failure_reason']
fields_values['failure_reason'] = failure_reason
if 'email_to_mail' in recipient_info:
fields_values['email_to'] = recipient_info['email_to_mail']
if partner:
fields_values['recipient_ids'] = partner
# specific for partner: email_formatted is used
if partner:
@ -149,6 +193,7 @@ class MassMailCase(MailCase, MockLinkTracker):
content=content,
email_to_recipients=email_to_recipients,
fields_values=fields_values,
email_values=email_values,
)
# specific if email is False -> could have troubles finding it if several falsy traces
elif not email and status in ('cancel', 'bounce'):
@ -158,6 +203,7 @@ class MassMailCase(MailCase, MockLinkTracker):
content=content,
email_to_recipients=email_to_recipients,
fields_values=fields_values,
email_values=email_values,
)
else:
self.assertMailMailWEmails(
@ -166,9 +212,10 @@ class MassMailCase(MailCase, MockLinkTracker):
content=content,
email_to_recipients=email_to_recipients,
fields_values=fields_values,
email_values=email_values,
)
if link_info:
if link_info and not mail_not_created:
trace_mail = self._find_mail_mail_wrecord(record)
for (anchor_id, url, is_shortened, add_link_params) in link_info:
link_params = {'utm_medium': 'Email', 'utm_source': mailing.name}
@ -184,7 +231,7 @@ class MassMailCase(MailCase, MockLinkTracker):
# TOOLS
# ------------------------------------------------------------
def gateway_mail_bounce(self, mailing, record, bounce_base_values=None):
def gateway_mail_trace_bounce(self, mailing, record, bounce_base_values=None):
""" Generate a bounce at mailgateway level.
:param mailing: a ``mailing.mailing`` record on which we find a trace
@ -192,27 +239,31 @@ class MassMailCase(MailCase, MockLinkTracker):
:param record: record which should bounce;
:param bounce_base_values: optional values given to routing;
"""
record_email = record[record._primary_email]
trace = mailing.mailing_trace_ids.filtered(
lambda t: t.model == record._name and t.res_id == record.id
)
self.assertTrue(trace)
self.assertEqual(trace.email, email_normalize(record_email))
parsed_bounce_values = {
'email_from': 'some.email@external.example.com', # TDE check: email_from -> trace email ?
'to': 'bounce@test.example.com', # TDE check: bounce alias ?
'message_id': tools.generate_tracking_message_id('MailTest'),
'message_id': mail.generate_tracking_message_id('MailTest'),
'bounced_partner': self.env['res.partner'].sudo(),
'bounced_message': self.env['mail.message'].sudo()
'bounced_message': self.env['mail.message'].sudo(),
'body': 'This is the bounce email',
}
if bounce_base_values:
parsed_bounce_values.update(bounce_base_values)
parsed_bounce_values.update({
'bounced_email': trace.email,
'bounced_msg_id': [trace.message_id],
'bounced_msg_ids': [trace.message_id],
})
self.env['mail.thread']._routing_handle_bounce(False, parsed_bounce_values)
return trace
def gateway_mail_click(self, mailing, record, click_label):
def gateway_mail_trace_click(self, mailing, record, click_label):
""" Simulate a click on a sent email.
:param mailing: a ``mailing.mailing`` record on which we find a trace
@ -220,13 +271,16 @@ class MassMailCase(MailCase, MockLinkTracker):
:param record: record which should click;
:param click_label: label of link on which we should click;
"""
record_email = record[record._primary_email]
trace = mailing.mailing_trace_ids.filtered(
lambda t: t.model == record._name and t.res_id == record.id
)
self.assertTrue(trace)
self.assertEqual(trace.email, email_normalize(record_email))
email = self._find_sent_mail_wemail(trace.email)
email = self._find_sent_email_wemail(record_email)
self.assertTrue(bool(email))
for (_url_href, link_url, _dummy, label) in re.findall(tools.HTML_TAG_URL_REGEX, email['body']):
for (_url_href, link_url, _dummy, label) in re.findall(mail.HTML_TAG_URL_REGEX, email['body']):
if label == click_label and '/r/' in link_url: # shortened link, like 'http://localhost:8069/r/LBG/m/53'
parsed_url = werkzeug.urls.url_parse(link_url)
path_items = parsed_url.path.split('/')
@ -244,7 +298,7 @@ class MassMailCase(MailCase, MockLinkTracker):
raise AssertionError('url %s not found in mailing %s for record %s' % (click_label, mailing, record))
return trace
def gateway_mail_open(self, mailing, record):
def gateway_mail_trace_open(self, mailing, record):
""" Simulate opening an email through blank.gif icon access. As we
don't want to use the whole Http layer just for that we will just
call 'set_opened()' on trace, until having a better option.
@ -256,9 +310,28 @@ class MassMailCase(MailCase, MockLinkTracker):
trace = mailing.mailing_trace_ids.filtered(
lambda t: t.model == record._name and t.res_id == record.id
)
self.assertTrue(trace)
trace.set_opened()
return trace
def gateway_mail_trace_reply(self, mailing, record):
""" Simulate replying to an email. As we don't want to use the whole
mail and gateway layer just for that we will just call 'set_replied()'
on trace.
:param mailing: a ``mailing.mailing`` record on which we find a trace
to open;
:param record: record which should open;
"""
trace = mailing.mailing_trace_ids.filtered(
lambda t: t.model == record._name and t.res_id == record.id
)
self.assertTrue(trace)
trace.set_replied()
return trace
@classmethod
def _create_bounce_trace(cls, mailing, records, dt=None):
if dt is None:
@ -300,23 +373,35 @@ class MassMailCase(MailCase, MockLinkTracker):
def _create_mailing_list(cls):
""" Shortcut to create mailing lists. Currently hardcoded, maybe evolve
in a near future. """
cls.mailing_list_1 = cls.env['mailing.list'].with_context(cls._test_context).create({
'name': 'List1',
'contact_ids': [
(0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}),
(0, 0, {'name': 'Gorramts', 'email': 'gorramts@example.com'}),
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
]
})
cls.mailing_list_2 = cls.env['mailing.list'].with_context(cls._test_context).create({
'name': 'List2',
'contact_ids': [
(0, 0, {'name': 'Gilberte', 'email': 'gilberte@example.com'}),
(0, 0, {'name': 'Gilberte En Mieux', 'email': 'gilberte@example.com'}),
(0, 0, {'name': 'Norbert', 'email': 'norbert@example.com'}),
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
]
})
cls.mailing_list_1, cls.mailing_list_2, cls.mailing_list_3, cls.mailing_list_4 = cls.env['mailing.list'].with_context(cls._test_context).create([
{
'contact_ids': [
(0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}),
(0, 0, {'name': 'Gorramts', 'email': 'gorramts@example.com'}),
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
],
'name': 'List1',
'is_public': True,
}, {
'contact_ids': [
(0, 0, {'name': 'Gilberte', 'email': 'gilberte@example.com'}),
(0, 0, {'name': 'Gilberte En Mieux', 'email': 'gilberte@example.com'}),
(0, 0, {'name': 'Norbert', 'email': 'norbert@example.com'}),
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
],
'name': 'List2',
'is_public': True,
}, {
'contact_ids': [
(0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}),
],
'name': 'List3',
'is_public': True,
}, {
'name': 'List4',
}
])
cls.mailing_list_3.subscription_ids[0].opt_out = True
@classmethod
def _create_mailing_list_of_x_contacts(cls, contacts_nbr):
@ -325,8 +410,11 @@ class MassMailCase(MailCase, MockLinkTracker):
return cls.env['mailing.list'].with_context(cls._test_context).create({
'name': 'Test List',
'contact_ids': [
(0, 0, {'name': 'Contact %s' % i, 'email': 'contact%s@example.com' % i})
for i in range(contacts_nbr)
(0, 0, {
'name': f'Contact %{idx}',
'email': f'contact%{idx}@example.com'
})
for idx in range(contacts_nbr)
],
})
@ -337,10 +425,13 @@ class MassMailCommon(MailCommon, MassMailCase):
def setUpClass(cls):
super(MassMailCommon, cls).setUpClass()
cls.user_marketing = mail_new_test_user(
cls.env, login='user_marketing',
cls.user_marketing, cls.user_marketing_1 = [mail_new_test_user(
cls.env,
groups='base.group_user,base.group_partner_manager,mass_mailing.group_mass_mailing_user',
name='Martial Marketing', signature='--\nMartial')
login=f'user_marketing{suffix}',
name=f'Martial Marketing{suffix}',
signature=f'--\nMartial{suffix}',
) for suffix in ('', '_1')]
cls.email_reply_to = 'MyCompany SomehowAlias <test.alias@test.mycompany.com>'

View file

@ -4,9 +4,8 @@
from datetime import datetime, timedelta
from odoo.addons.mass_mailing.tests.common import MassMailCommon
from odoo.tests import users, tagged
from odoo.tests import Form, users, tagged
from odoo.tools import mute_logger
from odoo.tests.common import Form
from odoo import fields
@ -54,7 +53,7 @@ class TestMailingABTesting(TestMailingABTestingCommon):
self.ab_testing_mailing_2.mailing_trace_ids[:15].set_opened()
self.ab_testing_mailing_ids.invalidate_recordset()
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66)
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66.67)
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 50)
with self.mock_mail_gateway():
@ -86,10 +85,10 @@ class TestMailingABTesting(TestMailingABTestingCommon):
self.ab_testing_mailing_2.mailing_trace_ids[:15].set_opened()
self.ab_testing_mailing_ids.invalidate_recordset()
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66)
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66.67)
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 50)
with self.mock_mail_gateway():
with self.mock_mail_gateway(), self.enter_registry_test_mode():
self.env.ref('mass_mailing.ir_cron_mass_mailing_ab_testing').sudo().method_direct_trigger()
self.ab_testing_mailing_ids.invalidate_recordset()
winner_mailing = self.ab_testing_campaign.mailing_mail_ids.filtered(lambda mailing: mailing.ab_testing_pc == 100)
@ -169,7 +168,7 @@ class TestMailingABTesting(TestMailingABTestingCommon):
self.ab_testing_mailing_2.mailing_trace_ids[:15].set_opened()
self.ab_testing_mailing_ids.invalidate_recordset()
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66)
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66.67)
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 50)
with self.mock_mail_gateway():

View file

@ -2,30 +2,52 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from markupsafe import Markup
from requests import Session, PreparedRequest, Response
import datetime
import werkzeug
from odoo import tools
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mass_mailing.tests.common import MassMailCommon
from odoo.tests.common import HttpCase
from odoo.tests import tagged
from odoo.tests import HttpCase, tagged
from odoo.tools import mute_logger
@tagged('link_tracker')
class TestMailingControllers(MassMailCommon, HttpCase):
class TestMailingControllersCommon(MassMailCommon, HttpCase):
@classmethod
def setUpClass(cls):
super(TestMailingControllers, cls).setUpClass()
super(TestMailingControllersCommon, cls).setUpClass()
# cleanup lists
cls.env['mailing.list'].search([]).unlink()
cls._create_mailing_list()
cls.test_mailing = cls.env['mailing.mailing'].create({
cls.test_mailing_on_contacts = cls.env['mailing.mailing'].create({
'body_html': '<p>Hello <t t-out="object.name"/><br />Go to <a id="url" href="https://www.example.com/foo/bar?baz=qux">this link</a></p>',
'mailing_domain': [],
'mailing_model_id': cls.env['ir.model']._get_id('mailing.contact'),
'mailing_type': 'mail',
'name': 'TestMailing on Contacts',
'subject': 'TestMailing on Contacts',
})
cls.test_mailing_on_documents = cls.env['mailing.mailing'].create({
'body_html': '<p>Hello <t t-out="object.name"/><br />Go to <a id="url" href="https://www.example.com/foo/bar?baz=qux">this link</a></p>',
'mailing_domain': [],
'mailing_model_id': cls.env['ir.model']._get_id('res.partner'),
'mailing_type': 'mail',
'name': 'TestMailing on Documents',
'subject': 'TestMailing on Documents',
})
cls.test_mailing_on_lists = cls.env['mailing.mailing'].create({
'body_html': '<p>Hello <t t-out="object.name"/><br />Go to <a id="url" href="https://www.example.com/foo/bar?baz=qux">this link</a></p>',
'contact_list_ids': [(4, cls.mailing_list_1.id), (4, cls.mailing_list_2.id)],
'mailing_model_id': cls.env['ir.model']._get('mailing.list').id,
'mailing_model_id': cls.env['ir.model']._get_id('mailing.list'),
'mailing_type': 'mail',
'name': 'TestMailing',
'name': 'TestMailing on Lists',
'reply_to': cls.email_reply_to,
'subject': 'Test',
'subject': 'TestMailing on Lists',
})
cls.test_contact = cls.mailing_list_1.contact_ids[0]
@ -33,10 +55,587 @@ class TestMailingControllers(MassMailCommon, HttpCase):
# freeze time base value
cls._reference_now = datetime.datetime(2022, 6, 14, 10, 0, 0)
@classmethod
def _request_handler(cls, s: Session, r: PreparedRequest, /, **kw):
if r.url.startswith('https://www.example.com/foo/bar'):
r = Response()
r.status_code = 200
return r
return super()._request_handler(s, r, **kw)
@tagged('mailing_portal', 'post_install', '-at_install')
class TestMailingControllers(TestMailingControllersCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_email = tools.formataddr(('Déboulonneur', '<fleurus@example.com>'))
cls.test_email_normalized = 'fleurus@example.com'
def test_assert_initial_values(self):
""" Ensure test base data to ease test understanding. Globally test_email
is member of 2 mailing public lists. """
memberships = self.env['mailing.subscription'].search([
('contact_id.email_normalized', '=', self.test_email_normalized)]
)
self.assertEqual(memberships.list_id, self.mailing_list_1 + self.mailing_list_3)
self.assertEqual(memberships.mapped('opt_out'), [False, True])
contact_l1 = self.mailing_list_1.contact_ids.filtered(
lambda contact: contact.email == self.test_email_normalized
)
self.assertTrue(contact_l1)
self.assertFalse(contact_l1.is_blacklisted)
self.assertFalse(contact_l1.message_ids)
subscription_l1 = self.mailing_list_1.subscription_ids.filtered(
lambda subscription: subscription.contact_id == contact_l1
)
self.assertTrue(subscription_l1)
self.assertFalse(subscription_l1.is_blacklisted)
self.assertFalse(subscription_l1.opt_out)
self.assertFalse(subscription_l1.opt_out_datetime)
contact_l2 = self.mailing_list_2.contact_ids.filtered(
lambda contact: contact.email == self.test_email_normalized
)
self.assertFalse(contact_l2)
contact_l3 = self.mailing_list_3.contact_ids.filtered(
lambda contact: contact.email == self.test_email_normalized
)
self.assertTrue(contact_l3)
self.assertTrue(contact_l3 != contact_l1)
self.assertFalse(contact_l3.is_blacklisted)
subscription_l3 = self.mailing_list_3.subscription_ids.filtered(
lambda subscription: subscription.contact_id == contact_l3
)
self.assertFalse(subscription_l3.is_blacklisted)
self.assertTrue(subscription_l3.opt_out)
contact_l4 = self.mailing_list_4.contact_ids.filtered(
lambda contact: contact.email == self.test_email_normalized
)
self.assertFalse(contact_l4)
self.assertFalse(self.env['mail.blacklist'].search([('email', '=', self.test_email_normalized)]))
@mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view')
def test_mailing_report_unsubscribe(self):
""" Test deactivation of mailing report sending. It requires usage of
a hash token. """
test_mailing = self.test_mailing_on_lists.with_env(self.env)
self.env['ir.config_parameter'].sudo().set_param(
'mass_mailing.mass_mailing_reports', True
)
hash_token = test_mailing._generate_mailing_report_token(self.user_marketing.id)
self.authenticate('user_marketing', 'user_marketing')
# TEST: various invalid cases
for test_user_id, test_token, error_code in [
(self.user_marketing.id, '', 400), # no token
(self.user_marketing.id, 'zboobs', 401), # invalid token
(self.env.uid, hash_token, 401), # invalid credentials
]:
with self.subTest(test_user_id=test_user_id, test_token=test_token):
res = self.url_open(
tools.urls.urljoin(
test_mailing.get_base_url(),
f'mailing/report/unsubscribe?user_id={test_user_id}&token={test_token}',
)
)
self.assertEqual(res.status_code, error_code)
self.assertTrue(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports'))
# TEST: not mailing user
self.user_marketing.write({
'group_ids': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
})
res = self.url_open(
tools.urls.urljoin(
test_mailing.get_base_url(),
f'mailing/report/unsubscribe?user_id={self.user_marketing.id}&token={hash_token}',
)
)
self.assertEqual(res.status_code, 401)
self.assertTrue(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports'))
# TEST: finally valid call
self.user_marketing.write({
'group_ids': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
})
res = self.url_open(
tools.urls.urljoin(
test_mailing.get_base_url(),
f'mailing/report/unsubscribe?user_id={self.user_marketing.id}&token={hash_token}',
)
)
self.assertEqual(res.status_code, 200)
self.assertFalse(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports'))
def test_mailing_unsubscribe_from_document_tour(self):
""" Test portal unsubscribe on mailings performed on documents (not
mailing lists or contacts). Primary effect is to automatically exclude
the email (see tour).
Two tests are performed (with and without existing list subscriptions)
as it triggers the display of the mailing list part of the UI.
Tour effects
* unsubscribe from mailing based on a document = blocklist;
* add feedback (block list): Other reason, with 'My feedback' feedback;
* remove email from exclusion list;
* re-add email to exclusion list;
"""
opt_out_reasons = self.env['mailing.subscription.optout'].search([])
test_mailing = self.test_mailing_on_documents.with_env(self.env)
test_feedback = "My feedback"
for test_email, tour_name in [
('"Not Déboulonneur" <not.fleurus@example.com>', 'mailing_portal_unsubscribe_from_document'),
(self.test_email, 'mailing_portal_unsubscribe_from_document_with_lists'),
]:
with self.subTest(test_email=test_email, tour_name=tour_name):
test_partner = self.env['res.partner'].create({
'email': test_email,
'name': 'Test Déboulonneur'
})
self.assertFalse(test_partner.is_blacklisted)
previous_messages = test_partner.message_ids
test_email_normalized = tools.email_normalize(test_email)
# launch unsubscription tour
hash_token = test_mailing._generate_mailing_recipient_token(test_partner.id, test_partner.email_normalized)
with freeze_time(self._reference_now):
self.start_tour(
f"/mailing/{test_mailing.id}/unsubscribe?email={test_partner.email_normalized}&document_id={test_partner.id}&hash_token={hash_token}",
tour_name,
login=None,
)
# status update check
self.assertTrue(test_partner.is_blacklisted)
# partner (document): new message for blocklist addition with feedback
self.assertEqual(len(test_partner.message_ids), len(previous_messages) + 1)
msg_fb = test_partner.message_ids[0]
self.assertEqual(
msg_fb.body,
Markup(f'<p>Feedback from {test_email_normalized}<br>{test_feedback}</p>')
)
# posted messages on exclusion list record: activated, feedback, deactivated, activated again
bl_record = self.env['mail.blacklist'].search([('email', '=', test_partner.email_normalized)])
self.assertEqual(len(bl_record.message_ids), 5)
self.assertEqual(bl_record.opt_out_reason_id, opt_out_reasons[-1])
msg_bl2, msg_unbl, msg_fb, msg_bl, msg_create = bl_record.message_ids
self.assertEqual(
msg_bl2.body,
Markup(f'<p>Blocklist request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
f'data-oe-model="{test_partner._name}" data-oe-id="{test_partner.id}">Contact</a>)</p>')
)
self.assertEqual(
msg_unbl.body,
Markup(f'<p>Blocklist removal request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
f'data-oe-model="{test_partner._name}" data-oe-id="{test_partner.id}">Contact</a>)</p>')
)
self.assertEqual(
msg_fb.body,
Markup(f'<p>Feedback from {test_email_normalized}<br>{test_feedback}</p>')
)
self.assertTracking(msg_fb, [('opt_out_reason_id', 'many2one', False, opt_out_reasons[-1])])
self.assertEqual(
msg_bl.body,
Markup(f'<p>Blocklist request from unsubscribe link of mailing <a href="#" data-oe-model="{test_mailing._name}" '
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
f'data-oe-model="{test_partner._name}" data-oe-id="{test_partner.id}">Contact</a>)</p>')
)
self.assertEqual(msg_create.body, Markup('<p>Mail Blacklist created</p>'))
def test_mailing_unsubscribe_from_document_tour_mailing_user(self):
""" Test portal unsubscribe on mailings performed on documents (not
mailing lists or contacts) using a generic '/unsubscribe' link allowing
mailing users to see and edit unsubcribe page.
Tour effects
* unsubscribe from mailing based on a document = blocklist;
* add feedback (block list): Other reason, with 'My feedback' feedback;
* remove email from exclusion list;
* re-add email to exclusion list;
"""
# update user to link it with existing mailing contacts and allow the tour
# to run; test without and with mailing group
self.user_marketing.write({
'email': tools.formataddr(("Déboulonneur", "fleurus@example.com")),
'group_ids': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
})
test_mailing = self.test_mailing_on_documents.with_env(self.env)
self.authenticate('user_marketing', 'user_marketing')
# no group -> no direct access to /unsubscribe
res = self.url_open(
tools.urls.urljoin(
test_mailing.get_base_url(),
f'mailing/{test_mailing.id}/unsubscribe',
)
)
self.assertEqual(res.status_code, 400)
# group -> direct access to /unsubscribe should wokr
self.user_marketing.write({
'group_ids': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
})
# launch unsubscription tour
with freeze_time(self._reference_now):
self.start_tour(
f"/mailing/{test_mailing.id}/unsubscribe",
"mailing_portal_unsubscribe_from_document_with_lists",
login=self.user_marketing.login,
)
def test_mailing_unsubscribe_from_list_tour(self):
""" Test portal unsubscribe on mailings performed on mailing lists. Their
effect is to opt-out from the mailing list.
Tour effects
* unsubscribe from mailing based on lists = opt-out from lists;
* add feedback (opt-out): Other reason, with 'My feedback' feedback;
* add email to exclusion list;
"""
opt_out_reasons = self.env['mailing.subscription.optout'].search([])
test_mailing = self.test_mailing_on_lists.with_env(self.env)
test_feedback = "My feedback"
# fetch contact and its subscription and blacklist status, to see the tour effects
contact_l1 = self.mailing_list_1.contact_ids.filtered(
lambda contact: contact.email == self.test_email_normalized
)
subscription_l1 = self.mailing_list_1.subscription_ids.filtered(
lambda subscription: subscription.contact_id == contact_l1
)
# launch unsubscribe tour
hash_token = test_mailing._generate_mailing_recipient_token(contact_l1.id, contact_l1.email)
with freeze_time(self._reference_now):
self.start_tour(
f"/mailing/{test_mailing.id}/unsubscribe?email={self.test_email_normalized}&document_id={contact_l1.id}&hash_token={hash_token}",
"mailing_portal_unsubscribe_from_list",
login=None,
)
# status update check on list 1
self.assertTrue(subscription_l1.opt_out)
self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now)
self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1])
# status update check on list 2: unmodified (was not member, still not member)
contact_l2 = self.mailing_list_2.contact_ids.filtered(
lambda contact: contact.email == self.test_email_normalized
)
self.assertFalse(contact_l2)
# posted messages on contact record for mailing list 1: feedback, unsubscription
message_feedback = contact_l1.message_ids[0]
self.assertEqual(
message_feedback.body,
Markup(f'<p>Feedback from {contact_l1.email_normalized}<br>{test_feedback}</p>')
)
message_unsub = contact_l1.message_ids[1]
self.assertEqual(
message_unsub.body,
Markup(f'<p>{contact_l1.display_name} unsubscribed from the following mailing list(s)</p><ul><li>{self.mailing_list_1.name}</li></ul>')
)
# posted messages on exclusion list record: activated, deactivated, activated again
bl_record = self.env['mail.blacklist'].search([('email', '=', contact_l1.email_normalized)])
self.assertEqual(len(bl_record.message_ids), 2)
self.assertFalse(bl_record.opt_out_reason_id)
msg_bl, msg_create = bl_record.message_ids
self.assertEqual(
msg_bl.body,
Markup(f'<p>Blocklist request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
f'data-oe-model="{contact_l1._name}" data-oe-id="{contact_l1.id}">Mailing Contact</a>)</p>')
)
self.assertEqual(msg_create.body, Markup('<p>Mail Blacklist created</p>'))
def test_mailing_unsubscribe_from_list_with_update_tour(self):
""" Test portal unsubscribe on mailings performed on mailing lists. Their
effect is to opt-out from the mailing list. Optional exclusion list can
be done through interface (see tour).
Tour effects
* unsubscribe from mailing based on lists = opt-out from lists;
* add feedback (opt-out): Other reason, with 'My feedback' feedback;
* add email to exclusion list;
* remove email from exclusion list;
* come back to List3;
* join List2 (with no feedback, as no opt-out / block list was done);
* re-add email to exclusion list;
"""
opt_out_reasons = self.env['mailing.subscription.optout'].search([])
test_mailing = self.test_mailing_on_lists.with_env(self.env)
test_feedback = "My feedback"
# fetch contact and its subscription and blacklist status, to see the tour effects
contact_l1 = self.mailing_list_1.contact_ids.filtered(
lambda contact: contact.email == self.test_email_normalized
)
subscription_l1 = self.mailing_list_1.subscription_ids.filtered(
lambda subscription: subscription.contact_id == contact_l1
)
contact_l3 = self.mailing_list_3.contact_ids.filtered(
lambda contact: contact.email == self.test_email_normalized
)
subscription_l3 = self.mailing_list_3.subscription_ids.filtered(
lambda subscription: subscription.contact_id == contact_l3
)
# launch unsubscription tour
hash_token = test_mailing._generate_mailing_recipient_token(contact_l1.id, contact_l1.email)
with freeze_time(self._reference_now):
self.start_tour(
f"/mailing/{test_mailing.id}/unsubscribe?email={contact_l1.email}&document_id={contact_l1.id}&hash_token={hash_token}",
"mailing_portal_unsubscribe_from_list_with_update",
login=None,
)
# status update check on list 1
self.assertTrue(subscription_l1.opt_out)
self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now)
self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1])
# status update check on list 3 (opt-in during test)
self.assertFalse(subscription_l3.opt_out)
self.assertFalse(subscription_l3.opt_out_datetime)
# posted messages on contact record for mailing list 1: subscription update, feedback, unsubscription
message_update = contact_l1.message_ids[0]
self.assertEqual(
message_update.body,
Markup(f'<p>{contact_l1.display_name} subscribed to the following mailing list(s)</p>'
f'<ul><li>{self.mailing_list_2.name}</li></ul>')
)
message_feedback = contact_l1.message_ids[1]
self.assertEqual(
message_feedback.body,
Markup(f'<p>Feedback from {contact_l1.email_normalized}<br>{test_feedback}</p>')
)
message_unsub = contact_l1.message_ids[2]
self.assertEqual(
message_unsub.body,
Markup(f'<p>{contact_l1.display_name} unsubscribed from the following mailing list(s)</p>'
f'<ul><li>{self.mailing_list_1.name}</li></ul>')
)
# posted messages on contact record for mailing list 3: subscription
message_sub = contact_l3.message_ids[0]
self.assertEqual(
message_sub.body,
Markup(f'<p>{contact_l3.display_name} subscribed to the following mailing list(s)</p>'
f'<ul><li>{self.mailing_list_3.name}</li><li>{self.mailing_list_2.name}</li></ul>')
)
# posted messages on exclusion list record: activated, deactivated, activated again, feedback
bl_record = self.env['mail.blacklist'].search([('email', '=', contact_l1.email_normalized)])
self.assertEqual(bl_record.opt_out_reason_id, opt_out_reasons[0])
self.assertEqual(len(bl_record.message_ids), 5)
msg_fb, msg_bl2, msg_unbl, msg_bl, msg_create = bl_record.message_ids
self.assertTracking(msg_fb, [('opt_out_reason_id', 'many2one', False, opt_out_reasons[0])])
self.assertFalse(msg_fb.body)
self.assertEqual(
msg_bl2.body,
Markup(f'<p>Blocklist request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
f'data-oe-model="{contact_l1._name}" data-oe-id="{contact_l1.id}">Mailing Contact</a>)</p>')
)
self.assertEqual(
msg_unbl.body,
Markup(f'<p>Blocklist removal request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
f'data-oe-model="{contact_l1._name}" data-oe-id="{contact_l1.id}">Mailing Contact</a>)</p>')
)
self.assertEqual(
msg_bl.body,
Markup(f'<p>Blocklist request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
f'data-oe-model="{contact_l1._name}" data-oe-id="{contact_l1.id}">Mailing Contact</a>)</p>')
)
self.assertEqual(msg_create.body, Markup('<p>Mail Blacklist created</p>'))
def test_mailing_unsubscribe_from_my(self):
""" Test portal unsubscribe using the 'my' mailing-specific portal page.
It allows to opt-in / opt-out from mailing lists as well as to manage
blocklist (see tour).
Tour effects
* opt-in List3 from opt-out, opt-in List2, opt-out List1;
* add feedback (as new opt-out): Other reason, with 'My feedback' feedback;
* add email in block list;
* add feedback (as block list addition): First reason (hence no feedback);
"""
test_feedback = "My feedback"
portal_user = mail_new_test_user(
self.env,
email=tools.formataddr(("Déboulonneur", "fleurus@example.com")),
groups='base.group_portal',
login='user_portal_fleurus',
name='Déboulonneur User',
signature='--\nDéboulonneur',
)
_test_email, test_email_normalized = portal_user.email, portal_user.email_normalized
opt_out_reasons = self.env['mailing.subscription.optout'].search([])
# list opted-out and non-public should not be displayed
private_list = self.env['mailing.list'].with_context(self._test_context).create({
'contact_ids': [
(0, 0, {'name': 'Déboulonneur User', 'email': 'fleurus@example.com'}),
],
'name': 'List5',
'is_public': False
})
private_list.subscription_ids[0].opt_out = True
# launch 'my' mailing' tour
self.authenticate(portal_user.login, portal_user.login)
with freeze_time(self._reference_now):
self.start_tour(
"/mailing/my",
"mailing_portal_unsubscribe_from_my",
login=portal_user.login,
)
# fetch contact and its subscription and blacklist status, to see the tour effects
contact_l1 = self.mailing_list_1.contact_ids.filtered(
lambda contact: contact.email == test_email_normalized
)
subscription_l1 = self.mailing_list_1.subscription_ids.filtered(
lambda subscription: subscription.contact_id == contact_l1
)
contact_l2 = self.mailing_list_2.contact_ids.filtered(
lambda contact: contact.email == test_email_normalized
)
subscription_l2 = self.mailing_list_2.subscription_ids.filtered(
lambda subscription: subscription.contact_id == contact_l2
)
contact_l3 = self.mailing_list_3.contact_ids.filtered(
lambda contact: contact.email == test_email_normalized
)
subscription_l3 = self.mailing_list_3.subscription_ids.filtered(
lambda subscription: subscription.contact_id == contact_l3
)
self.assertEqual(contact_l2, contact_l3,
'When creating new membership, should link with first found existing contact')
self.assertTrue(contact_l1.is_blacklisted)
self.assertTrue(contact_l3.is_blacklisted)
self.assertTrue(subscription_l1.opt_out)
self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now,
'Subscription: opt-outed during test, datetime should have been set')
self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1])
self.assertFalse(subscription_l2.opt_out)
self.assertFalse(subscription_l2.opt_out_datetime)
self.assertFalse(subscription_l2.opt_out_reason_id)
self.assertFalse(subscription_l3.opt_out)
self.assertFalse(subscription_l3.opt_out_datetime,
'Subscription: opt-in during test, datetime should have been reset')
self.assertFalse(subscription_l3.opt_out_reason_id)
# message on contact for list 1: opt-out L1, join L2
msg_fb, msg_sub, msg_uns = contact_l1.message_ids
self.assertEqual(
msg_fb.body,
Markup(f'<p>Feedback from {portal_user.name} ({test_email_normalized})<br>{test_feedback}</p>')
)
self.assertEqual(
msg_sub.body,
Markup(f'<p>{contact_l1.name} subscribed to the following mailing list(s)</p>'
f'<ul><li>{self.mailing_list_2.name}</li></ul>')
)
self.assertEqual(
msg_uns.body,
Markup(f'<p>{contact_l1.name} unsubscribed from the following mailing list(s)</p>'
f'<ul><li>{self.mailing_list_1.name}</li></ul>')
)
# message on contact for list 2: opt-in L3 and L2
msg_fb, msg_sub = contact_l3.message_ids
self.assertEqual(
msg_fb.body,
Markup(f'<p>Feedback from {portal_user.name} ({test_email_normalized})<br>{test_feedback}</p>')
)
self.assertEqual(
msg_sub.body,
Markup(f'<p>{contact_l3.name} subscribed to the following mailing list(s)</p>'
f'<ul><li>{self.mailing_list_3.name}</li><li>{self.mailing_list_2.name}</li></ul>')
)
# block list record created, feedback logged
bl_record = self.env['mail.blacklist'].search([('email', '=', contact_l1.email_normalized)])
self.assertEqual(bl_record.opt_out_reason_id, opt_out_reasons[0])
self.assertEqual(len(bl_record.message_ids), 3)
msg_fb, msg_bl, _msg_create = bl_record.message_ids
self.assertTracking(msg_fb, [('opt_out_reason_id', 'many2one', False, opt_out_reasons[0])])
self.assertEqual(msg_bl.body, Markup('<p>Blocklist request from portal</p>'))
@mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view')
def test_mailing_view(self):
""" Test preview of mailing. It requires either a token, either being
mailing user. """
test_mailing = self.test_mailing_on_documents.with_env(self.env)
shadow_mailing = test_mailing.copy()
doc_id, email_normalized = self.user_marketing.partner_id.id, self.user_marketing.email_normalized
hash_token = test_mailing._generate_mailing_recipient_token(doc_id, email_normalized)
self.user_marketing.write({
'group_ids': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
})
self.authenticate('user_marketing', 'user_marketing')
# TEST: various invalid cases
for test_mid, test_doc_id, test_email, test_token, error_code in [
(test_mailing.id, doc_id, email_normalized, '', 400), # no token
(test_mailing.id, doc_id, email_normalized, 'zboobs', 401), # wrong token
(test_mailing.id, self.env.user.partner_id.id, email_normalized, hash_token, 401), # mismatch
(test_mailing.id, doc_id, 'not.email@example.com', hash_token, 401), # mismatch
(shadow_mailing.id, doc_id, email_normalized, hash_token, 401), # valid credentials but wrong mailing_id
(0, doc_id, email_normalized, hash_token, 400), # valid credentials but missing mailing_id
]:
with self.subTest(test_mid=test_mid, test_email=test_email, test_doc_id=test_doc_id, test_token=test_token):
res = self.url_open(
tools.urls.urljoin(
test_mailing.get_base_url(),
f'mailing/{test_mid}/view?email={test_email}&document_id={test_doc_id}&hash_token={test_token}',
)
)
self.assertEqual(res.status_code, error_code)
# TEST: valid call using credentials
res = self.url_open(
tools.urls.urljoin(
test_mailing.get_base_url(),
f'mailing/{test_mailing.id}/view?email={email_normalized}&document_id={doc_id}&hash_token={hash_token}',
)
)
self.assertEqual(res.status_code, 200)
# TEST: invalid credentials but mailing user
self.user_marketing.write({
'group_ids': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
})
res = self.url_open(
tools.urls.urljoin(
test_mailing.get_base_url(),
f'mailing/{test_mailing.id}/view',
)
)
self.assertEqual(res.status_code, 200)
@tagged('link_tracker', 'mailing_portal')
class TestMailingTracking(TestMailingControllersCommon):
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mass_mailing.models.mailing')
def test_tracking_short_code(self):
""" Test opening short code linked to a mailing trace: should set the
trace as opened and clicked, create a click record. """
mailing = self.test_mailing.with_env(self.env)
mailing = self.test_mailing_on_lists.with_env(self.env)
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
@ -53,43 +652,47 @@ class TestMailingControllers(MassMailCommon, HttpCase):
self.assertFalse(mailing_trace.open_datetime)
self.assertEqual(mailing_trace.trace_status, 'sent')
short_link_url = werkzeug.urls.url_join(
short_link_url = tools.urls.urljoin(
mail.get_base_url(),
f'r/{link_tracker_code.code}/m/{mailing_trace.id}'
)
with freeze_time(self._reference_now):
response = self.url_open(short_link_url, allow_redirects=False)
self.assertEqual(response.headers['Location'], 'https://www.example.com/foo/bar?baz=qux&utm_source=TestMailing&utm_medium=Email')
self.assertTrue(response.headers['Location'].startswith('https://www.example.com/foo/bar?baz=qux'))
self.assertEqual(link_tracker_code.link_id.count, 1)
self.assertEqual(mailing_trace.links_click_datetime, self._reference_now)
self.assertEqual(mailing_trace.open_datetime, self._reference_now)
self.assertEqual(mailing_trace.trace_status, 'open')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mass_mailing.models.mailing')
def test_tracking_url_token(self):
""" Test tracking of mails linked to a mailing trace: should set the
trace as opened. """
mailing = self.test_mailing.with_env(self.env)
mailing = self.test_mailing_on_lists.with_env(self.env)
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
mail = self._find_mail_mail_wrecord(self.test_contact)
mail_id_int = mail.id
mail_tracking_url = mail._get_tracking_url()
mailing_trace = mail.mailing_trace_ids
self.assertEqual(mail.state, 'sent')
self.assertEqual(len(mailing_trace), 1)
self.assertFalse(mailing_trace.open_datetime)
self.assertEqual(mailing_trace.trace_status, 'sent')
mail.unlink() # the mail might be removed during the email sending
self.env.flush_all()
with freeze_time(self._reference_now):
response = self.url_open(mail._get_tracking_url())
response = self.url_open(mail_tracking_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(mail.state, 'sent')
self.assertEqual(mailing_trace.open_datetime, self._reference_now)
self.assertEqual(mailing_trace.trace_status, 'open')
track_url = werkzeug.urls.url_join(
mail.get_base_url(),
'mail/track/%s/fake_token/blank.gif' % mail.id
track_url = tools.urls.urljoin(
mailing.get_base_url(),
f'mail/track/{mail_id_int}/fake_token/blank.gif'
)
response = self.url_open(track_url)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.status_code, 401)

View file

@ -15,7 +15,7 @@ from odoo.addons.base.tests.test_ir_cron import CronMixinCase
from odoo.addons.mass_mailing.tests.common import MassMailCommon
from odoo.exceptions import ValidationError
from odoo.sql_db import Cursor
from odoo.tests.common import users, Form, HttpCase, tagged
from odoo.tests import Form, HttpCase, users, tagged
from odoo.tools import mute_logger
BASE_64_STRING = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
@ -51,9 +51,9 @@ class TestMassMailValues(MassMailCommon):
return urls
else:
return []
with patch("odoo.addons.mass_mailing.models.mailing.MassMailing._get_image_by_url",
with patch("odoo.addons.mass_mailing.models.mailing.MailingMailing._get_image_by_url",
new=patched_get_image), \
patch("odoo.addons.mass_mailing.models.mailing.MassMailing._create_attachments_from_inline_images",
patch("odoo.addons.mass_mailing.models.mailing.MailingMailing._create_attachments_from_inline_images",
new=patched_images_to_urls):
mailing = self.env['mailing.mailing'].create({
'name': 'Test',
@ -61,19 +61,19 @@ class TestMassMailValues(MassMailCommon):
'state': 'draft',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
'body_html': """
<html>
<section>
<!--[if mso]>
<v:image src="https://www.example.com/image" style="width:100px;height:100px;"/>
<![endif]-->
</html>
</section>
""",
})
self.assertEqual(str(mailing.body_html), f"""
<html>
self.assertEqual(str(mailing.body_html).strip(), f"""
<section>
<!--[if mso]>
<v:image src="/web/image/{attachment['id']}?access_token={attachment['token']}" style="width:100px;height:100px;"/>
<![endif]-->
</html>
</section>
""".strip())
@users('user_marketing')
@ -94,7 +94,7 @@ class TestMassMailValues(MassMailCommon):
'token': attachment_token,
})
return urls
with patch("odoo.addons.mass_mailing.models.mailing.MassMailing._create_attachments_from_inline_images",
with patch("odoo.addons.mass_mailing.models.mailing.MailingMailing._create_attachments_from_inline_images",
new=patched_images_to_urls):
mailing = self.env['mailing.mailing'].create({
'name': 'Test',
@ -102,7 +102,7 @@ class TestMassMailValues(MassMailCommon):
'state': 'draft',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
'body_html': f"""
<html><body>
<section>
<img src="data:image/png;base64,{BASE_64_STRING}0">
<img src="data:image/jpg;base64,{BASE_64_STRING}1">
<div style='color: red; background-image:url("data:image/jpg;base64,{BASE_64_STRING}2"); display: block;'/>
@ -124,21 +124,21 @@ class TestMassMailValues(MassMailCommon):
<div style="color: red; background-image: url(data:image/jpg;base64,{BASE_64_STRING}16); background: url('data:image/jpg;base64,{BASE_64_STRING}17'); display: block;"/>
<![endif]-->
<img src="data:image/png;base64,{BASE_64_STRING}0">
</body></html>
</section>
""",
})
self.assertEqual(len(attachments), 19)
self.assertEqual(attachments[0]['id'], attachments[18]['id'])
self.assertEqual(str(mailing.body_html), f"""
<html><body>
<img src="/web/image/{attachments[0]['id']}?access_token={attachments[0]['token']}">
<img src="/web/image/{attachments[1]['id']}?access_token={attachments[1]['token']}">
<div style='color: red; background-image:url("/web/image/{attachments[2]['id']}?access_token={attachments[2]['token']}"); display: block;'></div>
<div style="color: red; background-image:url('/web/image/{attachments[3]['id']}?access_token={attachments[3]['token']}'); display: block;"></div>
<div style='color: red; background-image:url("/web/image/{attachments[4]['id']}?access_token={attachments[4]['token']}"); display: block;'></div>
<div style='color: red; background-image:url("/web/image/{attachments[5]['id']}?access_token={attachments[5]['token']}"); display: block;'></div>
<div style="color: red; background-image:url(/web/image/{attachments[6]['id']}?access_token={attachments[6]['token']}); display: block;"></div>
<div style="color: red; background-image: url(/web/image/{attachments[7]['id']}?access_token={attachments[7]['token']}); background: url('/web/image/{attachments[8]['id']}?access_token={attachments[8]['token']}'); display: block;"></div>
self.assertEqual(str(mailing.body_html).strip(), f"""
<section>
<img src="/web/image/{attachments[0]['id']}?access_token={attachments[0]['token']}"/>
<img src="/web/image/{attachments[1]['id']}?access_token={attachments[1]['token']}"/>
<div style="color: red; background-image:url(&quot;/web/image/{attachments[2]['id']}?access_token={attachments[2]['token']}&quot;); display: block;"/>
<div style="color: red; background-image:url('/web/image/{attachments[3]['id']}?access_token={attachments[3]['token']}'); display: block;"/>
<div style="color: red; background-image:url(&quot;/web/image/{attachments[4]['id']}?access_token={attachments[4]['token']}&quot;); display: block;"/>
<div style="color: red; background-image:url(&quot;/web/image/{attachments[5]['id']}?access_token={attachments[5]['token']}&quot;); display: block;"/>
<div style="color: red; background-image:url(/web/image/{attachments[6]['id']}?access_token={attachments[6]['token']}); display: block;"/>
<div style="color: red; background-image: url(/web/image/{attachments[7]['id']}?access_token={attachments[7]['token']}); background: url('/web/image/{attachments[8]['id']}?access_token={attachments[8]['token']}'); display: block;"/>
<!--[if mso]>
<img src="/web/image/{attachments[9]['id']}?access_token={attachments[9]['token']}">Fake url, in text: img src="data:image/png;base64,{BASE_64_STRING}"
Fake url, in text: img src="data:image/png;base64,{BASE_64_STRING}"
@ -151,8 +151,8 @@ class TestMassMailValues(MassMailCommon):
<div style="color: red; background-image:url(/web/image/{attachments[15]['id']}?access_token={attachments[15]['token']}); display: block;"/>
<div style="color: red; background-image: url(/web/image/{attachments[16]['id']}?access_token={attachments[16]['token']}); background: url('/web/image/{attachments[17]['id']}?access_token={attachments[17]['token']}'); display: block;"/>
<![endif]-->
<img src="/web/image/{attachments[18]['id']}?access_token={attachments[18]['token']}">
</body></html>
<img src="/web/image/{attachments[18]['id']}?access_token={attachments[18]['token']}"/>
</section>
""".strip())
@users('user_marketing')
@ -178,14 +178,14 @@ class TestMassMailValues(MassMailCommon):
composer = self.env['mail.compose.message'].with_user(self.user_marketing).with_context({
'default_composition_mode': 'mass_mail',
'default_model': 'res.partner',
'default_res_id': recipient.id,
'default_res_ids': recipient.ids,
}).create({
'subject': 'Mass Mail Responsive',
'body': 'I am Responsive body',
'mass_mailing_id': mailing.id
})
mail_values = composer.get_mail_values([recipient.id])
mail_values = composer._prepare_mail_values([recipient.id])
body_html = mail_values[recipient.id]['body_html']
self.assertIn('<!DOCTYPE html>', body_html)
@ -211,8 +211,7 @@ class TestMassMailValues(MassMailCommon):
self.assertEqual(mailing.mailing_model_real, 'res.partner')
self.assertEqual(mailing.reply_to_mode, 'new')
self.assertEqual(mailing.reply_to, self.user_marketing.email_formatted)
# default for partner: remove blacklisted
self.assertEqual(literal_eval(mailing.mailing_domain), [('is_blacklisted', '=', False)])
self.assertEqual(literal_eval(mailing.mailing_domain), [])
# update domain
mailing.write({
'mailing_domain': [('email', 'ilike', 'test.example.com')]
@ -237,10 +236,10 @@ class TestMassMailValues(MassMailCommon):
# reset mailing model -> reset domain and reply to mode
mailing.write({
'mailing_model_id': self.env['ir.model']._get('mail.channel').id,
'mailing_model_id': self.env['ir.model']._get('discuss.channel').id,
})
self.assertEqual(mailing.mailing_model_name, 'mail.channel')
self.assertEqual(mailing.mailing_model_real, 'mail.channel')
self.assertEqual(mailing.mailing_model_name, 'discuss.channel')
self.assertEqual(mailing.mailing_model_real, 'discuss.channel')
self.assertEqual(mailing.reply_to_mode, 'update')
self.assertFalse(mailing.reply_to)
@ -255,14 +254,13 @@ class TestMassMailValues(MassMailCommon):
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
})
# default for partner: remove blacklisted
self.assertEqual(literal_eval(mailing.mailing_domain), [('is_blacklisted', '=', False)])
self.assertEqual(literal_eval(mailing.mailing_domain), [])
# prepare initial data
filter_1, filter_2, filter_3 = self.env['mailing.filter'].create([
{'name': 'General channel',
'mailing_domain' : [('name', '=', 'general')],
'mailing_model_id': self.env['ir.model']._get('mail.channel').id,
'mailing_model_id': self.env['ir.model']._get('discuss.channel').id,
},
{'name': 'LLN City',
'mailing_domain' : [('city', 'ilike', 'LLN')],
@ -283,7 +281,7 @@ class TestMassMailValues(MassMailCommon):
mailing.mailing_filter_id = filter_1
# resetting model should reset domain, even if filter was chosen previously
mailing.mailing_model_id = self.env['ir.model']._get('mail.channel').id
mailing.mailing_model_id = self.env['ir.model']._get('discuss.channel').id
self.assertEqual(literal_eval(mailing.mailing_domain), [])
# changing the filter should update the mailing domain correctly
@ -328,17 +326,17 @@ class TestMassMailValues(MassMailCommon):
# for mass mailing. from_filter matches domain of company alias domain
# before record creation
{
'name': 'mass_mailing_test_match_from_filter',
'from_filter': self.alias_domain,
'smtp_host': 'not_real@smtp.com',
'name' : 'mass_mailing_test_match_from_filter',
'from_filter' : self.alias_domain,
'smtp_host' : 'not_real@smtp.com',
},
# Case where alias domain is set and there is a default outgoing email server
# for mass mailing. from_filter DOES NOT match domain of company alias domain
# before record creation
{
'name': 'mass_mailing_test_from_missmatch',
'from_filter': 'notcompanydomain.com',
'smtp_host': 'not_real@smtp.com',
'name' : 'mass_mailing_test_from_missmatch',
'from_filter' : 'test.com',
'smtp_host' : 'not_real@smtp.com',
},
])
@ -351,7 +349,7 @@ class TestMassMailValues(MassMailCommon):
]
expected_from_all = [
self.env.user.email_formatted, # default when no server
self.env['ir.mail_server']._get_default_from_address(), # matches company alias domain
self.env.user.company_id.alias_domain_id.default_from_email, # matches company alias domain
self.env.user.email_formatted, # not matching from filter -> back to user from
]
@ -361,8 +359,8 @@ class TestMassMailValues(MassMailCommon):
# settings to designate a dedicated outgoing email server
if mail_server:
self.env['res.config.settings'].sudo().create({
'mass_mailing_mail_server_id': mail_server.id,
'mass_mailing_outgoing_mail_server': mail_server,
'mass_mailing_mail_server_id' : mail_server.id,
'mass_mailing_outgoing_mail_server' : mail_server,
}).execute()
# Create mailing
@ -390,6 +388,35 @@ class TestMassMailValues(MassMailCommon):
)
self.assertEqual(mailing_form.mailing_model_real, 'res.partner')
@users('user_marketing')
def test_mailing_create_on_send(self):
recipient = self.env['res.partner'].create({
'name': 'Mass Mail Partner',
'email': 'Customer <test.customer@example.com>',
})
mass_mailing_name = "An arbitrary mailing name"
composer = self.env['mail.compose.message'].with_user(self.user_marketing).with_context({
'default_composition_mode': 'mass_mail',
'default_model': 'res.partner',
'default_res_ids': recipient.ids,
}).create({
'subject': 'Mass Mail Responsive',
'body': 'I am Responsive body',
'mass_mailing_name': mass_mailing_name
})
self.assertFalse(composer.mass_mailing_id, "No mailing should've been created")
with self.mock_mail_gateway():
composer._action_send_mail(recipient.ids)
self.assertEqual(len(composer.mass_mailing_id.ids), 1, "A mailing should've been created")
self.assertEqual(composer.mass_mailing_id.name, mass_mailing_name, f"Mailing name should be: {mass_mailing_name}")
mail_values = composer._prepare_mail_values(recipient.ids)[recipient.id]
self.assertIn(f"Received the mailing <b>{mass_mailing_name}</b>", mail_values["body"], "The composer doesn't use the provided mass_mailing_name")
@mute_logger('odoo.sql_db')
@users('user_marketing')
def test_mailing_trace_values(self):
@ -492,8 +519,8 @@ class TestMassMailValues(MassMailCommon):
'schedule_date': datetime(2023, 2, 17, 11, 0),
})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._process_mass_mailing_queue()
with self.mock_mail_gateway(mail_unlink_sent=False), self.enter_registry_test_mode():
self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().method_direct_trigger()
self.assertFalse(mailing.body_html)
self.assertEqual(mailing.mailing_model_name, 'res.partner')
@ -710,14 +737,19 @@ class TestMassMailFeatures(MassMailCommon, CronMixinCase):
'mailing_domain': [('id', 'in', (partner_a | partner_b).ids)],
'body_html': 'This is mass mail marketing demo'
})
self.assertEqual(mailing.user_id, self.user_marketing)
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._process_mass_mailing_queue()
self.assertEqual(mailing.email_from, self.env.user.email_formatted)
with self.mock_mail_gateway(mail_unlink_sent=False), self.enter_registry_test_mode():
self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().method_direct_trigger()
author = self.user_marketing.partner_id
email_values = {'email_from': mailing.email_from}
self.assertMailTraces(
[{'partner': partner_a},
{'partner': partner_b, 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
mailing, partner_a + partner_b, check_mail=True
[{'partner': partner_a, 'email_values': email_values},
{'partner': partner_b, 'trace_status': 'cancel', 'failure_type': 'mail_bl', 'email_values': email_values}],
mailing, partner_a + partner_b,
check_mail=True, author=author,
)
@users('user_marketing')
@ -745,14 +777,17 @@ Email: <a id="url5" href="mailto:test@odoo.com">test@odoo.com</a></div>""",
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._process_mass_mailing_queue()
with self.mock_mail_gateway(mail_unlink_sent=False), self.enter_registry_test_mode():
self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().method_direct_trigger()
author = self.user_marketing.partner_id
email_values = {'email_from': mailing.email_from}
self.assertMailTraces(
[{'email': 'fleurus@example.com'},
{'email': 'gorramts@example.com'},
{'email': 'ybrant@example.com'}],
mailing, self.mailing_list_1.contact_ids, check_mail=True
[{'email': 'fleurus@example.com', 'email_values': email_values},
{'email': 'gorramts@example.com', 'email_values': email_values},
{'email': 'ybrant@example.com', 'email_values': email_values}],
mailing, self.mailing_list_1.contact_ids,
check_mail=True, author=author,
)
for contact in self.mailing_list_1.contact_ids:
@ -813,7 +848,7 @@ class TestMailingHeaders(MassMailCommon, HttpCase):
# check outgoing email headers (those are put into outgoing email
# not in the mail.mail record)
email = self._find_sent_mail_wemail(contact.email)
email = self._find_sent_email_wemail(contact.email)
headers = email.get("headers")
unsubscribe_oneclick_url = test_mailing._get_unsubscribe_oneclick_url(contact.email, contact.id)
self.assertTrue(headers, "Mass mailing emails should have headers for unsubscribe")
@ -826,10 +861,10 @@ class TestMailingHeaders(MassMailCommon, HttpCase):
# unsubscribe in one-click
unsubscribe_oneclick_url = headers["List-Unsubscribe"].strip("<>")
self.opener.post(unsubscribe_oneclick_url)
self.url_open(unsubscribe_oneclick_url, method='POST')
# should be unsubscribed
self.assertTrue(contact.subscription_list_ids.opt_out)
self.assertTrue(contact.subscription_ids.opt_out)
class TestMailingScheduleDateWizard(MassMailCommon):
@ -854,3 +889,27 @@ class TestMailingScheduleDateWizard(MassMailCommon):
self.assertEqual(mailing.schedule_date, datetime(2021, 4, 30, 9, 0))
self.assertEqual(mailing.schedule_type, 'scheduled')
self.assertEqual(mailing.state, 'in_queue')
class TestMassMailingActions(MassMailCommon):
def test_mailing_action_open(self):
mass_mailings = self.env['mailing.mailing'].create([
{'subject': 'First subject'},
{'subject': 'Second subject'}
])
# Create two traces: one linked to the created mass.mailing and one not (action should open only the first)
self.env["mailing.trace"].create([{
"trace_status": "open",
"mass_mailing_id": mass_mailings[0].id,
"model": "res.partner",
"res_id": self.partner_admin.id,
}, {
"trace_status": "open",
"mass_mailing_id": mass_mailings[1].id,
"model": "res.partner",
"res_id": self.partner_employee.id,
}
])
results = mass_mailings[0].action_view_opened()
results_partner = self.env["res.partner"].search(results['domain'])
self.assertEqual(results_partner, self.partner_admin, "Trace leaked from mass_mailing_2 to mass_mailing_1")

View file

@ -1,14 +1,48 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import copy
from datetime import datetime
from freezegun import freeze_time
from unittest.mock import patch
from odoo import exceptions
from odoo.addons.mass_mailing.tests.common import MassMailCommon
from odoo.tests.common import Form, users
from odoo.tests import Form, tagged, users
@tagged('mailing_list')
class TestMailingContactAccess(MassMailCommon):
@users('user_marketing')
def test_mailing_contact_properties_access(self):
# Check that mailing user can edit properties on mailing contact
value = [{'type': 'char', 'name': 'test', 'value': 'test', 'definition_changed': True}]
contact = self.env['mailing.contact'].create({'properties': copy.deepcopy(value)})
self.assertEqual(dict(contact.properties), {'test': 'test'})
delete_value = [{'type': 'char', 'name': 'test', 'value': 'test', 'definition_deleted': True}]
contact = self.env['mailing.contact'].create({'properties': copy.deepcopy(delete_value)})
self.assertEqual(dict(contact.properties), {})
# Sanity check, mailing user can only edit the definition on partner in SUDO
with self.assertRaises(exceptions.AccessError):
self.env['res.partner'].create({'properties': copy.deepcopy(value), 'name': 'test'})
partner = self.env['res.partner'].sudo().create({'properties': copy.deepcopy(value), 'name': 'test'})
self.assertEqual(dict(partner.properties), {'test': 'test'})
base_definition = self.env['properties.base.definition']._get_definition_for_property_field('mailing.contact', 'properties')
self.assertTrue(base_definition)
with self.assertRaises(exceptions.AccessError):
base_definition.properties_field_id = self.env["ir.model.fields"].sudo()._get('res.partner', 'properties').id
partner_base_definition = self.env['properties.base.definition']._get_definition_for_property_field('res.partner', 'properties')
with self.assertRaises(exceptions.AccessError):
partner_base_definition.unlink()
@tagged('mailing_list')
class TestMailingContactToList(MassMailCommon):
@users('user_marketing')
@ -27,13 +61,17 @@ class TestMailingContactToList(MassMailCommon):
# create wizard with context values
wizard_form = Form(self.env['mailing.contact.to.list'].with_context(default_contact_ids=contacts.ids))
self.assertEqual(wizard_form.contact_ids._get_ids(), contacts.ids)
self.assertEqual(wizard_form.contact_ids.ids, contacts.ids)
# set mailing list and add contacts
wizard_form.mailing_list_id = mailing
wizard = wizard_form.save()
action = wizard.action_add_contacts()
self.assertEqual(contacts.list_ids, mailing)
frozen_time = datetime(2025, 1, 1, 0, 0)
with self.mock_datetime_and_now(frozen_time):
action = wizard.action_add_contacts()
self.assertEqual(contacts.list_ids, mailing)
create_dates = contacts.subscription_ids.mapped('create_date')
self.assertTrue(all(date == frozen_time for date in create_dates), "All create dates should be equal to frozen datetime")
self.assertEqual(action["type"], "ir.actions.client")
self.assertTrue(action.get("params", {}).get("next"), "Should return a notification with a next action")
subaction = action["params"]["next"]
@ -55,6 +93,7 @@ class TestMailingContactToList(MassMailCommon):
self.assertEqual(subaction["context"]["default_contact_list_ids"], [mailing2.id])
@tagged('mailing_list')
class TestMailingListMerge(MassMailCommon):
@classmethod
@ -84,7 +123,7 @@ class TestMailingListMerge(MassMailCommon):
new = self.env['mailing.contact'].with_context(default_list_ids=default_list_ids).create([{
'name': 'Contact_%d' % x,
'email': 'contact_%d@test.example.com' % x,
'subscription_list_ids': [(0, 0, {
'subscription_ids': [(0, 0, {
'list_id': self.mailing_list_1.id,
'opt_out': True,
}), (0, 0, {
@ -102,18 +141,32 @@ class TestMailingListMerge(MassMailCommon):
new = new.with_context(default_list_ids=[list_id])
self.assertFalse(any(contact.opt_out for contact in new))
with freeze_time('2022-01-01 12:00'):
with freeze_time('2022-01-01 12:00'), \
patch.object(self.env.cr, 'now', lambda: datetime(2022, 1, 1, 12, 0, 0)):
contact_form = Form(self.env['mailing.contact'])
contact_form.name = 'Contact_test'
with contact_form.subscription_list_ids.new() as subscription:
with contact_form.subscription_ids.new() as subscription:
subscription.list_id = self.mailing_list_1
subscription.opt_out = True
with contact_form.subscription_list_ids.new() as subscription:
with contact_form.subscription_ids.new() as subscription:
subscription.list_id = self.mailing_list_2
subscription.opt_out = False
contact = contact_form.save()
self.assertEqual(contact.subscription_list_ids[0].unsubscription_date, datetime(2022, 1, 1, 12, 0, 0))
self.assertFalse(contact.subscription_list_ids[1].unsubscription_date)
self.assertEqual(contact.subscription_ids.filtered(lambda s: s.list_id == self.mailing_list_1).opt_out_datetime, datetime(2022, 1, 1, 12, 0, 0))
self.assertFalse(contact.subscription_ids.filtered(lambda s: s.list_id == self.mailing_list_2).opt_out_datetime)
@users('user_marketing')
def test_mailing_list_action_send_mailing(self):
mailing_ctx = self.mailing_list_1.action_send_mailing().get('context', {})
form = Form(self.env['mailing.mailing'].with_context(mailing_ctx))
form.subject = 'Test Mail'
mailing = form.save()
# Check that mailing model and mailing list are set properly
self.assertEqual(
mailing.mailing_model_id, self.env['ir.model']._get('mailing.list'),
'Should have correct mailing model set')
self.assertEqual(mailing.contact_list_ids, self.mailing_list_1, 'Should have correct mailing list set')
self.assertEqual(mailing.mailing_type, 'mail', 'Should have correct mailing_type')
@users('user_marketing')
def test_mailing_list_contact_copy_in_context_of_mailing_list(self):
@ -121,7 +174,7 @@ class TestMailingListMerge(MassMailCommon):
contact_1 = MailingContact.create({
'name': 'Sam',
'email': 'gamgee@shire.com',
'subscription_list_ids': [(0, 0, {'list_id': self.mailing_list_3.id})],
'subscription_ids': [(0, 0, {'list_id': self.mailing_list_3.id})],
})
# Copy the contact with default_list_ids in context, which should not raise anything
contact_2 = contact_1.with_context(default_list_ids=self.mailing_list_3.ids).copy()
@ -175,6 +228,7 @@ class TestMailingListMerge(MassMailCommon):
self.assertEqual(merge.dest_list_id, self.mailing_list_3)
@tagged('mailing_list')
class TestMailingContactImport(MassMailCommon):
"""Test the transient <mailing.contact.import>."""
@ -238,6 +292,7 @@ class TestMailingContactImport(MassMailCommon):
# Test that the context key "default_list_ids" is ignored (because we manually set list_ids)
contact_import.with_context(default_list_ids=(first_list | second_list).ids).action_import()
self.env['mailing.list'].invalidate_model(['contact_ids'])
# Check the contact of the first mailing list
contacts = [
(contact.name, contact.email)
@ -269,3 +324,49 @@ class TestMailingContactImport(MassMailCommon):
contact = self.env['mailing.contact'].search([('email', '=', 'already_exists_list_1@example.com')])
self.assertEqual(len(contact), 1, 'Should have updated the existing contact instead of creating a new one')
@tagged('mailing_list')
class TestSubscriptionManagement(MassMailCommon):
@users('user_marketing')
def test_mailing_update_optout(self):
_email_formatted = '"Mireille Labeille" <mireille@test.example.com>'
_email_formatted_upd = '"Mireille Oreille-Labeille" <mireille@test.example.com>'
_email_normalized = 'mireille@test.example.com'
self._create_mailing_list()
ml_1, ml_2 = self.mailing_list_1.with_env(self.env), self.mailing_list_2.with_env(self.env)
ml_3 = self._create_mailing_list_of_x_contacts(3)
self.assertEqual(ml_1.contact_count, 3)
self.assertEqual(ml_1.contact_count_blacklisted, 0)
self.assertEqual(ml_1.contact_count_email, 3)
self.assertEqual(ml_1.contact_count_opt_out, 0)
self.assertEqual(ml_2.contact_count, 4)
self.assertEqual(ml_2.contact_count_blacklisted, 0)
self.assertEqual(ml_2.contact_count_email, 4)
self.assertEqual(ml_2.contact_count_opt_out, 0)
self.assertEqual(ml_3.contact_count, 3)
self.assertEqual(ml_3.contact_count_blacklisted, 0)
self.assertEqual(ml_3.contact_count_email, 3)
self.assertEqual(ml_3.contact_count_opt_out, 0)
# create a new test contact
contact = self.env['mailing.contact'].browse(
self.env['mailing.contact'].name_create(_email_formatted)[0]
)
self.assertEqual(contact.email, _email_normalized)
self.assertEqual(contact.name, 'Mireille Labeille')
# add new subscriptions (and ensure email_normalized is used)
(ml_1 + ml_2)._update_subscription_from_email(_email_formatted_upd, opt_out=False)
subs = self.env['mailing.subscription'].search(
[('contact_id', '=', contact.id)]
)
self.assertEqual(subs.list_id, ml_1 + ml_2)
# opt-out from opted-in mailing list + 1 non opted-in mailing list
(ml_2 + ml_3)._update_subscription_from_email(_email_formatted_upd, opt_out=True)
subs = self.env['mailing.subscription'].search(
[('contact_id', '=', contact.id)]
)
self.assertEqual(subs.list_id, ml_1 + ml_2)

View file

@ -27,10 +27,14 @@ class TestMailingRetry(MassMailCommon, CronMixinCase):
mailing.action_launch()
# force email sending to fail to test our retry mechanism
def patched_mail_mail_send(mail_records, auto_commit=False, raise_exception=False, smtp_session=None):
def patched_mail_mail_send(mail_records, auto_commit=False, raise_exception=False, smtp_session=None,
alias_domain_id=False, mail_server=False, post_send_callback=None):
mail_records.write({'state': 'exception', 'failure_reason': 'forced_failure'})
with patch('odoo.addons.mail.models.mail_mail.MailMail._send', patched_mail_mail_send):
with (
patch('odoo.addons.mail.models.mail_mail.MailMail._send', patched_mail_mail_send),
self.enter_registry_test_mode(),
):
self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().method_direct_trigger()
with self.capture_triggers('mass_mailing.ir_cron_mass_mailing_queue') as captured_triggers:

View file

@ -1,45 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.addons.mass_mailing.tests.common import MassMailCommon
from odoo.tests import tagged
@odoo.tests.tagged('-at_install', 'post_install')
class TestUi(HttpCaseWithUserDemo):
def setUp(self):
super().setUp()
self.user_demo.groups_id |= self.env.ref('mass_mailing.group_mass_mailing_user')
self.user_demo.groups_id |= self.env.ref('mail.group_mail_template_editor')
self.user_demo.groups_id |= self.env.ref('mass_mailing.group_mass_mailing_campaign')
@tagged('-at_install', 'post_install')
class TestMailingUi(MassMailCommon, HttpCaseWithUserDemo):
def test_01_mass_mailing_editor_tour(self):
self.start_tour("/web", 'mass_mailing_editor_tour', login="demo")
mail = self.env['mailing.mailing'].search([('subject', '=', 'Test')])[0]
# The tour created and saved an email. The edited version should be
# saved in body_arch, and its transpiled version (see convert_inline)
# for email client compatibility should be saved in body_html. This
# ensures both fields have different values (the mailing body should
# have been converted to a table in body_html).
self.assertIn('data-snippet="s_title"', mail.body_arch)
self.assertTrue(mail.body_arch.startswith('<div'))
self.assertIn('data-snippet="s_title"', mail.body_html)
self.assertTrue(mail.body_html.startswith('<table'))
@classmethod
def setUpClass(cls):
super(TestMailingUi, cls).setUpClass()
def test_02_mass_mailing_snippets_menu_tabs(self):
self.start_tour("/web", 'mass_mailing_snippets_menu_tabs', login="demo")
cls.user_marketing.write({
'group_ids': [
(4, cls.env.ref('mail.group_mail_template_editor').id),
],
})
cls.user_demo.write({
'group_ids': [
(4, cls.env.ref('mass_mailing.group_mass_mailing_campaign').id),
(4, cls.env.ref('mass_mailing.group_mass_mailing_user').id),
],
})
def test_03_mass_mailing_snippets_toolbar_mobile_hide(self):
self.start_tour("/web", 'mass_mailing_snippets_menu_toolbar_new_mailing_mobile', login="demo")
def test_04_mass_mailing_snippets_menu_hide(self):
self.start_tour("/web", 'mass_mailing_snippets_menu_toolbar', login="demo")
def test_05_mass_mailing_basic_theme_toolbar(self):
self.start_tour('/web', 'mass_mailing_basic_theme_toolbar', login="demo")
def test_06_mass_mailing_campaign_new_mailing(self):
self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('mass_mailing.group_mass_mailing_campaign').id)]})
def test_mailing_campaign_tour(self):
# self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('mass_mailing.group_mass_mailing_campaign').id)]})
campaign = self.env['utm.campaign'].create({
'name': 'Test Newsletter',
'user_id': self.env.ref("base.user_admin").id,
@ -54,7 +41,44 @@ class TestUi(HttpCaseWithUserDemo):
self.env['mailing.list'].create({
'name': 'Test Newsletter',
})
self.start_tour("/web", 'mass_mailing_campaing_new_mailing', login="demo")
self.user_marketing.write({
'group_ids': [
(4, self.env.ref('mass_mailing.group_mass_mailing_campaign').id),
],
})
self.start_tour("/odoo", 'mailing_campaign', login="user_marketing")
def test_07_mass_mailing_code_view_tour(self):
self.start_tour("/web?debug=tests", 'mass_mailing_code_view_tour', login="demo")
def test_mailing_editor_tour(self):
mailing = self.env['mailing.mailing'].search([('subject', '=', 'TestFromTour')], limit=1)
self.assertFalse(mailing)
self.start_tour("/odoo", 'mailing_editor', login="user_marketing")
# The tour created and saved a mailing. The edited version should be
# saved in body_arch, and its transpiled version (see convert_inline)
# for email client compatibility should be saved in body_html. This
# ensures both fields have different values (the mailing body should
# have been converted to a table in body_html).
mailing = self.env['mailing.mailing'].search([('subject', '=', 'TestFromTour')], limit=1)
self.assertTrue(mailing)
self.assertIn('data-snippet="s_title"', mailing.body_arch)
self.assertTrue(mailing.body_arch.startswith('<div'))
self.assertIn('data-snippet="s_title"', mailing.body_html)
self.assertTrue(mailing.body_html.startswith('<table'))
def test_mailing_editor_theme_tour(self):
self.start_tour('/odoo', 'mailing_editor_theme', login="demo")
def test_snippets_mailing_menu_tabs_tour(self):
self.start_tour("/odoo", 'snippets_mailing_menu_tabs', login="demo")
def test_snippets_mailing_menu_toolbar_tour(self):
self.start_tour("/odoo", 'snippets_mailing_menu_toolbar', login="demo")
def test_snippets_mailing_menu_toolbar_mobile_tour(self):
self.start_tour("/odoo", 'snippets_mailing_menu_toolbar_mobile', login="demo")
def test_mass_mailing_code_view_tour(self):
self.start_tour("/odoo?debug=tests", 'mass_mailing_code_view_tour', login="demo")
def test_mass_mailing_dynamic_placeholder_tour(self):
self.start_tour("/odoo", 'mass_mailing_dynamic_placeholder_tour', login="demo")