summaryrefslogtreecommitdiff
path: root/addons/mass_mailing/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mass_mailing/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mass_mailing/tests')
-rw-r--r--addons/mass_mailing/tests/__init__.py7
-rw-r--r--addons/mass_mailing/tests/common.py249
-rw-r--r--addons/mass_mailing/tests/test_mailing_controllers.py22
-rw-r--r--addons/mass_mailing/tests/test_mailing_internals.py332
-rw-r--r--addons/mass_mailing/tests/test_mailing_list.py112
5 files changed, 722 insertions, 0 deletions
diff --git a/addons/mass_mailing/tests/__init__.py b/addons/mass_mailing/tests/__init__.py
new file mode 100644
index 00000000..ddc9e161
--- /dev/null
+++ b/addons/mass_mailing/tests/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import common
+from . import test_mailing_internals
+from . import test_mailing_list
+from . import test_mailing_controllers
diff --git a/addons/mass_mailing/tests/common.py b/addons/mass_mailing/tests/common.py
new file mode 100644
index 00000000..1baf6c7d
--- /dev/null
+++ b/addons/mass_mailing/tests/common.py
@@ -0,0 +1,249 @@
+# -*- 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, 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 / ignored / bounced / exception / opened (sent by default),
+ 'record: linked record,
+ # 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',
+ 'opened': 'sent', # opened implies something has been sent
+ 'replied': 'sent', # replied implies something has been sent
+ 'ignored': 'cancel',
+ 'exception': 'exception',
+ 'canceled': 'cancel',
+ 'bounced': 'cancel',
+ }
+
+ traces = self.env['mailing.trace'].search([
+ ('mass_mailing_id', 'in', mailing.ids),
+ ('res_id', 'in', records.ids)
+ ])
+
+ # ensure trace coherency
+ self.assertTrue(all(s.model == records._name for s in traces))
+ self.assertEqual(set(s.res_id for s in traces), set(records.ids))
+
+ # check each traces
+ 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 = record or recipient_info.get('record')
+ content = recipient_info.get('content')
+ if email is None and partner:
+ email = partner.email_normalized
+
+ recipient_trace = traces.filtered(
+ lambda t: t.email == email and t.state == state and (t.res_id == record.id if record else True)
+ )
+ self.assertTrue(
+ 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:
+ author = self.env.user.partner_id
+
+ fields_values = {'mailing_id': mailing}
+ if 'failure_type' in recipient_info:
+ fields_values['failure_type'] = recipient_info['failure_type']
+
+ # specific for partner: email_formatted is used
+ if partner:
+ 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):
+
+ @classmethod
+ def setUpClass(cls):
+ super(MassMailCommon, cls).setUpClass()
+
+ cls.user_marketing = mail_new_test_user(
+ cls.env, login='user_marketing',
+ groups='base.group_user,base.group_partner_manager,mass_mailing.group_mass_mailing_user',
+ name='Martial Marketing', signature='--\nMartial')
+
+ cls.email_reply_to = 'MyCompany SomehowAlias <test.alias@test.mycompany.com>'
+
+ cls.env['base'].flush()
+
+ @classmethod
+ 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'}),
+ ]
+ })
diff --git a/addons/mass_mailing/tests/test_mailing_controllers.py b/addons/mass_mailing/tests/test_mailing_controllers.py
new file mode 100644
index 00000000..310664c4
--- /dev/null
+++ b/addons/mass_mailing/tests/test_mailing_controllers.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import werkzeug
+
+from odoo.addons.mass_mailing.tests.common import MassMailCommon
+from odoo.tests.common import HttpCase
+
+
+class TestMassMailingControllers(MassMailCommon, HttpCase):
+
+ def test_tracking_url_token(self):
+ mail_mail = self.env['mail.mail'].create({})
+
+ response = self.url_open(mail_mail._get_tracking_url())
+ self.assertEqual(response.status_code, 200)
+
+ base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+ url = werkzeug.urls.url_join(base_url, 'mail/track/%s/fake_token/blank.gif' % mail_mail.id)
+
+ response = self.url_open(url)
+ self.assertEqual(response.status_code, 400)
diff --git a/addons/mass_mailing/tests/test_mailing_internals.py b/addons/mass_mailing/tests/test_mailing_internals.py
new file mode 100644
index 00000000..5fb3771a
--- /dev/null
+++ b/addons/mass_mailing/tests/test_mailing_internals.py
@@ -0,0 +1,332 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from ast import literal_eval
+
+from odoo.addons.mass_mailing.tests.common import MassMailCommon
+from odoo.tests.common import users, Form
+from odoo.tools import formataddr, mute_logger
+
+class TestMassMailValues(MassMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMassMailValues, cls).setUpClass()
+ cls._create_mailing_list()
+
+ @users('user_marketing')
+ def test_mailing_body_responsive(self):
+ """ Testing mail mailing responsive mail body """
+ recipient = self.env['res.partner'].create({
+ 'name': 'Mass Mail Partner',
+ 'email': 'Customer <test.customer@example.com>',
+ })
+ mailing = self.env['mailing.mailing'].create({
+ 'name': 'Test',
+ 'subject': 'Test',
+ 'state': 'draft',
+ 'mailing_model_id': self.env['ir.model']._get('res.partner').id,
+ })
+
+ 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,
+ }).create({
+ 'subject': 'Mass Mail Responsive',
+ 'body': 'I am Responsive body',
+ 'mass_mailing_id': mailing.id
+ })
+
+ mail_values = composer.get_mail_values([recipient.id])
+ body_html = str(mail_values[recipient.id]['body_html'])
+
+ self.assertIn('<!DOCTYPE html>', body_html)
+ self.assertIn('<head>', body_html)
+ self.assertIn('viewport', body_html)
+ self.assertIn('@media', body_html)
+ self.assertIn('I am Responsive body', body_html)
+
+ @users('user_marketing')
+ def test_mailing_computed_fields(self):
+ # Create on res.partner, with default values for computed fields
+ mailing = self.env['mailing.mailing'].create({
+ 'name': 'TestMailing',
+ 'subject': 'Test',
+ 'mailing_type': 'mail',
+ 'body_html': '<p>Hello ${object.name}</p>',
+ 'mailing_model_id': self.env['ir.model']._get('res.partner').id,
+ })
+ self.assertEqual(mailing.user_id, self.user_marketing)
+ self.assertEqual(mailing.medium_id, self.env.ref('utm.utm_medium_email'))
+ self.assertEqual(mailing.mailing_model_name, 'res.partner')
+ self.assertEqual(mailing.mailing_model_real, 'res.partner')
+ self.assertEqual(mailing.reply_to_mode, 'email')
+ 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)])
+ # update domain
+ mailing.write({
+ 'mailing_domain': [('email', 'ilike', 'test.example.com')]
+ })
+ self.assertEqual(literal_eval(mailing.mailing_domain), [('email', 'ilike', 'test.example.com')])
+
+ # reset mailing model -> reset domain; set reply_to -> keep it
+ mailing.write({
+ 'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
+ 'reply_to': self.email_reply_to,
+ })
+ self.assertEqual(mailing.mailing_model_name, 'mailing.list')
+ self.assertEqual(mailing.mailing_model_real, 'mailing.contact')
+ self.assertEqual(mailing.reply_to_mode, 'email')
+ self.assertEqual(mailing.reply_to, self.email_reply_to)
+ # default for mailing list: depends upon contact_list_ids
+ self.assertEqual(literal_eval(mailing.mailing_domain), [])
+ mailing.write({
+ 'contact_list_ids': [(4, self.mailing_list_1.id), (4, self.mailing_list_2.id)]
+ })
+ self.assertEqual(literal_eval(mailing.mailing_domain), [('list_ids', 'in', (self.mailing_list_1 | self.mailing_list_2).ids)])
+
+ # reset mailing model -> reset domain and reply to mode
+ mailing.write({
+ 'mailing_model_id': self.env['ir.model']._get('mail.channel').id,
+ })
+ self.assertEqual(mailing.mailing_model_name, 'mail.channel')
+ self.assertEqual(mailing.mailing_model_real, 'mail.channel')
+ self.assertEqual(mailing.reply_to_mode, 'thread')
+ self.assertFalse(mailing.reply_to)
+
+ @users('user_marketing')
+ def test_mailing_computed_fields_default(self):
+ mailing = self.env['mailing.mailing'].with_context(
+ default_mailing_domain=repr([('email', 'ilike', 'test.example.com')])
+ ).create({
+ 'name': 'TestMailing',
+ 'subject': 'Test',
+ 'mailing_type': 'mail',
+ 'body_html': '<p>Hello ${object.name}</p>',
+ 'mailing_model_id': self.env['ir.model']._get('res.partner').id,
+ })
+ self.assertEqual(literal_eval(mailing.mailing_domain), [('email', 'ilike', 'test.example.com')])
+
+ @users('user_marketing')
+ def test_mailing_computed_fields_form(self):
+ mailing_form = Form(self.env['mailing.mailing'].with_context(
+ default_mailing_domain="[('email', 'ilike', 'test.example.com')]",
+ default_mailing_model_id=self.env['ir.model']._get('res.partner').id,
+ ))
+ self.assertEqual(
+ literal_eval(mailing_form.mailing_domain),
+ [('email', 'ilike', 'test.example.com')],
+ )
+ self.assertEqual(mailing_form.mailing_model_real, 'res.partner')
+
+
+class TestMassMailFeatures(MassMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMassMailFeatures, cls).setUpClass()
+ cls._create_mailing_list()
+
+ @users('user_marketing')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_channel_blacklisted_recipients(self):
+ """ Posting a message on a channel should send one email to all recipients, except the blacklisted ones """
+ def _join_channel(channel, partners):
+ for partner in partners:
+ channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner.id})]})
+ channel.invalidate_cache()
+
+ test_channel = self.env['mail.channel'].create({
+ 'name': 'Test',
+ 'description': 'Description',
+ 'alias_name': 'test',
+ 'public': 'public',
+ 'email_send': True,
+ })
+ test_partner = self.env['res.partner'].create({
+ 'name': 'Test Partner',
+ 'email': 'test@example.com',
+ })
+
+ blacklisted_partner = self.env['res.partner'].create({
+ 'name': 'Blacklisted Partner',
+ 'email': 'test@black.list',
+ })
+
+ # Set Blacklist
+ self.env['mail.blacklist'].create({
+ 'email': 'test@black.list',
+ })
+
+ _join_channel(test_channel, test_partner)
+ _join_channel(test_channel, blacklisted_partner)
+ with self.mock_mail_gateway():
+ test_channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment')
+
+ self.assertEqual(len(self._mails), 1, 'Number of mail incorrect. Should be equal to 1.')
+ for email in self._mails:
+ self.assertEqual(
+ set(email['email_to']),
+ set([formataddr((test_partner.name, test_partner.email))]),
+ 'email_to incorrect. Should be equal to "%s"' % (
+ formataddr((test_partner.name, test_partner.email))))
+
+ @users('user_marketing')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mailing_deletion(self):
+ """ Test deletion in various use case, depending on reply-to """
+ # 1- Keep archives and reply-to set to 'answer = new thread'
+ mailing = self.env['mailing.mailing'].create({
+ 'name': 'TestSource',
+ 'subject': 'TestDeletion',
+ 'body_html': "<div>Hello {object.name}</div>",
+ 'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
+ 'contact_list_ids': [(6, 0, self.mailing_list_1.ids)],
+ 'keep_archives': True,
+ 'reply_to_mode': 'email',
+ 'reply_to': self.email_reply_to,
+ })
+ self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
+
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ mailing.action_send_mail()
+
+ self.assertEqual(len(self._mails), 3)
+ self.assertEqual(len(self._new_mails.exists()), 3)
+ self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
+
+ # 2- Keep archives and reply-to set to 'answer = update thread'
+ self.mailing_list_1.contact_ids.message_ids.unlink()
+ mailing = mailing.copy()
+ mailing.write({
+ 'reply_to_mode': 'thread',
+ })
+ self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
+
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ mailing.action_send_mail()
+
+ self.assertEqual(len(self._mails), 3)
+ self.assertEqual(len(self._new_mails.exists()), 3)
+ self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
+
+ # 3- Remove archives and reply-to set to 'answer = new thread'
+ self.mailing_list_1.contact_ids.message_ids.unlink()
+ mailing = mailing.copy()
+ mailing.write({
+ 'keep_archives': False,
+ 'reply_to_mode': 'email',
+ 'reply_to': self.email_reply_to,
+ })
+ self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
+
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ mailing.action_send_mail()
+
+ self.assertEqual(len(self._mails), 3)
+ self.assertEqual(len(self._new_mails.exists()), 0)
+ self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
+
+ # 4- Remove archives and reply-to set to 'answer = update thread'
+ # Imply keeping mail.message for gateway reply)
+ self.mailing_list_1.contact_ids.message_ids.unlink()
+ mailing = mailing.copy()
+ mailing.write({
+ 'keep_archives': False,
+ 'reply_to_mode': 'thread',
+ })
+ self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
+
+ with self.mock_mail_gateway(mail_unlink_sent=True):
+ mailing.action_send_mail()
+
+ self.assertEqual(len(self._mails), 3)
+ self.assertEqual(len(self._new_mails.exists()), 0)
+ self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
+
+ @users('user_marketing')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mailing_on_res_partner(self):
+ """ Test mailing on res.partner model: ensure default recipients are
+ correctly computed """
+ partner_a = self.env['res.partner'].create({
+ 'name': 'test email 1',
+ 'email': 'test1@example.com',
+ })
+ partner_b = self.env['res.partner'].create({
+ 'name': 'test email 2',
+ 'email': 'test2@example.com',
+ })
+ self.env['mail.blacklist'].create({'email': 'Test2@example.com',})
+
+ mailing = self.env['mailing.mailing'].create({
+ 'name': 'One',
+ 'subject': 'One',
+ 'mailing_model_id': self.env['ir.model']._get('res.partner').id,
+ 'mailing_domain': [('id', 'in', (partner_a | partner_b).ids)],
+ 'body_html': 'This is mass mail marketing demo'
+ })
+ mailing.action_put_in_queue()
+ with self.mock_mail_gateway(mail_unlink_sent=False):
+ mailing._process_mass_mailing_queue()
+
+ self.assertMailTraces(
+ [{'partner': partner_a},
+ {'partner': partner_b, 'state': 'ignored'}],
+ mailing, partner_a + partner_b, check_mail=True
+ )
+
+ @users('user_marketing')
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mailing_shortener(self):
+ mailing = self.env['mailing.mailing'].create({
+ 'name': 'TestSource',
+ 'subject': 'TestShortener',
+ 'body_html': """<div>
+Hi,
+% set url = "www.odoo.com"
+% set httpurl = "https://www.odoo.eu"
+Website0: <a id="url0" href="https://www.odoo.tz/my/${object.name}">https://www.odoo.tz/my/${object.name}</a>
+Website1: <a id="url1" href="https://www.odoo.be">https://www.odoo.be</a>
+Website2: <a id="url2" href="https://${url}">https://${url}</a>
+Website3: <a id="url3" href="${httpurl}">${httpurl}</a>
+External1: <a id="url4" href="https://www.example.com/foo/bar?baz=qux">Youpie</a>
+Email: <a id="url5" href="mailto:test@odoo.com">test@odoo.com</a></div>""",
+ 'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
+ 'reply_to_mode': 'email',
+ 'reply_to': self.email_reply_to,
+ 'contact_list_ids': [(6, 0, self.mailing_list_1.ids)],
+ 'keep_archives': True,
+ })
+
+ mailing.action_put_in_queue()
+
+ with self.mock_mail_gateway(mail_unlink_sent=False):
+ mailing._process_mass_mailing_queue()
+
+ self.assertMailTraces(
+ [{'email': 'fleurus@example.com'},
+ {'email': 'gorramts@example.com'},
+ {'email': 'ybrant@example.com'}],
+ mailing, self.mailing_list_1.contact_ids, check_mail=True
+ )
+
+ for contact in self.mailing_list_1.contact_ids:
+ new_mail = self._find_mail_mail_wrecord(contact)
+ for link_info in [('url0', 'https://www.odoo.tz/my/%s' % contact.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),
+ ('url5', 'mailto:test@odoo.com', False)]:
+ # TDE FIXME: why going to mail message id ? mail.body_html seems to fail, check
+ link_params = {'utm_medium': 'Email', 'utm_source': mailing.name}
+ if link_info[0] == 'url4':
+ link_params['baz'] = 'qux'
+ self.assertLinkShortenedHtml(
+ new_mail.mail_message_id.body,
+ link_info,
+ link_params=link_params,
+ )
diff --git a/addons/mass_mailing/tests/test_mailing_list.py b/addons/mass_mailing/tests/test_mailing_list.py
new file mode 100644
index 00000000..5501f944
--- /dev/null
+++ b/addons/mass_mailing/tests/test_mailing_list.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import exceptions
+from odoo.addons.mass_mailing.tests.common import MassMailCommon
+from odoo.tests.common import Form, users
+
+
+class TestMailingListMerge(MassMailCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMailingListMerge, cls).setUpClass()
+ cls._create_mailing_list()
+
+ cls.mailing_list_3 = cls.env['mailing.list'].with_context(cls._test_context).create({
+ 'name': 'ListC',
+ 'contact_ids': [
+ (0, 0, {'name': 'Norberto', 'email': 'norbert@example.com'}),
+ ]
+ })
+
+
+ @users('user_marketing')
+ def test_mailing_contact_create(self):
+ default_list_ids = (self.mailing_list_2 | self.mailing_list_3).ids
+
+ # simply set default list in context
+ 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,
+ } for x in range(0, 5)])
+ self.assertEqual(new.list_ids, (self.mailing_list_2 | self.mailing_list_3))
+
+ # default list and subscriptions should be merged
+ 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, {
+ 'list_id': self.mailing_list_1.id,
+ 'opt_out': True,
+ }), (0, 0, {
+ 'list_id': self.mailing_list_2.id,
+ 'opt_out': True,
+ })],
+ } for x in range(0, 5)])
+ self.assertEqual(new.list_ids, (self.mailing_list_1 | self.mailing_list_2 | self.mailing_list_3))
+ # should correctly take subscription opt_out value
+ for list_id in (self.mailing_list_1 | self.mailing_list_2).ids:
+ new = new.with_context(default_list_ids=[list_id])
+ self.assertTrue(all(contact.opt_out for contact in new))
+ # not opt_out for new subscription without specific create values
+ for list_id in self.mailing_list_3.ids:
+ new = new.with_context(default_list_ids=[list_id])
+ self.assertFalse(any(contact.opt_out for contact in new))
+
+ @users('user_marketing')
+ def test_mailing_list_contact_copy_in_context_of_mailing_list(self):
+ MailingContact = self.env['mailing.contact']
+ contact_1 = MailingContact.create({
+ 'name': 'Sam',
+ 'email': 'gamgee@shire.com',
+ 'subscription_list_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()
+ self.assertEqual(contact_1.list_ids, contact_2.list_ids, 'Should copy the existing mailing list(s)')
+
+ @users('user_marketing')
+ def test_mailing_list_merge(self):
+ # TEST CASE: Merge A,B into the existing mailing list C
+ # The mailing list C contains the same email address than 'Norbert' in list B
+ # This test ensure that the mailing lists are correctly merged and no
+ # duplicates are appearing in C
+ merge_form = Form(self.env['mailing.list.merge'].with_context(
+ active_ids=[self.mailing_list_1.id, self.mailing_list_2.id],
+ active_model='mailing.list'
+ ))
+ merge_form.new_list_name = False
+ merge_form.dest_list_id = self.mailing_list_3
+ merge_form.merge_options = 'existing'
+ merge_form.archive_src_lists = False
+ result_list = merge_form.save().action_mailing_lists_merge()
+
+ # Assert the number of contacts is correct
+ self.assertEqual(
+ len(result_list.contact_ids.ids), 5,
+ 'The number of contacts on the mailing list C is not equal to 5')
+
+ # Assert there's no duplicated email address
+ self.assertEqual(
+ len(list(set(result_list.contact_ids.mapped('email')))), 5,
+ 'Duplicates have been merged into the destination mailing list. Check %s' % (result_list.contact_ids.mapped('email')))
+
+ @users('user_marketing')
+ def test_mailing_list_merge_cornercase(self):
+ """ Check wrong use of merge wizard """
+ with self.assertRaises(exceptions.UserError):
+ merge_form = Form(self.env['mailing.list.merge'].with_context(
+ active_ids=[self.mailing_list_1.id, self.mailing_list_2.id],
+ ))
+
+ merge_form = Form(self.env['mailing.list.merge'].with_context(
+ active_ids=[self.mailing_list_1.id],
+ active_model='mailing.list',
+ default_src_list_ids=[self.mailing_list_1.id, self.mailing_list_2.id],
+ default_dest_list_id=self.mailing_list_3.id,
+ default_merge_options='existing',
+ ))
+ merge = merge_form.save()
+ self.assertEqual(merge.src_list_ids, self.mailing_list_1 + self.mailing_list_2)
+ self.assertEqual(merge.dest_list_id, self.mailing_list_3)