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

@ -6,6 +6,7 @@ from . import test_blacklist_behavior
from . import test_blacklist_mixin
from . import test_link_tracker
from . import test_link_tracker_sms
from . import test_mail_composer
from . import test_mailing
from . import test_mailing_server
from . import test_mailing_sms
@ -13,4 +14,5 @@ from . import test_mailing_statistics
from . import test_mailing_statistics_sms
from . import test_mailing_test
from . import test_performance
from . import test_sms_controller
from . import test_utm

View file

@ -4,10 +4,9 @@
from odoo.addons.phone_validation.tools import phone_validation
from odoo.addons.mass_mailing_sms.tests.common import MassSMSCommon
from odoo.addons.test_mail_sms.tests.common import TestSMSCommon
class TestMassMailCommon(MassSMSCommon, TestSMSCommon):
class TestMassMailCommon(MassSMSCommon):
@classmethod
def setUpClass(cls):
@ -15,7 +14,6 @@ class TestMassMailCommon(MassSMSCommon, TestSMSCommon):
cls.test_alias = cls.env['mail.alias'].create({
'alias_name': 'test.alias',
'alias_user_id': False,
'alias_model_id': cls.env['ir.model']._get('mailing.test.simple').id,
'alias_contact': 'everyone'
})
@ -119,7 +117,7 @@ class TestMassSMSCommon(TestMassMailCommon):
'name': 'Partner_%s' % (x),
'email': '_test_partner_%s@example.com' % (x),
'country_id': country_be_id,
'mobile': '045600%s%s99' % (x, x)
'phone': '045600%s%s99' % (x, x)
})
records += cls.env['mail.test.sms'].with_context(**cls._test_context).create({
'name': 'MassSMSTest_%s' % (x),
@ -137,6 +135,24 @@ class TestMassSMSCommon(TestMassMailCommon):
})
cls.partner_numbers = [
phone_validation.phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
phone_validation.phone_format(partner.phone, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
for partner in partners
]
@classmethod
def _get_sms_test_records(cls, mobile_numbers):
""" Helper to create data. Currently simple, to be improved. """
country_be_id = cls.env.ref('base.be').id
partners = cls.env['res.partner'].with_context(**cls._test_context).create([{
'name': f'Partner_{x}',
'email': f'_test_partner_{x}@example.com',
'country_id': country_be_id,
'phone': mobile_numbers[x]
} for x, mobile_number in enumerate(mobile_numbers)])
records = cls.env['mail.test.sms'].with_context(**cls._test_context).create([{
'name': f'MassSMSTest_{x}',
'customer_id': partner.id,
'phone_nbr': mobile_number
} for x, (mobile_number, partner) in enumerate(zip(mobile_numbers, partners))])
return records

View file

@ -63,20 +63,23 @@ class TestAutoBlacklist(common.TestMassMailCommon):
# is not sufficient
with freeze_time(new_dt), patch.object(Cursor, 'now', lambda *args, **kwargs: new_dt):
traces += self._create_bounce_trace(new_mailing, target, dt=datetime.datetime.now() - datetime.timedelta(weeks=idx+2))
self.gateway_mail_bounce(new_mailing, target, bounce_base_values)
self.gateway_mail_trace_bounce(new_mailing, target, bounce_base_values)
# mass mail record: ok, not blacklisted yet
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com'}],
[{
'email': 'test.record.00@test.example.com',
'email_to_mail': '"TestCustomer 00" <test.record.00@test.example.com>',
}],
mailing, target,
check_mail=True
)
# call bounced
self.gateway_mail_bounce(mailing, target, bounce_base_values)
self.gateway_mail_trace_bounce(mailing, target, bounce_base_values)
# check blacklist
blacklist_record = self.env['mail.blacklist'].sudo().search([('email', '=', target.email_normalized)])
@ -88,6 +91,11 @@ class TestAutoBlacklist(common.TestMassMailCommon):
with self.mock_mail_gateway(mail_unlink_sent=False):
new_mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
[{
'email': 'test.record.00@test.example.com',
'email_to_mail': '"TestCustomer 00" <test.record.00@test.example.com>',
'failure_type': 'mail_bl',
'trace_status': 'cancel',
}],
new_mailing, target, check_mail=True
)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mass_mailing.models.mailing_models import MailingBLacklist
from odoo.addons.test_mass_mailing.models.mailing_models import MailingTestBlacklist
from odoo.addons.test_mass_mailing.tests import common
from odoo.exceptions import UserError
from odoo.tests.common import users
@ -23,15 +23,15 @@ class TestBLMixin(common.TestMassMailCommon):
@users('employee')
def test_bl_mixin_primary_field_consistency(self):
MailingBLacklist._primary_email = 'not_a_field'
MailingTestBlacklist._primary_email = 'not_a_field'
with self.assertRaises(UserError):
self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', False)])
MailingBLacklist._primary_email = ['not_a_str']
MailingTestBlacklist._primary_email = ['not_a_str']
with self.assertRaises(UserError):
self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', False)])
MailingBLacklist._primary_email = 'email_from'
MailingTestBlacklist._primary_email = 'email_from'
self.env['mailing.test.blacklist'].search([('is_blacklisted', '=', False)])
@users('employee')

View file

@ -10,9 +10,9 @@ class TestLinkTracker(common.TestMassMailCommon):
def setUp(self):
super(TestLinkTracker, self).setUp()
self.link = self.env['link.tracker'].search_or_create({
self.link = self.env['link.tracker'].search_or_create([{
'url': 'https://www.example.com'
})
}])
self.click = self.env['link.tracker.click'].create({
'link_id': self.link.id,
@ -34,15 +34,6 @@ class TestLinkTracker(common.TestMassMailCommon):
self.assertEqual(click.country_id, self.env.ref('base.be'))
self.assertEqual(self.link.count, 2)
# click from same IP (even another country) does not create a new entry
click = self.env['link.tracker.click'].sudo().add_click(
code,
ip='100.00.00.01',
country_code='FRA'
)
self.assertEqual(click, None)
self.assertEqual(self.link.count, 2)
@users('user_marketing')
def test_add_link_mail_stat(self):
record = self.env['mailing.test.blacklist'].create({})

View file

@ -0,0 +1,38 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields
from odoo.tests import freeze_time, tagged, users
from odoo.tools import mute_logger
from odoo.addons.test_mail.tests.test_mail_composer import TestMailComposer
from odoo.addons.test_mass_mailing.tests import common
@tagged('mail_composer')
class TestMailComposerMassMailing(TestMailComposer, common.TestMassMailCommon):
@users('user_marketing')
@mute_logger('odoo.addons.mass_mailing.models.mailing')
@freeze_time('2025-08-06 15:02:00')
def test_mail_composer_mailing_creation(self):
"""Check mailing configuration created through the mail composer."""
for use_exclusion_list in (True, False):
mass_mailing_name = f'Test Create Mass Mailing From Composer (use_exclusion_list: {use_exclusion_list})'
composer = self.env['mail.compose.message'].with_context(
self._get_web_context(self.test_records)
).create({
'body': '<p>Body</p>',
'mass_mailing_name': mass_mailing_name,
'subject': 'Test',
'use_exclusion_list': use_exclusion_list,
})
composer._action_send_mail()
mailing = self.env['mailing.mailing'].search([('name', '=', mass_mailing_name)])
self.assertTrue(mailing)
self.assertEqual(mailing.body_html, '<p>Body</p>')
self.assertEqual(mailing.mailing_domain, f"[('id', 'in', {self.test_records.ids})]")
self.assertEqual(mailing.mailing_model_name, self.test_record._name)
self.assertEqual(mailing.sent_date, fields.Datetime.now())
self.assertEqual(mailing.state, 'done')
self.assertEqual(mailing.subject, 'Test')
self.assertEqual(mailing.use_exclusion_list, use_exclusion_list)

View file

@ -11,9 +11,33 @@ from odoo.tools import mute_logger, email_normalize
@tagged('mass_mailing')
class TestMassMailing(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestMassMailing, cls).setUpClass()
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_author(self):
""" Check the author of the mails sent by a mailing. """
mailing = self.env['mailing.mailing'].with_user(self.user_marketing).create({
'body_html': '<p>Test</p>',
'mailing_domain': [('id', 'in', self.user_employee.partner_id.ids)],
'mailing_model_id': self.env['ir.model']._get_id('res.partner'),
'name': 'test',
'subject': 'Test author',
})
mailing_2 = mailing.copy({'user_id': self.user_marketing_1.id})
with self.mock_mail_gateway(mail_unlink_sent=False):
(mailing | mailing_2).with_user(self.env.ref('base.user_root')).action_send_mail()
self.assertEqual(
self._new_mails.filtered(lambda m: m.model == 'res.partner').mapped('author_id'),
self.user_marketing.partner_id | self.user_marketing_1.partner_id,
"When a mailing is sent by OdooBot, the author of the mails must be the author of the mailing."
)
with self.mock_mail_gateway(mail_unlink_sent=False):
(mailing | mailing_2).copy().with_user(self.user_marketing_1).action_send_mail()
self.assertEqual(
self._new_mails.filtered(lambda m: m.model == 'res.partner').mapped('author_id'),
self.user_marketing_1.partner_id,
"When a mailing is sent by a user, the author of the mails must be the author who has sent the mailing."
)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_thread')
@ -86,8 +110,10 @@ class TestMassMailing(TestMassMailCommon):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': record.email_normalized}
for record in recipients],
[{
'email': record.email_normalized,
'email_to_mail': record.email_from,
} for record in recipients],
mailing, recipients,
mail_links_info=[[
('url0', 'https://www.odoo.tz/my/%s' % record.name, True, {}),
@ -107,16 +133,23 @@ class TestMassMailing(TestMassMailCommon):
self.assertMailingStatistics(mailing, expected=5, delivered=5, sent=5)
# simulate a click
self.gateway_mail_click(mailing, recipients[0], 'https://www.odoo.be')
self.gateway_mail_trace_click(mailing, recipients[0], 'https://www.odoo.be')
mailing.invalidate_recordset()
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])
self.gateway_mail_trace_bounce(mailing, recipients[1])
mailing.invalidate_recordset()
self.assertMailingStatistics(mailing, expected=5, delivered=4, sent=5, opened=1, clicked=1, bounced=1)
self.assertEqual(recipients[1].message_bounce, 1)
self.assertMailTraces([{
'email': 'test.record.01@test.example.com',
'email_to_mail': recipients[1].email_from,
'failure_reason': 'This is the bounce email',
'failure_type': 'mail_bounce',
'trace_status': 'bounce',
}], mailing, recipients[1], check_mail=False)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
@ -209,51 +242,57 @@ class TestMassMailing(TestMassMailCommon):
# -> email_to_recipients: email_to for outgoing emails, list means several recipients
self.assertMailTraces(
[
{'email': 'customer.multi.1@example.com, "Test Multi 2" <customer.multi.2@example.com>',
{'email': 'customer.multi.1@example.com',
'email_to_mail': '', # using recipient_ids, not email_to
'email_to_recipients': [[f'"{customer_mult.name}" <customer.multi.1@example.com>', f'"{customer_mult.name}" <customer.multi.2@example.com>']],
'failure_type': False,
'partner': customer_mult,
'trace_status': 'sent'},
{'email': '"Formatted Customer" <test.customer.format@example.com>',
{'email': 'test.customer.format@example.com',
'email_to_mail': '', # using recipient_ids, not email_to
# mail to avoids double encapsulation
'email_to_recipients': [[f'"{customer_fmt.name}" <test.customer.format@example.com>']],
'failure_type': False,
'partner': customer_fmt,
'trace_status': 'sent'},
{'email': '"Unicode Customer" <test.customer.😊@example.com>',
{'email': 'test.customer.😊@example.com',
'email_to_mail': '', # using recipient_ids, not email_to
# mail to avoids double encapsulation
'email_to_recipients': [[f'"{customer_unic.name}" <test.customer.😊@example.com>']],
'failure_type': False,
'partner': customer_unic,
'trace_status': 'sent'},
{'email': 'TEST.CUSTOMER.CASE@EXAMPLE.COM',
{'email': 'test.customer.case@example.com',
'email_to_mail': '', # using recipient_ids, not email_to
'email_to_recipients': [[f'"{customer_case.name}" <test.customer.case@example.com>']],
'failure_type': False,
'partner': customer_case,
'trace_status': 'sent'}, # lower cased
{'email': 'test.customer.weird@example.com Weird Format',
{'email': 'test.customer.weird@example.comweirdformat',
'email_to_mail': '', # using recipient_ids, not email_to
'email_to_recipients': [[f'"{customer_weird.name}" <test.customer.weird@example.comweirdformat>']],
'failure_type': False,
'partner': customer_weird,
'trace_status': 'sent'}, # concatenates everything after domain
{'email': 'Weird Format2 test.customer.weird.2@example.com',
{'email': 'test.customer.weird.2@example.com',
'email_to_mail': '', # using recipient_ids, not email_to
'email_to_recipients': [[f'"{customer_weird_2.name}" <test.customer.weird.2@example.com>']],
'failure_type': False,
'partner': customer_weird_2,
'trace_status': 'sent'},
{'email': 'record.multi.1@example.com',
'email_to_mail': 'record.multi.1@example.com,record.multi.2@example.com',
'email_to_recipients': [['record.multi.1@example.com', 'record.multi.2@example.com']],
'email_to_mail': 'record.multi.1@example.com,"Record Multi 2" <record.multi.2@example.com>',
'email_to_recipients': [['record.multi.1@example.com', '"Record Multi 2" <record.multi.2@example.com>']],
'failure_type': False,
'trace_status': 'sent'},
{'email': 'record.format@example.com',
'email_to_mail': 'record.format@example.com',
'email_to_recipients': [['record.format@example.com']],
'email_to_mail': '"Formatted Record" <record.format@example.com>',
'email_to_recipients': [['"Formatted Record" <record.format@example.com>']],
'failure_type': False,
'trace_status': 'sent'},
{'email': 'record.😊@example.com',
'email_to_mail': 'record.😊@example.com',
'email_to_recipients': [['record.😊@example.com']],
'email_to_mail': '"Unicode Record" <record.😊@example.com>',
'email_to_recipients': [['"Unicode Record" <record.😊@example.com>']],
'failure_type': False,
'trace_status': 'sent'},
{'email': 'test.record.case@example.com',
@ -267,8 +306,8 @@ class TestMassMailing(TestMassMailCommon):
'failure_type': False,
'trace_status': 'sent'},
{'email': 'test.record.weird.2@example.com',
'email_to_mail': 'test.record.weird.2@example.com',
'email_to_recipients': [['test.record.weird.2@example.com']],
'email_to_mail': '"Weird Format2" <test.record.weird.2@example.com>',
'email_to_recipients': [['"Weird Format2" <test.record.weird.2@example.com>']],
'failure_type': False,
'trace_status': 'sent'},
],
@ -294,7 +333,11 @@ class TestMassMailing(TestMassMailCommon):
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
answer_rec = self.gateway_mail_reply_wemail(MAIL_TEMPLATE, recipients[0].email_normalized, target_model=self.test_alias.alias_model_id.model)
answer_rec = self.gateway_mail_reply_wemail(
MAIL_TEMPLATE,
recipients[0].email_from,
target_model=self.test_alias.alias_model_id.model,
)
self.assertTrue(bool(answer_rec))
self.assertEqual(answer_rec.name, 'Re: %s' % mailing.subject)
self.assertEqual(
@ -320,7 +363,11 @@ class TestMassMailing(TestMassMailCommon):
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing.action_send_mail()
answer_rec = self.gateway_mail_reply_wemail(MAIL_TEMPLATE, recipients[0].email_normalized, target_model=self.test_alias.alias_model_id.model)
answer_rec = self.gateway_mail_reply_wemail(
MAIL_TEMPLATE,
recipients[0].email_from,
target_model=self.test_alias.alias_model_id.model,
)
self.assertFalse(bool(answer_rec))
self.assertEqual(
recipients[0].message_ids[1].subject, mailing.subject,
@ -388,8 +435,8 @@ class TestMassMailing(TestMassMailCommon):
self.env['mail.blacklist'].create({'email': recipients[4].email_normalized})
# unblacklist record 2
self.env['mail.blacklist'].action_remove_with_reason(
recipients[2].email_normalized, "human error"
self.env['mail.blacklist']._remove(
recipients[2].email_normalized, message="human error"
)
self.env['mail.blacklist'].flush_model(['active'])
@ -398,14 +445,36 @@ class TestMassMailing(TestMassMailCommon):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com'},
{'email': 'test.record.01@test.example.com'},
{'email': 'test.record.02@test.example.com'},
{'email': 'test.record.03@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'},
{'email': 'test.record.04@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
mailing, recipients, check_mail=True
[{
'email': record.email_normalized,
'email_to_mail': record.email_from,
} for record in recipients[:3]] + [{
'email': record.email_normalized,
'email_to_mail': record.email_from,
'failure_type': 'mail_bl',
'trace_status': 'cancel',
} for record in recipients[3:]],
mailing, recipients
)
self.assertEqual(mailing.canceled, 2)
self.assertEqual(len(self.env['mail.mail'].sudo().search([('mailing_id', '=', mailing.id)])), 3,
"Only the 3 sent mails have been created, the canceled ones have not been created")
# Same test but with the option use_exclusion_list set to False
mailing = mailing.copy()
mailing.use_exclusion_list = False
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing.action_send_mail()
self.assertMailTraces(
[{
'email': record.email_normalized,
'email_to_mail': record.email_from,
} for record in recipients],
mailing, recipients, check_mail=True
)
self.assertEqual(mailing.canceled, 0)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
@ -413,6 +482,9 @@ class TestMassMailing(TestMassMailCommon):
"""Test that blacklist is applied even if the target model doesn't inherit
from mail.thread.blacklist."""
test_records = self._create_mailing_test_records(model='mailing.test.simple', count=2)
# Normalize email_from for assertMailTraces
for test_record in test_records:
test_record.email_from = email_normalize(test_record.email_from)
self.mailing_bl.write({
'mailing_domain': [('id', 'in', test_records.ids)],
'mailing_model_id': self.env['ir.model']._get('mailing.test.simple').id,
@ -425,9 +497,26 @@ class TestMassMailing(TestMassMailCommon):
with self.mock_mail_gateway(mail_unlink_sent=False):
self.mailing_bl.action_send_mail()
self.assertMailTraces([
{'email': email_normalize(test_records[0].email_from), 'trace_status': 'cancel', 'failure_type': 'mail_bl'},
{'email': email_normalize(test_records[1].email_from), 'trace_status': 'sent'},
], self.mailing_bl, test_records, check_mail=False)
{'email': test_records[0].email_from, 'trace_status': 'cancel', 'failure_type': 'mail_bl'},
{'email': test_records[1].email_from, 'trace_status': 'sent'},
], self.mailing_bl, test_records, check_mail=True)
self.assertEqual(self.mailing_bl.canceled, 1)
self.assertEqual(len(self.env['mail.mail'].sudo().search([('mailing_id', '=', self.mailing_bl.id)])), 1,
"Only the sent mail has been created, the canceled one has not been created")
# Same test but with the option use_exclusion_list set to False
mailing_no_blacklist = self.mailing_bl.copy()
mailing_no_blacklist.use_exclusion_list = False
with self.mock_mail_gateway(mail_unlink_sent=False):
mailing_no_blacklist.action_send_mail()
self.assertMailTraces([
{'email': test_records[0].email_from, 'trace_status': 'sent'},
{'email': test_records[1].email_from, 'trace_status': 'sent'},
], mailing_no_blacklist, test_records, check_mail=True)
self.assertEqual(mailing_no_blacklist.canceled, 0)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
@ -448,14 +537,23 @@ class TestMassMailing(TestMassMailCommon):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'},
{'email': 'test.record.01@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'},
{'email': 'test.record.02@test.example.com'},
{'email': 'test.record.03@test.example.com'},
{'email': 'test.record.04@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
mailing, recipients, check_mail=True
)
[{
'email': record.email_normalized,
'email_to_mail': record.email_from,
'failure_type': 'mail_optout',
'trace_status': 'cancel',
} for record in recipients[:2]] + [{
'email': record.email_normalized,
'email_to_mail': record.email_from,
} for record in recipients[2:4]] + [{
'email': record.email_normalized,
'email_to_mail': record.email_from,
'failure_type': 'mail_bl',
'trace_status': 'cancel'
} for record in recipients[4:]], mailing, recipients)
self.assertEqual(mailing.canceled, 3)
self.assertEqual(len(self.env['mail.mail'].sudo().search([('mailing_id', '=', self.mailing_bl.id)])), 2,
"Only the 2 sent mails have been created, the canceled ones have not been created")
@users('user_marketing')
def test_mailing_w_seenlist(self):
@ -481,7 +579,7 @@ class TestMassMailing(TestMassMailCommon):
with self.mock_mail_gateway():
for i in range(0, 20, BATCH_SIZE):
mailing.action_send_mail(records[i:i + BATCH_SIZE].mapped('id'))
mailing.action_send_mail(records[i:i + BATCH_SIZE].ids)
self.assertEqual(len(self._mails), BATCH_SIZE)
self.assertEqual(mailing.canceled, 15)
mails_sent = [email_normalize(mail['email_to'][0]) for mail in self._mails]
@ -532,11 +630,13 @@ class TestMassMailing(TestMassMailCommon):
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_mailing_mailing_list_optout(self):
""" Test mailing list model specific optout behavior """
mailing_contact_1 = self.env['mailing.contact'].create({'name': 'test 1A', 'email': 'test@test.example.com'})
mailing_contact_2 = self.env['mailing.contact'].create({'name': 'test 1B', 'email': 'test@test.example.com'})
# as duplication checks body and subject, we create 2 exact copies to make sure only 1 is sent
mailing_contact_1 = self.env['mailing.contact'].create({'name': 'test 1', 'email': 'test@test.example.com'})
mailing_contact_2 = self.env['mailing.contact'].create({'name': 'test 1', 'email': 'test@test.example.com'})
mailing_contact_3 = self.env['mailing.contact'].create({'name': 'test 3', 'email': 'test3@test.example.com'})
mailing_contact_4 = self.env['mailing.contact'].create({'name': 'test 4', 'email': 'test4@test.example.com'})
mailing_contact_5 = self.env['mailing.contact'].create({'name': 'test 5', 'email': 'test5@test.example.com'})
records = mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5
# create mailing list record
mailing_list_1 = self.env['mailing.list'].create({
@ -558,7 +658,7 @@ class TestMassMailing(TestMassMailCommon):
# contact_1 is optout but same email is not optout from the same list
# contact 3 is optout in list 1 but not in list 2
# contact 5 is optout
subs = self.env['mailing.contact.subscription'].search([
subs = self.env['mailing.subscription'].search([
'|', '|',
'&', ('contact_id', '=', mailing_contact_1.id), ('list_id', '=', mailing_list_1.id),
'&', ('contact_id', '=', mailing_contact_3.id), ('list_id', '=', mailing_list_1.id),
@ -578,13 +678,12 @@ class TestMassMailing(TestMassMailCommon):
mailing.action_send_mail()
self.assertMailTraces(
[{'email': 'test@test.example.com', 'trace_status': 'sent'},
{'email': 'test@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_dup'},
[{'email': 'test@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_dup'},
{'email': 'test@test.example.com', 'trace_status': 'sent'},
{'email': 'test3@test.example.com'},
{'email': 'test4@test.example.com'},
{'email': 'test5@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'}],
mailing,
mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5,
check_mail=True
)
self.assertEqual(mailing.canceled, 2)

View file

@ -8,13 +8,12 @@ from odoo.tests.common import users
from odoo.tools import mute_logger
@tagged('mass_mailing')
@tagged('mass_mailing', 'mail_server')
class TestMassMailingServer(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super(TestMassMailingServer, cls).setUpClass()
cls._init_mail_servers()
cls.recipients = cls._create_mailing_test_records(model='mailing.test.optout', count=8)
def test_mass_mailing_server_archived_usage_protection(self):
@ -73,98 +72,48 @@ class TestMassMailingServer(TestMassMailCommon):
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing')
def test_mass_mailing_server_batch(self):
def test_mass_mailing_server_find(self):
"""Test that the right mail server is chosen to send the mailing.
Test also the envelop and the SMTP headers.
"""
# Test sending mailing in batch
mailings = self.env['mailing.mailing'].create([{
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': 'specific_user@test.com',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
'email_from': 'specific_user@test.mycompany.com',
'mailing_model_id': self.env['ir.model']._get_id('mailing.test.optout'),
}, {
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': 'unknown_name@test.com',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
}])
with self.mock_smtplib_connection():
mailings.action_send_mail()
self.assertEqual(self.find_mail_server_mocked.call_count, 2, 'Must be called only once per mail from')
self.assert_email_sent_smtp(
smtp_from='specific_user@test.com',
message_from='specific_user@test.com',
from_filter=self.server_user.from_filter,
emails_count=8,
)
self.assert_email_sent_smtp(
# Must use the bounce address here because the mail server
# is configured for the entire domain "test.com"
smtp_from=lambda x: 'bounce' in x,
message_from='unknown_name@test.com',
from_filter=self.server_domain.from_filter,
emails_count=8,
)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing')
def test_mass_mailing_server_default(self):
# We do not have a mail server for this address email, so fall back to the
# "notifications@domain" email.
mailings = self.env['mailing.mailing'].create([{
'email_from': 'unknown_name@test.mycompany.com',
'mailing_model_id': self.env['ir.model']._get_id('mailing.test.optout'),
}, {
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': '"Testing" <unknow_email@unknow_domain.com>',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
}])
with self.mock_smtplib_connection():
mailings.action_send_mail()
self.assertEqual(self.find_mail_server_mocked.call_count, 1)
self.assert_email_sent_smtp(
smtp_from='notifications@test.com',
message_from='"Testing" <notifications@test.com>',
from_filter=self.server_notification.from_filter,
emails_count=8,
)
self.assertEqual(self.find_mail_server_mocked.call_count, 1, 'Must be called only once')
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.addons.mass_mailing.models.mailing')
def test_mass_mailing_server_forced(self):
# We force a mail server on one mailing
mailings = self.env['mailing.mailing'].create([{
'subject': 'Mailing',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': self.server_user.from_filter,
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
'mailing_model_id': self.env['ir.model']._get_id('mailing.test.optout'),
}, {
'subject': 'Mailing',
'subject': 'Mailing Forced',
'body_html': 'Body for <t t-out="object.name" />',
'email_from': 'unknow_email@unknow_domain.com',
'mailing_model_id': self.env['ir.model']._get('mailing.test.optout').id,
'mail_server_id': self.server_notification.id,
'mailing_model_id': self.env['ir.model']._get_id('mailing.test.optout'),
'mail_server_id': self.mail_server_notification.id,
}])
with self.mock_smtplib_connection():
mailings.action_send_mail()
self.assertEqual(self.find_mail_server_mocked.call_count, 1, 'Must not be called when mail server is forced')
self.assertEqual(self.find_mail_server_mocked.call_count, 3, 'Must be called only once per mail from except when forced')
self.assert_email_sent_smtp(
smtp_from='specific_user@test.com',
message_from='specific_user@test.com',
from_filter=self.server_user.from_filter,
emails_count=8,
)
self.assert_email_sent_smtp(
smtp_from='unknow_email@unknow_domain.com',
message_from='unknow_email@unknow_domain.com',
from_filter=self.server_notification.from_filter,
emails_count=8,
)
for (expected_smtp_from, expected_msg_from, expected_mail_server) in [
('specific_user@test.mycompany.com', 'specific_user@test.mycompany.com', self.mail_server_user),
(f'{self.alias_bounce}@{self.alias_domain}', 'unknown_name@test.mycompany.com', self.mail_server_domain),
# We do not have a mail server for this address email, so fall back to the "notifications@domain" email.
(f'{self.default_from}@{self.alias_domain}', f'"Testing" <{self.default_from}@{self.alias_domain}>', self.mail_server_notification),
# forced sever
('unknow_email@unknow_domain.com', 'unknow_email@unknow_domain.com', self.mail_server_notification),
]:
self.assertSMTPEmailsSent(
smtp_from=expected_smtp_from,
message_from=expected_msg_from,
mail_server=expected_mail_server,
emails_count=8,
)

View file

@ -4,6 +4,7 @@
from ast import literal_eval
from odoo.addons.phone_validation.tools import phone_validation
from odoo.addons.sms_twilio.tests.common import MockSmsTwilioApi
from odoo.addons.test_mass_mailing.tests.common import TestMassSMSCommon
from odoo import exceptions
from odoo.tests import tagged
@ -30,7 +31,7 @@ class TestMassSMSInternals(TestMassSMSCommon):
'mailing_model_id': self.env['ir.model']._get('mail.test.sms.bl').id,
'mailing_type': 'sms',
})
self.assertEqual(literal_eval(mailing.mailing_domain), [('phone_sanitized_blacklisted', '=', False)])
self.assertEqual(literal_eval(mailing.mailing_domain), [])
@users('user_marketing')
def test_mass_sms_internals(self):
@ -97,7 +98,7 @@ class TestMassSMSInternals(TestMassSMSCommon):
nr2_partner = self.env['res.partner'].create({
'name': 'Partner_nr2',
'country_id': country_be_id,
'mobile': '0456449999',
'phone': '0456449999',
})
new_record_2 = self.env['mail.test.sms'].create({
'name': 'MassSMSTest_nr2',
@ -107,17 +108,27 @@ class TestMassSMSInternals(TestMassSMSCommon):
records_numbers = self.records_numbers + ['+32456999999']
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
mailing.write({'sms_force_send': False}) # force outgoing sms, not sent
mailing.write({
'sms_force_send': False, # force outgoing sms, not sent
'keep_archives': True, # keep a note on document (mass_keep_log)
})
with self.with_user('user_marketing'):
with self.mockSMSGateway():
mailing.action_send_sms()
valid_records = self.records | new_record_1
self.assertSMSTraces(
[{'partner': record.customer_id, 'number': records_numbers[i],
'content': 'Dear %s this is a mass SMS' % record.display_name}
for i, record in enumerate(self.records | new_record_1)],
mailing, self.records | new_record_1,
mailing, valid_records,
)
self.assertEqual(
len(self.env['mail.message'].search([
('res_id', 'in', valid_records.ids), ('model', '=', 'mail.test.sms'),
('id', 'in', self._new_sms.mail_message_id.ids)])),
len(valid_records),
"Only not canceled message must be logged in the chatter")
# duplicates
self.assertSMSTraces(
[{'partner': new_record_2.customer_id, 'number': self.records_numbers[0],
@ -147,11 +158,30 @@ class TestMassSMSInternals(TestMassSMSCommon):
for record in falsy_record_1 + falsy_record_2],
mailing, falsy_record_1 + falsy_record_2,
)
self.assertEqual(mailing.canceled, 5)
# Same test using use_exclusion_list = False
mailing_no_blacklist = mailing.copy()
mailing_no_blacklist.use_exclusion_list = False
with self.with_user('user_marketing'):
with self.mockSMSGateway():
mailing_no_blacklist.action_send_sms()
self.assertSMSTraces(
[{'partner': bl_record_1.customer_id,
'number': phone_validation.phone_format(bl_record_1.phone_nbr, 'BE', '32', force_format='E164'),
'content': 'Dear %s this is a mass SMS' % bl_record_1.display_name}],
mailing_no_blacklist, bl_record_1,
)
self.assertEqual(mailing_no_blacklist.canceled, 4)
@users('user_marketing')
def test_mass_sms_internals_done_ids(self):
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
mailing.write({'sms_force_send': False}) # check with outgoing traces, not already sent
mailing.write({
'sms_force_send': False, # check with outgoing traces, not already pending
'keep_archives': True, # keep a note on document (mass_keep_log)
})
with self.with_user('user_marketing'):
with self.mockSMSGateway():
@ -189,6 +219,75 @@ class TestMassSMSInternals(TestMassSMSCommon):
mailing, self.records[5:],
)
@users('user_marketing')
def test_mass_sms_processing_with_force_send(self):
"""Test that a `processing` status returned by IAP is immediately applied to traces and mailing stays sending.
The status update case where the sms are sent via the cron is done in TestSmsController.
"""
with self.with_user('user_marketing'):
mailing = self.env['mailing.mailing'].create({
'name': 'Xmas Spam',
'subject': 'Xmas Spam',
'mailing_model_id': self.env['ir.model']._get('mail.test.sms').id,
'mailing_type': 'sms',
'mailing_domain': '%s' % repr([('name', 'ilike', 'MassSMSTest')]),
'sms_template_id': self.sms_template.id,
'sms_allow_unsubscribe': False,
})
remaining_res_ids = mailing._get_remaining_recipients()
self.assertEqual(set(remaining_res_ids), set(self.records.ids))
mailing.sms_force_send = True
with self.mockSMSGateway(moderated=True):
mailing.action_send_sms()
self.assertEqual(mailing.state, 'sending')
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': self.records_numbers[i],
'content': 'Dear %s this is a mass SMS.' % record.display_name,
'trace_status': 'process',
} for i, record in enumerate(self.records)],
mailing, self.records,
)
@users('user_marketing')
def test_mass_sms_error_with_force_send(self):
"""Test that a failed status returned by IAP is immediately applied to traces."""
with self.with_user('user_marketing'):
mailing = self.env['mailing.mailing'].create({
'name': 'Xmas Spam',
'subject': 'Xmas Spam',
'mailing_model_id': self.env['ir.model']._get('mail.test.sms').id,
'mailing_type': 'sms',
'mailing_domain': '%s' % repr([('name', 'ilike', 'MassSMSTest')]),
'sms_template_id': self.sms_template.id,
'sms_allow_unsubscribe': False,
})
remaining_res_ids = mailing._get_remaining_recipients()
self.assertEqual(set(remaining_res_ids), set(self.records.ids))
mailing.sms_force_send = True
with self.mockSMSGateway(sim_error='server_error'):
mailing.action_send_sms()
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': self.records_numbers[i],
'content': 'Dear %s this is a mass SMS.' % record.display_name,
'trace_status': 'error',
'failure_type': 'sms_server'
} for i, record in enumerate(self.records)],
mailing, self.records,
)
@tagged('mass_mailing', 'mass_mailing_sms', 'mailing_test')
class TestMassSMSTest(TestMassSMSCommon):
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
def test_mass_sms_test_button(self):
mailing = self.env['mailing.mailing'].create({
@ -199,15 +298,23 @@ class TestMassSMSInternals(TestMassSMSCommon):
'mailing_type': 'sms',
'body_plaintext': 'Hello {{ object.name }}',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
'sms_allow_unsubscribe': True,
})
mailing_test = self.env['mailing.sms.test'].with_user(self.user_marketing).create({
'numbers': '+32456001122',
'numbers': '+32456001122\n+32455334455\nwrong\n\n',
'mailing_id': mailing.id,
})
with self.with_user('user_marketing'):
with self.mockSMSGateway():
mailing_test.action_send_sms()
new_traces = self.env['mailing.trace'].search([('mass_mailing_id', '=', mailing.id)])
self.assertEqual(len(new_traces), 2, 'Should have create 1 trace / valid number')
self.assertEqual(new_traces.mapped('is_test_trace'), [True, True], 'Traces should be flagged as test')
self.assertEqual(
sorted(new_traces.mapped('sms_number')),
['+32455334455', '+32456001122']
)
# Test if bad inline_template in the body raises an error
mailing.write({
@ -238,7 +345,7 @@ class TestMassSMS(TestMassSMSCommon):
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': self.records_numbers[i],
'trace_status': 'sent',
'trace_status': 'pending',
'content': 'Dear %s this is a mass SMS with two links' % record.display_name
} for i, record in enumerate(self.records)],
mailing, self.records,
@ -273,7 +380,7 @@ class TestMassSMS(TestMassSMSCommon):
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': record.customer_id.phone_sanitized,
'trace_status': 'sent',
'trace_status': 'pending',
'content': 'Dear %s this is a mass SMS with two links' % record.display_name
} for record in records],
mailing, records,
@ -298,7 +405,7 @@ class TestMassSMS(TestMassSMSCommon):
self.assertSMSTraces(
[{'partner': new_record.customer_id,
'number': new_record.customer_id.phone_sanitized,
'trace_status': 'sent',
'trace_status': 'pending',
'content': 'Dear %s this is a mass SMS with two links' % new_record.display_name
}],
mailing, new_record,
@ -345,6 +452,7 @@ class TestMassSMS(TestMassSMSCommon):
mailing.write({
'mailing_model_id': self.env['ir.model']._get('mail.test.sms.bl.optout'),
'mailing_domain': [('id', 'in', recipients.ids)],
'keep_archives': True, # keep a note on document (mass_keep_log)
})
with self.mockSMSGateway():
@ -353,9 +461,84 @@ class TestMassSMS(TestMassSMSCommon):
self.assertSMSTraces(
[{'number': '+32456000000', 'trace_status': 'cancel', 'failure_type': 'sms_optout'},
{'number': '+32456000101', 'trace_status': 'cancel', 'failure_type': 'sms_optout'},
{'number': '+32456000202', 'trace_status': 'sent'},
{'number': '+32456000303', 'trace_status': 'sent'},
{'number': '+32456000202', 'trace_status': 'pending'},
{'number': '+32456000303', 'trace_status': 'pending'},
{'number': '+32456000404', 'trace_status': 'cancel', 'failure_type': 'sms_blacklist'}],
mailing, recipients
)
self.assertEqual(mailing.canceled, 3)
@tagged('mass_mailing', 'mass_mailing_sms', 'twilio')
class TestMassSMSTwilio(TestMassSMSCommon, MockSmsTwilioApi):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._setup_sms_twilio(cls.user_admin.company_id)
cls.mailing_sms.write({
'body_plaintext': 'This is a mass SMS',
'sms_template_id': False,
'sms_force_send': True,
'sms_allow_unsubscribe': False,
})
cls.records_fail = cls.env['mail.test.sms'].create([
{
'name': 'MassSMSTest- No Number',
'phone_nbr': False,
}, {
'name': 'MassSMSTest- Invalid Number',
'phone_nbr': '1234',
},
])
cls.records += cls.records_fail
cls.records_numbers += [False, '1234']
@users('user_marketing')
def test_mass_sms(self):
""" Test SMS marketing using twilio """
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
with self.mock_sms_twilio_gateway():
mailing.action_send_sms()
exp_failure_types = [False] * 10 + ['sms_number_missing', 'sms_number_format']
self.assertSMSTraces(
[{
'failure_type': exp_failure_types[i],
'partner': record.customer_id,
'number': self.records_numbers[i],
'trace_status': 'pending' if record not in self.records_fail else 'cancel',
'content': 'This is a mass SMS',
} for i, record in enumerate(self.records)],
mailing,
self.records,
)
@users('user_marketing')
def test_mass_sms_twilio_issue(self):
""" Test specific propagation / update to trace failure_type from twilio
errors """
for error_type, exp_failure_type in [
("twilio_acc_unverified", "sms_acc"),
("twilio_callback", "twilio_callback"),
]:
with self.subTest(error_type=error_type):
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids).copy()
with self.mock_sms_twilio_gateway(error_type=error_type):
mailing.action_send_sms()
exp_failure_types = [exp_failure_type] * 10 + ['sms_number_missing', 'sms_number_format']
self.assertSMSTraces(
[{
'failure_type': exp_failure_types[i],
'partner': record.customer_id,
'number': self.records_numbers[i],
'trace_status': 'error' if record not in self.records_fail else 'cancel',
'content': 'This is a mass SMS',
} for i, record in enumerate(self.records)],
mailing,
self.records,
)

View file

@ -29,8 +29,9 @@ class TestMailingStatistics(TestMassMailCommon):
@users('user_marketing')
@mute_logger('odoo.addons.mass_mailing.models.mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
def test_mailing_statistics(self):
target_records = self._create_mailing_test_records(model='mailing.test.blacklist', count=11)
target_records = self._create_mailing_test_records(model='mailing.test.blacklist', count=13)
target_records[10]['email_from'] = False # void email should lead to a 'cancel' trace_status
target_records[11]['email_from'] = 'raoul@example¢¡.com' # wrong email should lead to a 'exception' trace_status
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
mailing.write({'mailing_domain': [('id', 'in', target_records.ids)], 'user_id': self.user_marketing_2.id})
mailing.action_put_in_queue()
@ -41,20 +42,27 @@ class TestMailingStatistics(TestMassMailCommon):
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[0], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[1], use_in_reply_to=True)
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[2], use_in_reply_to=True)
self.gateway_mail_click(mailing, target_records[0], 'https://www.odoo.be')
self.gateway_mail_click(mailing, target_records[2], 'https://www.odoo.be')
self.gateway_mail_click(mailing, target_records[3], 'https://www.odoo.be')
self.gateway_mail_trace_click(mailing, target_records[0], 'https://www.odoo.be')
self.gateway_mail_trace_click(mailing, target_records[2], 'https://www.odoo.be')
self.gateway_mail_trace_click(mailing, target_records[3], 'https://www.odoo.be')
self.assertEqual(target_records[12].message_bounce, 0)
self.gateway_mail_trace_bounce(mailing, target_records[12])
self.assertEqual(target_records[12].message_bounce, 1)
# check mailing statistics
self.assertEqual(mailing.bounced, 1)
self.assertEqual(mailing.bounced_ratio, 9.09)
self.assertEqual(mailing.canceled, 1)
self.assertEqual(mailing.expected, 13)
self.assertEqual(mailing.clicked, 3)
self.assertEqual(mailing.clicks_ratio, 30)
self.assertEqual(mailing.delivered, 10)
self.assertEqual(mailing.failed, 1)
self.assertEqual(mailing.opened, 4)
self.assertEqual(mailing.opened_ratio, 40)
self.assertEqual(mailing.replied, 3)
self.assertEqual(mailing.replied_ratio, 30)
self.assertEqual(mailing.sent, 10)
self.assertEqual(mailing.sent, 11)
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing._action_send_statistics()
@ -69,13 +77,13 @@ class TestMailingStatistics(TestMassMailCommon):
self.assertEqual(mail.state, 'outgoing')
# test body content: KPIs
body_html = html.fromstring(mail.body_html)
kpi_values = body_html.xpath('//div[@data-field="mail"]//*[hasclass("kpi_value")]/text()')
kpi_values = body_html.xpath('//table[@data-field="mail"]//*[hasclass("kpi_value")]/text()')
self.assertEqual(
[t.strip().strip('%') for t in kpi_values],
['100', str(mailing.opened_ratio), str(mailing.replied_ratio)]
['83.33', str(mailing.opened_ratio), str(mailing.replied_ratio)] # first value is received_ratio
)
# test body content: clicks (a bit hackish but hey we are in stable)
kpi_click_values = body_html.xpath('//div[hasclass("global_layout")]/table//tr[contains(@style,"color: #888888")]/td[contains(@style,"width: 30%")]/text()')
kpi_click_values = body_html.xpath('//table//tr[contains(@style,"color: #888888")]/td[contains(@style,"width: 30%")]/text()')
first_link_value = int(kpi_click_values[0].strip().split()[1].strip('()'))
self.assertEqual(first_link_value, mailing.clicked)

View file

@ -9,7 +9,6 @@ from odoo.tests.common import users
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('digest', 'mass_mailing', 'mass_mailing_sms')
class TestMailingStatistics(TestMassSMSCommon):
@ -24,6 +23,9 @@ class TestMailingStatistics(TestMassSMSCommon):
name='Marie Marketing',
signature='--\nMarie'
)
mobile_numbers = [f'045300{x}{x}99' for x in range(6)] + ['0453000099'] * 4
cls.records += cls._get_sms_test_records(mobile_numbers=mobile_numbers)
cls.records = cls._reset_mail_context(cls.records)
@users('user_marketing')
@mute_logger('odoo.addons.mass_mailing_sms.models.mailing_mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
@ -35,17 +37,31 @@ class TestMailingStatistics(TestMassSMSCommon):
with self.mockSMSGateway():
mailing.action_send_sms()
# simulate some replies and clicks
self.gateway_sms_click(mailing, target_records[0])
self.gateway_sms_click(mailing, target_records[2])
self.gateway_sms_click(mailing, target_records[3])
# simulate some delivery reports
for record_idx in range(4):
self.gateway_sms_delivered(mailing, target_records[record_idx])
# simulate some replies and clicks
for record_idx in (0, 2, 3):
self.gateway_sms_click(mailing, target_records[record_idx])
for record_idx in (7, 8):
record = target_records[record_idx]
trace = mailing.mailing_trace_ids.filtered(lambda t: t.model == record._name and t.res_id == record.id)
trace.set_bounced()
# check mailing statistics
self.assertEqual(mailing.clicked, 3)
self.assertEqual(mailing.delivered, 10)
self.assertEqual(mailing.delivered, 4)
self.assertEqual(mailing.opened, 3)
self.assertEqual(mailing.opened_ratio, 30)
self.assertEqual(mailing.sent, 10)
self.assertEqual(mailing.sent, 16)
self.assertEqual(mailing.scheduled, 0)
self.assertEqual(mailing.canceled, 4)
self.assertEqual(mailing.process, 0)
self.assertEqual(mailing.pending, 10)
self.assertEqual(mailing.bounced, 2)
self.assertEqual(mailing.received_ratio, 25)
self.assertEqual(mailing.opened_ratio, 21.43)
self.assertEqual(mailing.bounced_ratio, 12.5)
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing._action_send_statistics()
@ -60,12 +76,27 @@ class TestMailingStatistics(TestMassSMSCommon):
self.assertEqual(mail.state, 'outgoing')
# test body content: KPIs
body_html = html.fromstring(mail.body_html)
kpi_values = body_html.xpath('//div[@data-field="sms"]//*[hasclass("kpi_value")]/text()')
kpi_values = body_html.xpath('//table[@data-field="sms"]//*[hasclass("kpi_value")]/text()')
self.assertEqual(
[t.strip().strip('%') for t in kpi_values],
['100', str(mailing.clicks_ratio), str(mailing.bounced_ratio)]
['25.0', str(float(mailing.clicks_ratio)), str(float(mailing.bounced_ratio))]
)
# test body content: clicks (a bit hackish but hey we are in stable)
kpi_click_values = body_html.xpath('//div[hasclass("global_layout")]/table//tr[contains(@style,"color: #888888")]/td[contains(@style,"width: 30%")]/text()')
kpi_click_values = body_html.xpath('//table//tr[contains(@style,"color: #888888")]/td[contains(@style,"width: 30%")]/text()')
first_link_value = int(kpi_click_values[0].strip().split()[1].strip('()'))
self.assertEqual(first_link_value, mailing.clicked)
@users('user_marketing')
@mute_logger('odoo.addons.mass_mailing_sms.models.mailing_mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
def test_sent_delivered_sms(self):
""" Test that if we get delivered trace status first instead of sent from
providers for some reasons, the statistics for sent SMS will be correct. """
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
target_records = self.env['mail.test.sms'].browse(self.records.ids)
mailing.write({'mailing_domain': [('id', 'in', target_records.ids)], 'user_id': self.user_marketing_2.id})
mailing.action_put_in_queue()
with self.mockSMSGateway(force_delivered=True):
mailing.action_send_sms()
self.assertEqual(mailing.delivered, 16)
self.assertEqual(mailing.sent, 16)

View file

@ -2,8 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import lxml.html
from odoo.addons.sms_twilio.tests.common import MockSmsTwilioApi
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
from odoo.fields import Command
from odoo.addons.test_mass_mailing.tests.common import TestMassSMSCommon
from odoo.tests.common import users, tagged
from odoo.tools import mute_logger
@ -11,18 +12,31 @@ from odoo.tools import mute_logger
@tagged('mailing_manage')
class TestMailingTest(TestMassMailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_records = cls.env['mailing.test.blacklist'].create([
{
'email_from': f'test.mailing.{idx}@test.example.com',
'name': f'Test Mailing {idx}',
'user_id': cls.user_marketing.id,
}
for idx in range(5)
])
cls.test_mailing_bl = cls.env['mailing.mailing'].create({
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'mailing_domain': [('id', 'in', cls.test_records.ids)],
'mailing_model_id': cls.env['ir.model']._get_id('mailing.test.blacklist'),
'mailing_type': 'mail',
'name': 'TestButton',
'preview': 'Preview {{ object.name }}',
'subject': 'Subject {{ object.name }}',
})
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
def test_mailing_test_button(self):
mailing = self.env['mailing.mailing'].create({
'name': 'TestButton',
'subject': 'Subject {{ object.name }}',
'preview': 'Preview {{ object.name }}',
'state': 'draft',
'mailing_type': 'mail',
'body_html': '<p>Hello <t t-out="object.name"/></p>',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
})
mailing = self.test_mailing_bl.with_env(self.env)
mailing_test = self.env['mailing.mailing.test'].create({
'email_to': 'test@test.com',
'mass_mailing_id': mailing.id,
@ -62,27 +76,52 @@ class TestMailingTest(TestMassMailCommon):
with self.mock_mail_gateway(), self.assertRaises(Exception):
mailing_test.send_mail_test()
def test_mailing_test_equals_reality(self):
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
def test_mailing_test_button_links(self):
"""This tests that the link provided by the View in Browser snippet is correctly replaced
when sending a test mailing while the Unsubscribe button's link isn't, to preserve the testing route
/unsubscribe_from_list.
This also checks that other links containing the /view route aren't replaced along the way.
"""
Check that both test and real emails will format the qweb and inline placeholders correctly in body and subject.
"""
self.env['mailing.contact'].search([]).unlink()
contact_list = self.env['mailing.list'].create({
'name': 'Testers',
'contact_ids': [Command.create({
'name': 'Mitchell Admin',
'email': 'real@real.com',
})],
mailing = self.test_mailing_bl.with_env(self.env)
mailing_test = self.env['mailing.mailing.test'].create({
'email_to': 'test@test.com',
'mass_mailing_id': mailing.id,
})
# Test if link snippets are correctly converted
mailing.write({
'body_html':
'''<p>
Hello <a href="http://www.example.com/view">World<a/>
<div class="o_snippet_view_in_browser o_mail_snippet_general pt16 pb16" style="text-align: center; padding-left: 15px; padding-right: 15px;">
<a href="/view">
View Online
</a>
</div>
<div class="o_mail_footer_links">
<a role="button" href="/unsubscribe_from_list" class="btn btn-link">Unsubscribe</a>
</div>
</p>''',
'preview': 'Preview {{ object.name }}',
'subject': 'Subject {{ object.name }}',
})
mailing = self.env['mailing.mailing'].create({
'name': 'TestButton',
'subject': 'Subject {{ object.name }} <t t-out="object.name"/>',
'state': 'draft',
'mailing_type': 'mail',
with self.mock_mail_gateway():
mailing_test.send_mail_test()
body_html = self._mails.pop()['body']
self.assertIn(f'/mailing/{mailing.id}/view', body_html) # Is replaced
self.assertIn('/unsubscribe_from_list', body_html) # Isn't replaced
self.assertIn('http://www.example.com/view', body_html) # Isn't replaced
def test_mailing_test_equals_reality(self):
""" Check that both test and real emails will format the qweb and inline
placeholders correctly in body and subject. """
mailing = self.test_mailing_bl.with_env(self.env)
mailing.write({
'body_html': '<p>Hello {{ object.name }} <t t-out="object.name"/></p>',
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
'contact_list_ids': [contact_list.id],
'subject': 'Subject {{ object.name }} <t t-out="object.name"/>',
})
mailing_test = self.env['mailing.mailing.test'].create({
'email_to': 'test@test.com',
@ -92,11 +131,13 @@ class TestMailingTest(TestMassMailCommon):
with self.mock_mail_gateway():
mailing_test.send_mail_test()
expected_subject = 'Subject Mitchell Admin <t t-out="object.name"/>'
expected_body = 'Hello {{ object.name }} Mitchell Admin'
expected_test_record = self.env[mailing.mailing_model_real].search([], limit=1)
self.assertEqual(expected_test_record, self.test_records[0], 'Should take first found one')
expected_subject = f'Subject {expected_test_record.name} <t t-out="object.name"/>'
expected_body = 'Hello {{ object.name }}' + f' {expected_test_record.name}'
self.assertSentEmail(self.env.user.partner_id, ['test@test.com'],
subject=expected_subject,
subject='[TEST] %s' % expected_subject,
body_content=expected_body)
with self.mock_mail_gateway():
@ -104,6 +145,77 @@ class TestMailingTest(TestMassMailCommon):
mailing.action_launch()
self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').method_direct_trigger()
self.assertSentEmail(self.env.user.partner_id, ['real@real.com'],
self.assertSentEmail(
self.env.user.partner_id,
[expected_test_record.email_from],
subject=expected_subject,
body_content=expected_body)
body_content=expected_body,
)
self.assertEqual(
self.env['mailing.mailing.test'].create({
'mass_mailing_id': mailing.id,
}).email_to,
'test@test.com',
"Should use the value of the previous record's email_to as default",
)
# Also test that related messages were properly deleted
mailing.subject = 'Dummy Subject'
with self.mock_mail_gateway(mail_unlink_sent=True):
mailing_test.send_mail_test()
test_subject = '[TEST] %s' % mailing.subject
self.assertSentEmail(
self.env.user.partner_id,
['test@test.com'],
subject=test_subject,
)
self.assertFalse(self.env['mail.mail'].search([('subject', '=', test_subject)]))
self.assertFalse(self.env['mail.message'].search([('subject', '=', test_subject)]))
@tagged('mailing_manage', 'twilio')
class TestMailingSMSTest(TestMassSMSCommon, MockSmsTwilioApi):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._setup_sms_twilio(cls.user_marketing.company_id)
def test_mass_sms_test_button_twilio(self):
""" Test the testing tool when twilio is activated on company """
self._setup_sms_twilio(self.user_marketing.company_id)
mailing = self.env['mailing.mailing'].create({
'name': 'TestButton',
'subject': 'Subject {{ object.name }}',
'preview': 'Preview {{ object.name }}',
'state': 'draft',
'mailing_type': 'sms',
'body_plaintext': 'Hello {{ object.name }}',
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
})
mailing_test = self.env['mailing.sms.test'].with_user(self.user_marketing).create({
'numbers': '+32456001122',
'mailing_id': mailing.id,
})
for error_type, exp_state, exp_msg in [
(False, 'outgoing', '<ul><li>Test SMS successfully sent to +32456001122</li></ul>'),
(
'wrong_number_format', 'outgoing', # not sure why outgoing but hey
"<ul><li>Test SMS could not be sent to +32456001122: The number you're trying to reach is not correctly formatted</li></ul>"
),
]:
with self.subTest(error_type=error_type):
with self.with_user('user_marketing'):
with self.mock_sms_twilio_gateway(error_type=error_type):
mailing_test.action_send_sms()
notification = mailing.message_ids[0]
self.assertEqual(notification.body, exp_msg)
self.assertSMS(
self.env["res.partner"], '+32456001122', exp_state,
)

View file

@ -46,8 +46,11 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
# runbot needs +51 compared to local
with self.assertQueryCount(__system__=1473, marketing=1474):
# runbot needs +101 compared to local
with (
self.mock_mail_gateway(mail_unlink_sent=True),
self.assertQueryCount(__system__=1379, marketing=1383), # 1227, 1229
):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
@ -89,12 +92,14 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
# runbot needs +51 compared to local
with self.assertQueryCount(__system__=1546, marketing=1547):
# runbot needs +153 compared to local
with self.assertQueryCount(__system__=1410, marketing=1417): # 1257, 1260
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
self.assertEqual(mailing.delivered, 50)
self.assertEqual(mailing.canceled, 12)
cancelled_mail_count = self.env['mail.mail'].sudo().search([('mailing_id', '=', mailing.id)])
self.assertEqual(len(cancelled_mail_count), 12, 'Should not have auto deleted the blacklisted emails')
mail_mail_count = len(self.env['mail.mail'].sudo().search([('mailing_id', '=', mailing.id)]))
self.assertEqual(mail_mail_count, 0,
"Mail_mail for blacklisted emails mustn't have been created and others must have been deleted")

View file

@ -0,0 +1,60 @@
from odoo.addons.test_mass_mailing.tests.common import TestMassSMSCommon
from odoo.tools import mute_logger
class TestSmsController(TestMassSMSCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.recipients = cls._create_mailing_sms_test_records(model='mail.test.sms', count=5)
cls.mailing_sms.mailing_domain = [('id', 'in', cls.recipients.ids)]
def _send_sms_immediately_and_assert_traces(self, moderated=False):
self.mailing_sms.sms_force_send = True
with self.mockSMSGateway(moderated=moderated):
self.mailing_sms.action_send_sms()
all_traces = self.assertSMSTraces(
[{'partner': record.customer_id,
'number': '+32' + record.phone_nbr[1:],
'trace_status': 'process' if moderated else 'pending',
} for i, record in enumerate(self.recipients)],
self.mailing_sms, self.recipients,
)
return all_traces
@mute_logger("odoo.addons.base.models.ir_http")
def test_webhook_update_traces_pending_to_sent(self):
all_traces = self._send_sms_immediately_and_assert_traces()
first_two_traces = all_traces[:2]
other_traces = all_traces[2:]
statuses = [{'sms_status': 'delivered', 'uuids': first_two_traces.sms_tracker_ids.mapped('sms_uuid')}]
self.assertEqual(self._make_webhook_jsonrpc_request(statuses), 'OK')
self.assertEqual(set(first_two_traces.mapped('trace_status')), {'sent'})
self.assertEqual(set(other_traces.mapped('trace_status')), {'pending'})
@mute_logger("odoo.addons.base.models.ir_http")
def test_webhook_update_traces_process_to_pending(self):
self.assertEqual(self.mailing_sms.state, 'draft')
all_traces = self._send_sms_immediately_and_assert_traces(moderated=True)
self.assertEqual(self.mailing_sms.state, 'sending')
statuses = [{'sms_status': 'sent', 'uuids': all_traces.sms_tracker_ids.mapped('sms_uuid')}]
self.assertEqual(self._make_webhook_jsonrpc_request(statuses), 'OK')
self.assertEqual(set(all_traces.mapped('trace_status')), {'pending'})
self.assertEqual(self.mailing_sms.state, 'done')
@mute_logger("odoo.addons.base.models.ir_http")
def test_webhook_update_traces_sent_to_bounce_and_failed(self):
all_traces = self._send_sms_immediately_and_assert_traces()
trace_1, trace_2 = all_traces[:2]
other_traces = all_traces[2:]
statuses = [
{'sms_status': 'invalid_destination', 'uuids': [trace_1.sms_tracker_ids.sms_uuid]},
{'sms_status': 'sms_not_delivered', 'uuids': [trace_2.sms_tracker_ids.sms_uuid]},
{'sms_status': 'delivered', 'uuids': other_traces.sms_tracker_ids.mapped('sms_uuid')}
]
self.assertEqual(self._make_webhook_jsonrpc_request(statuses), 'OK')
self.assertEqual(trace_1.trace_status, 'bounce')
self.assertEqual(trace_2.trace_status, 'error')
self.assertTrue(set(other_traces.mapped('trace_status')), {'sent'})