[IMP] mail, (test)_mass_mailing, test_mail_full: improve email-based mailing tests

PURPOSE

Purpose is to have more tests when sending mail mailings, notably about
canceled or failed mails or sms as well as jinja and links rendering.

SPECIFICATIONS

In this commit we improve Email Marketing mailing tests. We notably

  * add tests for void and invalid email and numbers. It allows to check they
    correctly update their trace status;
  * add tests for unsubscribe and view links embedded in mass mailing emails;
  * add tests to simulate a click on links sent through mass mailing and
    ensure click statistics are effectively updated;
  * add tests to simulate bounce emails coming back to the mail gateway;

Default content of mailings used in test is updated to ensure jinja is
correctly rendered, including links and some corner cases. This will also
helps ensuring behavior is kept when converting to QWeb.

Various docstrings are added to helpers and custom asserts.

LINKS

Task ID-2508643
Followup of odoo/odoo#68874 (improve mail tests)
Prepares Task ID-27033 (support QWeb in templates)
Prepares Task ID-2377974 (clean trace and status management in mass mailing)
COM PR odoo/odoo#69461
ENT PR odoo/enterprise#17780

X-original-commit: 7b940ad46bde30f9bf16837f7bff5a70cd88c640
This commit is contained in:
Thibault Delavallée
2021-04-20 09:18:21 +00:00
parent 9f60e5cc97
commit b96d941aaa
9 changed files with 381 additions and 107 deletions
+51 -2
View File
@@ -125,6 +125,10 @@ class MockEmail(common.BaseCase):
return self.env[target_model].search([(target_field, '=', subject)])
def gateway_reply_wrecord(self, template, record, use_in_reply_to=True):
""" Deprecated, remove in 14.4 """
return self.gateway_mail_reply_wrecord(template, record, use_in_reply_to=use_in_reply_to)
def gateway_mail_reply_wrecord(self, template, record, use_in_reply_to=True):
""" Simulate a reply through the mail gateway. Usage: giving a record,
find an email sent to him and use its message-ID to simulate a reply.
@@ -176,7 +180,11 @@ class MockEmail(common.BaseCase):
""" Find a sent email with a given list of recipients. Email should match
exactly the recipients.
:return email: a dictionary mapping values given to ``build_email``;
:param email-to: a list of emails that will be compared to email_to
of sent emails (also a list of emails);
:return email: an email which is a dictionary mapping values given to
``build_email``;
"""
for sent_email in self._mails:
if set(sent_email['email_to']) == set([email_to]):
@@ -185,6 +193,20 @@ class MockEmail(common.BaseCase):
raise AssertionError('sent mail not found for email_to %s' % (email_to))
return sent_email
def _find_mail_mail_wid(self, mail_id):
""" Find a ``mail.mail`` record based on a given ID (used notably when having
mail ID in mailing traces).
:return mail: a ``mail.mail`` record generated during the mock and matching
given ID;
"""
for mail in self._new_mails:
if mail.id == mail_id:
break
else:
raise AssertionError('mail.mail not found for ID %s' % (mail_id))
return mail
def _find_mail_mail_wpartners(self, recipients, status, mail_message=None, author=None):
""" Find a mail.mail record based on various parameters, notably a list
of recipients (partners).
@@ -271,7 +293,7 @@ class MockEmail(common.BaseCase):
:param status: mail.mail state used to filter mails. If ``sent`` this method
also check that emails have been sent trough gateway;
:param mail_message: see ``_find_mail_mail_wpartners``;
:param author: see ``_find_mail_mail_wpartners``;;
:param author: see ``_find_mail_mail_wpartners``;
:param content: if given, check it is contained within mail html body;
:param fields_values: if given, should be a dictionary of field names /
values allowing to check ``mail.mail`` additional values (subject,
@@ -329,7 +351,33 @@ class MockEmail(common.BaseCase):
for email_to in emails:
self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [email_to], **(email_values or {}))
def assertMailMailWId(self, mail_id, status,
content=None, fields_values=None):
""" Assert mail.mail records are created and maybe sent as emails. Allow
asserting their content. Records to check are the one generated when
using mock (mail.mail and outgoing emails). This method takes partners
as source of record fetch and assert.
:param mail_id: a ``mail.mail`` DB ID. See ``_find_mail_mail_wid``;
:param status: mail.mail state to check upon found mail;
:param content: if given, check it is contained within mail html body;
:param fields_values: if given, should be a dictionary of field names /
values allowing to check ``mail.mail`` additional values (subject,
reply_to, ...);
"""
found_mail = self._find_mail_mail_wid(mail_id)
self.assertTrue(bool(found_mail))
if status:
self.assertEqual(found_mail.state, status)
if content:
self.assertIn(content, found_mail.body_html)
for fname, fvalue in (fields_values or {}).items():
self.assertEqual(
found_mail[fname], fvalue,
'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname]))
def assertNoMail(self, recipients, mail_message=None, author=None):
""" Check no mail.mail and email was generated during gateway mock. """
try:
self._find_mail_mail_wpartners(recipients, False, mail_message=mail_message, author=author)
except AssertionError:
@@ -340,6 +388,7 @@ class MockEmail(common.BaseCase):
self.assertNotSentEmail()
def assertNotSentEmail(self):
""" Check no email was generated during gateway mock. """
self.assertEqual(len(self._mails), 0)
def assertSentEmail(self, author, recipients, **values):
+2
View File
@@ -193,6 +193,8 @@ class MassMailing(models.Model):
self[key] = False
if not self.ids:
return
# ensure traces are sent to db
self.flush()
self.env.cr.execute("""
SELECT
m.id as mailing_id,
+152 -11
View File
@@ -1,32 +1,78 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import random
import re
import werkzeug
from odoo.addons.link_tracker.tests.common import MockLinkTracker
from odoo.addons.mail.tests.common import MailCase, MailCommon, mail_new_test_user
from odoo import tools
class MassMailCase(MailCase, MockLinkTracker):
# ------------------------------------------------------------
# ASSERTS
# ------------------------------------------------------------
def assertMailingStatistics(self, mailing, **kwargs):
""" Helper to assert mailing statistics fields. As we have many of them
it helps lessening test asserts. """
if not kwargs.get('expected'):
kwargs['expected'] = len(mailing.mailing_trace_ids)
if not kwargs.get('delivered'):
kwargs['delivered'] = len(mailing.mailing_trace_ids)
for fname in ['scheduled', 'expected', 'sent', 'delivered',
'opened', 'replied', 'clicked',
'ignored', 'failed', 'bounced']:
self.assertEqual(
mailing[fname], kwargs.get(fname, 0),
'Mailing %s statistics failed: got %s instead of %s' % (fname, mailing[fname], kwargs.get(fname, 0))
)
def assertMailTraces(self, recipients_info, mailing, records,
check_mail=True, author=None):
""" Check content of traces.
check_mail=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
holds expected information. Links content may be checked, notably to
assert shortening or unsubscribe links. Mail.mail records may optionally
be checked.
:param recipients_info: list[{
# TRACE
'partner': res.partner record (may be empty),
'email': email used when sending email (may be empty, computed based on partner),
'state': outgoing / sent / open / reply / error / cancel (sent by default),
'state': outgoing / sent / ignored / bounced / exception / opened (sent by default),
'record: linked record,
'content': UDPATE ME
'failure_type': optional: UPDATE ME
# MAIL.MAIL
'content': optional content that should be present in mail.mail body_html;
'failure_type': optional failure reason;
}, { ... }]
:param mailing: a mailing.mailing record from which traces have been
generated;
: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;
: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;
:param author: author of sent mail.mail;
"""
# map trace state to email state
state_mapping = {
'sent': 'sent',
'replied': 'sent', # replied imply something has been sent
'opened': 'sent', # opened implies something has been sent
'replied': 'sent', # replied implies something has been sent
'ignored': 'cancel',
'exception': 'exception',
'canceled': 'canceled',
'canceled': 'cancel',
'bounced': 'cancel',
}
traces = self.env['mailing.trace'].search([
@@ -39,11 +85,13 @@ class MassMailCase(MailCase, MockLinkTracker):
self.assertEqual(set(s.res_id for s in traces), set(records.ids))
# check each traces
for recipient_info in recipients_info:
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):
partner = recipient_info.get('partner', self.env['res.partner'])
email = recipient_info.get('email')
state = recipient_info.get('state', 'sent')
record = recipient_info.get('record')
record = record or recipient_info.get('record')
content = recipient_info.get('content')
if email is None and partner:
email = partner.email_normalized
@@ -55,6 +103,7 @@ class MassMailCase(MailCase, MockLinkTracker):
len(recipient_trace) == 1,
'MailTrace: email %s (recipient %s, state: %s, record: %s): found %s records (1 expected)' % (email, partner, state, record, len(recipient_trace))
)
self.assertTrue(bool(recipient_trace.mail_mail_id_int))
if check_mail:
if author is None:
@@ -64,11 +113,103 @@ class MassMailCase(MailCase, MockLinkTracker):
if 'failure_type' in recipient_info:
fields_values['failure_type'] = recipient_info['failure_type']
# specific for partner: email_formatted is used
if partner:
self.assertMailMail(partner, state_mapping[state], author=author, content=content, fields_values=fields_values)
if state == 'sent' and sent_unlink:
self.assertSentEmail(author, [partner])
else:
self.assertMailMail(partner, state_mapping[state], author=author, content=content, fields_values=fields_values)
# specific if email is False -> could have troubles finding it if several falsy traces
elif not email and state in ('ignored', 'canceled', 'bounced'):
self.assertMailMailWId(recipient_trace.mail_mail_id_int, state_mapping[state], content=content, fields_values=fields_values)
else:
self.assertMailMailWEmails([email], state_mapping[state], author=author, content=content, fields_values=fields_values)
if link_info:
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}
if add_link_params:
link_params.update(**add_link_params)
self.assertLinkShortenedHtml(
trace_mail.body_html,
(anchor_id, url, is_shortened),
link_params=link_params,
)
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def gateway_mail_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
to bounce;
:param record: record which should bounce;
:param bounce_base_values: optional values given to routing;
"""
trace = mailing.mailing_trace_ids.filtered(lambda t: t.model == record._name and t.res_id == record.id)
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'),
'bounced_partner': self.env['res.partner'].sudo(),
'bounced_message': self.env['mail.message'].sudo()
}
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],
})
self.env['mail.thread']._routing_handle_bounce(False, parsed_bounce_values)
def gateway_mail_click(self, mailing, record, click_label):
""" Simulate a click on a sent email. """
trace = mailing.mailing_trace_ids.filtered(lambda t: t.model == record._name and t.res_id == record.id)
email = self._find_sent_mail_wemail(trace.email)
self.assertTrue(bool(email))
for (_url_href, link_url, _dummy, label) in re.findall(tools.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('/')
code, trace_id = path_items[2], int(path_items[4])
self.assertEqual(trace.id, trace_id)
self.env['link.tracker.click'].sudo().add_click(
code,
ip='100.200.300.%3f' % random.random(),
country_code='BE',
mailing_trace_id=trace.id
)
break
else:
raise AssertionError('url %s not found in mailing %s for record %s' % (click_label, mailing, record))
@classmethod
def _create_bounce_trace(cls, mailing, record, dt=None):
if 'email_normalized' in record:
trace_email = record.email_normalized
elif 'email_from' in record:
trace_email = record.email_from
else:
trace_email = record.email
if dt is None:
dt = datetime.datetime.now() - datetime.timedelta(days=1)
randomized = random.random()
trace = cls.env['mailing.trace'].create({
'mass_mailing_id': mailing.id,
'model': record._name,
'res_id': record.id,
'bounced': dt,
# TDE FIXME: improve this with a mail-enabled heuristics
'email': trace_email,
'message_id': '<%5f@gilbert.boitempomils>' % randomized,
})
return trace
class MassMailCommon(MailCommon, MassMailCase):
@@ -242,7 +242,7 @@ class TestMassMailFeatures(MassMailCommon, CronMixinCase):
self.assertMailTraces(
[{'partner': partner_a},
{'partner': partner_b, 'state': 'ignored'}],
mailing, partner_a | partner_b, check_mail=True
mailing, partner_a + partner_b, check_mail=True
)
@users('user_marketing')
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from odoo.addons.test_mail_full.tests.common import TestMailFullCommon
from odoo.tests.common import users
from odoo.tools import config, mute_logger
from odoo.tools import mute_logger
from odoo.tests import tagged
@@ -17,49 +19,55 @@ class TestMassMailing(TestMailFullCommon):
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_w_blacklist_opt_out(self):
# TDE FIXME: better URLs check for unsubscribe / view (res_id + email + correct parse of url)
mailing = self.mailing_bl.with_user(self.env.user)
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
jinja_html = '''
<div>
<p>Hello <span class="text-muted">${object.name}</span></p>
<p>
Here are your personal links
<a href="http://www.example.com">External link</a>
<a href="/event/dummy-event-0">Internal link</a>
<a role="button" href="/unsubscribe_from_list" class="btn btn-link">Unsubscribe link</a>
<a href="/view">
View link
</a>
</p>
</div>'''
mailing.write({
'preview': 'Hi ${object.name} :)',
'body_html': jinja_html,
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
})
recipients = self._create_test_blacklist_records(model='mailing.test.optout', count=10)
mailing.write({'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id})
recipients = self._create_mailing_test_records(model='mailing.test.optout', count=10)
# optout records 1 and 2
(recipients[1] | recipients[2]).write({'opt_out': True})
# blacklist records 3 and 4
self.env['mail.blacklist'].create({'email': recipients[3].email_normalized})
self.env['mail.blacklist'].create({'email': recipients[4].email_normalized})
# have a duplicate email for 9
recipient_dup_1 = recipients[9].copy()
# have a void mail
recipient_void_1 = self.env['mailing.test.optout'].create({'name': 'TestRecord_void_1'})
# have a falsy mail
recipient_falsy_1 = self.env['mailing.test.optout'].create({
'name': 'TestRecord_falsy_1',
'email_from': 'falsymail'
})
recipients_all = recipients + recipient_dup_1 + recipient_void_1 + recipient_falsy_1
mailing.write({'mailing_domain': [('id', 'in', recipients.ids)]})
mailing.write({'mailing_domain': [('id', 'in', recipients_all.ids)]})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._process_mass_mailing_queue()
for recipient in recipients:
for recipient in recipients_all:
recipient_info = {
'email': recipient.email_normalized,
'content': 'Hello <span class="text-muted">%s</span' % recipient.name}
'content': 'Hello %s' % recipient.name}
# opt-out: ignored (cancel mail)
if recipient in recipients[1] | recipients[2]:
recipient_info['state'] = 'ignored'
# blacklisted: ignored (cancel mail)
elif recipient in recipients[3] | recipients[4]:
recipient_info['state'] = 'ignored'
# duplicates: ignored (cancel mail)
elif recipient == recipient_dup_1:
recipient_info['state'] = 'ignored'
# void: ignored (cancel mail)
elif recipient == recipient_void_1:
recipient_info['state'] = 'ignored'
# falsy: ignored (cancel mail)
elif recipient == recipient_falsy_1:
# TDE FIXME: currently setting False as email
# recipient_info['state'] = 'ignored'
# recipient_info['email'] = recipient.email_from # normalized is False but email should be falsymail
recipient_info['state'] = 'ignored'
recipient_info['email'] = False
else:
email = self._find_sent_mail_wemail(recipient.email_normalized)
# preview correctly integrated rendered jinja
@@ -68,13 +76,47 @@ class TestMassMailing(TestMailFullCommon):
email['body'])
# rendered unsubscribe
self.assertIn(
'http://localhost:%s/mail/mailing/%s/unsubscribe' % (config['http_port'], mailing.id),
'%s/mail/mailing/%s/unsubscribe' % (mailing.get_base_url(), mailing.id),
email['body'])
unsubscribe_href = self._get_href_from_anchor_id(email['body'], "url6")
unsubscribe_url = werkzeug.urls.url_parse(unsubscribe_href)
unsubscribe_params = unsubscribe_url.decode_query().to_dict(flat=True)
self.assertEqual(int(unsubscribe_params['res_id']), recipient.id)
self.assertEqual(unsubscribe_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._unsubscribe_token(unsubscribe_params['res_id'], (unsubscribe_params['email'])),
unsubscribe_params['token']
)
# rendered view
self.assertIn(
'http://localhost:%s/mailing/%s/view' % (config['http_port'], mailing.id),
'%s/mailing/%s/view' % (mailing.get_base_url(), mailing.id),
email['body'])
view_href = self._get_href_from_anchor_id(email['body'], "url6")
view_url = werkzeug.urls.url_parse(view_href)
view_params = view_url.decode_query().to_dict(flat=True)
self.assertEqual(int(view_params['res_id']), recipient.id)
self.assertEqual(view_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._unsubscribe_token(view_params['res_id'], (view_params['email'])),
view_params['token']
)
self.assertMailTraces([recipient_info], mailing, recipient, check_mail=True, author=self.env.user.partner_id)
self.assertMailTraces(
[recipient_info], mailing, recipient,
mail_links_info=[[
('url0', 'https://www.odoo.tz/my/%s' % recipient.name, True, {}),
('url1', 'https://www.odoo.be', True, {}),
('url2', 'https://www.odoo.com', True, {}),
('url3', 'https://www.odoo.eu', True, {}),
('url4', 'https://www.example.com/foo/bar?baz=qux', True, {'baz': 'qux'}),
('url5', '%s/event/dummy-event-0' % mailing.get_base_url(), True, {}),
# view is not shortened and parsed at sending
('url6', '%s/view' % mailing.get_base_url(), False, {}),
('url7', 'mailto:test@odoo.com', False, {}),
# unsubscribe is not shortened and parsed at sending
('url8', '%s/unsubscribe_from_list' % mailing.get_base_url(), False, {}),
]],
check_mail=True,)
self.assertEqual(mailing.ignored, 4)
# sent: 13, 2 bl, 2 opt-out, 3 invalid -> 6 remaining
self.assertMailingStatistics(mailing, expected=6, delivered=6, sent=6, ignored=7)
+31 -29
View File
@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import random
from odoo.addons.mass_mailing.tests.common import MassMailCommon
from odoo.addons.test_mail.tests.common import TestMailCommon
@@ -19,40 +16,45 @@ class TestMassMailCommon(MassMailCommon, TestMailCommon):
cls.mailing_bl = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({
'name': 'SourceName',
'subject': 'MailingSubject',
'body_html': '<p>Hello ${object.name}</p>',
'preview': 'Hi ${object.name} :)',
'body_html': """<div><p>Hello ${object.name}</p>,
% set url = "www.odoo.com"
% set httpurl = "https://www.odoo.eu"
<span>Website0: <a id="url0" href="https://www.odoo.tz/my/${object.name}">https://www.odoo.tz/my/${object.name}</a></span>
<span>Website1: <a id="url1" href="https://www.odoo.be">https://www.odoo.be</a></span>
<span>Website2: <a id="url2" href="https://${url}">https://${url}</a></span>
<span>Website3: <a id="url3" href="${httpurl}">${httpurl}</a></span>
<span>External1: <a id="url4" href="https://www.example.com/foo/bar?baz=qux">Youpie</a></span>
<span>Internal1: <a id="url5" href="/event/dummy-event-0">Internal link</a></span>
<span>Internal2: <a id="url6" href="/view"/>View link</a></span>
<span>Email: <a id="url7" href="mailto:test@odoo.com">test@odoo.com</a></span>
<p>Stop spam ? <a id="url8" role="button" href="/unsubscribe_from_list">Ok</a></p>
</div>""",
'mailing_type': 'mail',
'mailing_model_id': cls.env['ir.model']._get('mailing.test.blacklist').id,
})
@classmethod
def _create_test_blacklist_records(cls, model='mailing.test.blacklist', count=1):
""" Deprecated, remove in 14.4 """
return cls.__create_mailing_test_records(model=model, count=count)
@classmethod
def _create_mailing_test_records(cls, model='mailing.test.blacklist', partners=None, count=1):
""" Helper to create data. Currently simple, to be improved. """
Model = cls.env[model]
email_field = 'email' if 'email' in Model else 'email_from'
partner_field = 'customer_id' if 'customer_id' in Model else 'partner_id'
records = cls.env[model].create([{
'name': 'TestRecord_%02d' % x,
email_field: '"TestCustomer %02d" <test.record.%02d@test.example.com>' % (x, x),
} for x in range(0, count)])
return records
vals_list = []
for x in range(0, count):
vals = {
'name': 'TestRecord_%02d' % x,
email_field: '"TestCustomer %02d" <test.record.%02d@test.example.com>' % (x, x),
}
if partners:
vals[partner_field] = partners[x % len(partners)]
@classmethod
def _create_bounce_trace(cls, record, dt=None):
if dt is None:
dt = datetime.datetime.now() - datetime.timedelta(days=1)
randomized = random.random()
if 'email_normalized' in record:
trace_email = record.email_normalized
elif 'email_from' in record:
trace_email = record.email_from
else:
trace_email = record.email
trace = cls.env['mailing.trace'].create({
'model': record._name,
'res_id': record.id,
'bounced': dt,
# TDE FIXME: improve this with a mail-enabled heuristics
'email': trace_email,
'message_id': '<%5f@gilbert.boitempomils>' % randomized,
})
return trace
vals_list.append(vals)
return cls.env[model].create(vals_list)
@@ -11,24 +11,17 @@ class TestAutoBlacklist(common.TestMassMailCommon):
@users('user_marketing')
def test_mailing_bounce_w_auto_bl(self):
mailing = self.mailing_bl.with_user(self.env.user)
base_parsed_values = {
'email_from': 'toto@yaourth.com', 'to': 'tata@yaourth.com', 'message_id': '<123.321@yaourth.com>',
'bounced_partner': self.env['res.partner'].sudo(), 'bounced_message': self.env['mail.message'].sudo()
}
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
target = self._create_mailing_test_records()[0]
mailing.write({'mailing_domain': [('id', 'in', target.ids)]})
target = self._create_test_blacklist_records()[0]
# create bounced history of 4 statistics
for idx in range(4):
trace = self._create_bounce_trace(target, dt=datetime.datetime.now() - datetime.timedelta(weeks=idx+2))
base_parsed_values.update({
'bounced_email': target.email_normalized,
'bounced_msg_id': [trace.message_id],
})
self.env['mail.thread']._routing_handle_bounce(False, base_parsed_values)
new_mailing = mailing.copy()
self._create_bounce_trace(new_mailing, target, dt=datetime.datetime.now() - datetime.timedelta(weeks=idx+2))
self.gateway_mail_bounce(new_mailing, target)
# mass mail record: ok, not blacklisted yet
mailing.write({'mailing_domain': [('id', 'in', target.ids)]})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._process_mass_mailing_queue()
@@ -40,12 +33,7 @@ class TestAutoBlacklist(common.TestMassMailCommon):
)
# call bounced
trace = self._create_bounce_trace(target, dt=datetime.datetime.now())
base_parsed_values.update({
'bounced_email': target.email_normalized,
'bounced_msg_id': [trace.message_id],
})
self.env['mail.thread']._routing_handle_bounce(False, base_parsed_values)
self.gateway_mail_bounce(mailing, target)
# check blacklist
blacklist_record = self.env['mail.blacklist'].sudo().search([('email', '=', target.email_normalized)])
@@ -45,7 +45,7 @@ class TestMailingInternals(TestMassMailCommon):
# Test if bad jinja in the subject raises an error
mailing.write({'subject': 'Subject ${object.name_id.id}'})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
mailing_test.send_mail_test()
# Test if bad jinja in the body raises an error
mailing.write({
@@ -53,15 +53,15 @@ class TestMailingInternals(TestMassMailCommon):
'body_html': '<p>Hello ${object.name_id.id}</p>',
})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
mailing_test.send_mail_test()
# Test if bad jinja in the preview raises an error
mailing.write({
'body_html': '<p>Hello ${object.name}</p>',
'preview': 'Preview ${object.name_id.id}',
})
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
mailing_test.send_mail_test()
def test_mailing_trace_update(self):
customers = self.env['res.partner']
@@ -85,8 +85,8 @@ class TestMailingInternals(TestMassMailCommon):
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.gateway_reply_wrecord(MAIL_TEMPLATE, customers[0], use_in_reply_to=True)
self.gateway_reply_wrecord(MAIL_TEMPLATE, customers[1], use_in_reply_to=False)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, customers[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, customers[1], use_in_reply_to=False)
# customer2 looses headers
mail_mail = self._find_mail_mail_wrecord(customers[2])
@@ -152,8 +152,8 @@ class TestMailingInternals(TestMassMailCommon):
self.assertEqual(len(traces), 3)
# simulate response to mailing
self.gateway_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[0], use_in_reply_to=True)
self.gateway_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[1], use_in_reply_to=False)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, self.mailing_list_1.contact_ids[1], use_in_reply_to=False)
mailing_test_utms = self.env['mailing.test.utm'].search([('name', '=', 'Re: %s' % subject)])
self.assertEqual(len(mailing_test_utms), 2)
@@ -2,21 +2,71 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger
@tagged('mass_mailing')
class TestMassMailing(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestMassMailing, cls).setUpClass()
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_gateway_update(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(model='mailing.test.optout', count=5)
self.assertEqual(len(recipients), 5)
mailing.write({
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout'),
'mailing_domain': [('id', 'in', recipients.ids)]
})
mailing.action_put_in_queue()
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing._process_mass_mailing_queue()
self.assertMailTraces(
[{'email': record.email_normalized}
for record in recipients],
mailing, recipients,
mail_links_info=[[
('url0', 'https://www.odoo.tz/my/%s' % record.name, True, {}),
('url1', 'https://www.odoo.be', True, {}),
('url2', 'https://www.odoo.com', True, {}),
('url3', 'https://www.odoo.eu', True, {}),
('url4', 'https://www.example.com/foo/bar?baz=qux', True, {'baz': 'qux'}),
('url5', '%s/event/dummy-event-0' % mailing.get_base_url(), True, {}),
# view is not shortened and parsed at sending
('url6', '%s/view' % mailing.get_base_url(), False, {}),
('url7', 'mailto:test@odoo.com', False, {}),
# unsubscribe is not shortened and parsed at sending
('url8', '%s/unsubscribe_from_list' % mailing.get_base_url(), False, {}),
] for record in recipients],
check_mail=True
)
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5)
# simulate a click
self.gateway_mail_click(mailing, recipients[0], 'https://www.odoo.be')
mailing.invalidate_cache()
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5, opened=1, clicked=1)
# simulate a bounce
self.assertEqual(recipients[1].message_bounce, 0)
self.gateway_mail_bounce(mailing, recipients[1])
mailing.invalidate_cache()
self.assertMailingStatistics(mailing, expected=5, delivered=4, sent=5, opened=1, clicked=1, bounced=1)
self.assertEqual(recipients[1].message_bounce, 1)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_w_blacklist(self):
mailing = self.mailing_bl.with_user(self.env.user)
recipients = self._create_test_blacklist_records(count=5)
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(count=5)
# blacklist records 3 and 4
self.env['mail.blacklist'].create({'email': recipients[3].email_normalized})
@@ -40,8 +90,8 @@ class TestMassMailing(TestMassMailCommon):
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_w_opt_out(self):
mailing = self.mailing_bl.with_user(self.env.user)
recipients = self._create_test_blacklist_records(model='mailing.test.optout', count=5)
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
recipients = self._create_mailing_test_records(model='mailing.test.optout', count=5)
# optout records 0 and 1
(recipients[0] | recipients[1]).write({'opt_out': True})
@@ -114,13 +164,13 @@ class TestMassMailing(TestMassMailCommon):
mailing._process_mass_mailing_queue()
self.assertMailTraces(
[{'email': 'test@test.example.com', 'state': 'ignored'},
{'email': 'test@test.example.com', 'state': 'sent'},
[{'email': 'test@test.example.com', 'state': 'sent'},
{'email': 'test@test.example.com', 'state': 'ignored'},
{'email': 'test3@test.example.com'},
{'email': 'test4@test.example.com'},
{'email': 'test5@test.example.com', 'state': 'ignored'}],
mailing,
mailing_contact_1 | mailing_contact_2 | mailing_contact_3 | mailing_contact_4 | mailing_contact_5,
mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5,
check_mail=True
)
self.assertEqual(mailing.ignored, 2)