mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-22 07:42:02 +02:00
19.0 vanilla
This commit is contained in:
parent
5df8c07b59
commit
daa394e8b0
2114 changed files with 564841 additions and 299642 deletions
|
|
@ -5,3 +5,6 @@ from . import common
|
|||
from . import test_mailing_internals
|
||||
from . import test_mailing_retry
|
||||
from . import test_mailing_sms_ab_testing
|
||||
from . import test_mailing_ui
|
||||
from . import test_mailing_list
|
||||
from . import test_mailing_controllers
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import random
|
|||
import re
|
||||
import werkzeug
|
||||
|
||||
from odoo import tools
|
||||
from odoo.tools import mail
|
||||
from odoo.addons.link_tracker.tests.common import MockLinkTracker
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from odoo.addons.sms.tests.common import SMSCase, SMSCommon
|
||||
|
|
@ -22,7 +22,7 @@ class MassSMSCase(SMSCase, MockLinkTracker):
|
|||
return self.assertSMSTraces(recipients_info, mailing, records, check_sms=check_sms)
|
||||
|
||||
def assertSMSTraces(self, recipients_info, mailing, records,
|
||||
check_sms=True, sent_unlink=False,
|
||||
check_sms=True, is_cancel_not_sent=True, sent_unlink=False,
|
||||
sms_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
|
||||
|
|
@ -34,7 +34,8 @@ class MassSMSCase(SMSCase, MockLinkTracker):
|
|||
# TRACE
|
||||
'partner': res.partner record (may be empty),
|
||||
'number': number used for notification (may be empty, computed based on partner),
|
||||
'trace_status': outgoing / sent / cancel / bounce / error / opened (sent by default),
|
||||
'trace_status': outgoing / process / pending / sent / cancel / bounce / error / opened
|
||||
(sent by default),
|
||||
'record: linked record,
|
||||
# SMS.SMS
|
||||
'content': optional: if set, check content of sent SMS;
|
||||
|
|
@ -46,12 +47,16 @@ class MassSMSCase(SMSCase, MockLinkTracker):
|
|||
generated;
|
||||
:param records: records given to mailing that generated traces. It is
|
||||
used notably to find traces using their IDs;
|
||||
:param check_sms: if set, check sms.sms records that should be linked to traces;
|
||||
:param check_sms: if set, check sms.sms records that should be linked to traces
|
||||
unless not sent (trace_status == 'cancel');
|
||||
:param is_cancel_not_sent: if True, also check that no mail.message
|
||||
related to "cancel trace" have been created and disable check_sms for those.
|
||||
:param sent_unlink: it True, sent sms.sms are deleted and we check gateway
|
||||
output result instead of actual sms.sms records;
|
||||
:param sms_links_info: if given, should follow order of ``recipients_info``
|
||||
and give details about links. See ``assertLinkShortenedHtml`` helper for
|
||||
more details about content to give;
|
||||
more details about content to give
|
||||
Not tested for sms with trace status == 'cancel' if is_cancel_not_sent;
|
||||
]
|
||||
"""
|
||||
# map trace state to sms state
|
||||
|
|
@ -61,48 +66,102 @@ class MassSMSCase(SMSCase, MockLinkTracker):
|
|||
'error': 'error',
|
||||
'cancel': 'canceled',
|
||||
'bounce': 'error',
|
||||
'process': 'process',
|
||||
'pending': 'pending',
|
||||
}
|
||||
traces = self.env['mailing.trace'].search([
|
||||
('mass_mailing_id', 'in', mailing.ids),
|
||||
('res_id', 'in', records.ids)
|
||||
])
|
||||
debug_info = '\n'.join(
|
||||
f'Trace: to {t.sms_number} - state {t.trace_status} - res_id {t.res_id}'
|
||||
for t in traces
|
||||
)
|
||||
|
||||
traces_info = []
|
||||
for trace in traces:
|
||||
record = records.filtered(lambda r: r.id == trace.res_id)
|
||||
if record:
|
||||
traces_info.append(
|
||||
f'Trace: doc {trace.res_id} on {trace.sms_number} - status {trace.trace_status} (rec {record.id})'
|
||||
)
|
||||
else:
|
||||
traces_info.append(
|
||||
f'Trace: doc {trace.res_id} on {trace.sms_number} - status {trace.trace_status}'
|
||||
)
|
||||
debug_info = '\n'.join(traces_info)
|
||||
self.assertTrue(all(s.model == records._name for s in traces))
|
||||
# self.assertTrue(all(s.utm_campaign_id == mailing.campaign_id for s in traces))
|
||||
self.assertEqual(set(s.res_id for s in traces), set(records.ids))
|
||||
self.assertEqual(
|
||||
{s.res_id for s in traces}, set(records.ids),
|
||||
f'Should find one trace / record. Found\n{debug_info}'
|
||||
)
|
||||
|
||||
# check each trace
|
||||
if not sms_links_info:
|
||||
sms_links_info = [None] * len(recipients_info)
|
||||
for recipient_info, link_info, record in zip(recipients_info, sms_links_info, records):
|
||||
# check input
|
||||
invalid = set(recipient_info.keys()) - {
|
||||
'content',
|
||||
'record',
|
||||
# recipient
|
||||
'number',
|
||||
'partner',
|
||||
# trace info
|
||||
'failure_type',
|
||||
'trace_status',
|
||||
# check control
|
||||
'check_sms',
|
||||
}
|
||||
if invalid:
|
||||
raise AssertionError(f"assertSMSTraces: invalid input {invalid}")
|
||||
|
||||
# recipients
|
||||
partner = recipient_info.get('partner', self.env['res.partner'])
|
||||
number = recipient_info.get('number')
|
||||
status = recipient_info.get('trace_status', 'outgoing')
|
||||
content = recipient_info.get('content', None)
|
||||
if number is None and partner:
|
||||
number = partner._sms_get_recipients_info()[partner.id]['sanitized']
|
||||
# trace
|
||||
status = recipient_info.get('trace_status', 'outgoing')
|
||||
failure_type = recipient_info['failure_type'] if status in ('error', 'cancel', 'bounce') else None
|
||||
# content
|
||||
content = recipient_info.get('content', None)
|
||||
record = record or recipient_info.get('record')
|
||||
# checks
|
||||
recipient_check_sms = recipient_info.get('check_sms', check_sms)
|
||||
|
||||
trace = traces.filtered(
|
||||
lambda t: t.sms_number == number and t.trace_status == status and (t.res_id == record.id if record else True)
|
||||
)
|
||||
self.assertTrue(len(trace) == 1,
|
||||
'SMS: found %s notification for number %s, (status: %s) (1 expected)' % (len(trace), number, status))
|
||||
self.assertTrue(bool(trace.sms_sms_id_int))
|
||||
self.assertTrue(
|
||||
len(trace) == 1,
|
||||
'SMS: found %s notification for number %s (res_id: %s) (status: %s) (1 expected)\n--MOCKED DATA\n%s' % (
|
||||
len(trace), number, record.id,
|
||||
status, debug_info
|
||||
)
|
||||
)
|
||||
sms_not_created = is_cancel_not_sent and trace.trace_status == 'cancel'
|
||||
self.assertTrue(sms_not_created or bool(trace.sms_id_int))
|
||||
if sms_not_created:
|
||||
self.assertFalse(trace.sms_id_int)
|
||||
self.assertFalse(self.env['mail.message'].sudo().search(
|
||||
[('model', '=', record._name), ('res_id', '=', record.id),
|
||||
('id', 'in', self._new_sms.mail_message_id.ids)]))
|
||||
|
||||
if check_sms:
|
||||
if status == 'sent':
|
||||
if recipient_check_sms and not sms_not_created:
|
||||
if status in {'process', 'pending', 'sent'}:
|
||||
if sent_unlink:
|
||||
self.assertSMSIapSent([number], content=content)
|
||||
else:
|
||||
self.assertSMS(partner, number, 'sent', content=content)
|
||||
self.assertSMS(partner, number, status, content=content)
|
||||
elif status in state_mapping:
|
||||
sms_state = state_mapping[status]
|
||||
failure_type = recipient_info['failure_type'] if status in ('error', 'cancel', 'bounce') else None
|
||||
self.assertSMS(partner, number, sms_state, failure_type=failure_type, content=content)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
if link_info:
|
||||
if link_info and not sms_not_created:
|
||||
# shortened links are directly included in sms.sms record as well as
|
||||
# in sent sms (not like mails who are post-processed)
|
||||
sms_sent = self._find_sms_sent(partner, number)
|
||||
|
|
@ -123,30 +182,62 @@ class MassSMSCase(SMSCase, MockLinkTracker):
|
|||
(url, is_shortened),
|
||||
link_params=link_params,
|
||||
)
|
||||
return traces
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# GATEWAY TOOLS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def gateway_sms_click(self, mailing, record):
|
||||
def gateway_sms_bounce(self, mailing, records, error_code='invalid_destination'):
|
||||
""" Bounce SMS through sms/status controller """
|
||||
traces = mailing.mailing_trace_ids.filtered(lambda t: t.model == records._name and t.res_id in records.ids)
|
||||
statuses = [{
|
||||
'sms_status': error_code,
|
||||
'uuids': traces.sms_tracker_ids.mapped('sms_uuid'),
|
||||
}]
|
||||
with self.with_user(self.user_admin.login):
|
||||
self._make_webhook_jsonrpc_request(statuses)
|
||||
|
||||
def gateway_sms_click(self, mailing, record, use_sent_sms=True):
|
||||
""" Simulate a click on a sent SMS. Usage: giving a partner and/or
|
||||
a number, find an SMS sent to him, find shortened links in its body
|
||||
and call add_click to simulate a click. """
|
||||
trace = mailing.mailing_trace_ids.filtered(lambda t: t.model == record._name and t.res_id == record.id)
|
||||
sms_sent = self._find_sms_sent(self.env['res.partner'], trace.sms_number)
|
||||
self.assertTrue(bool(sms_sent))
|
||||
return self.gateway_sms_sent_click(sms_sent)
|
||||
if use_sent_sms:
|
||||
sms_sent = self._find_sms_sent(self.env['res.partner'], trace.sms_number)
|
||||
self.assertTrue(bool(sms_sent))
|
||||
return self.gateway_sms_sent_click(sms_sent)
|
||||
sms_sms = self._find_sms_sms(record._mail_get_partners()[record.id], trace.sms_number, 'outgoing')
|
||||
self.assertTrue(bool(sms_sms))
|
||||
with self.with_user(self.user_admin.login):
|
||||
return self.gateway_sms_sms_click(sms_sms)
|
||||
|
||||
def gateway_sms_delivered(self, mailing, records):
|
||||
""" Simulate a delivery report received for a sent SMS."""
|
||||
traces = mailing.mailing_trace_ids.filtered(lambda t: t.model == records._name and t.res_id in records.ids)
|
||||
with self.with_user(self.user_admin.login):
|
||||
statuses = [{
|
||||
'sms_status': 'delivered',
|
||||
'uuids': traces.with_user(self.user_admin).sms_tracker_ids.mapped('sms_uuid'),
|
||||
}]
|
||||
self._make_webhook_jsonrpc_request(statuses)
|
||||
|
||||
def gateway_sms_sent_click(self, sms_sent):
|
||||
return self._gateway_sms_click(sms_sent['body'])
|
||||
|
||||
def gateway_sms_sms_click(self, sms_sms):
|
||||
return self._gateway_sms_click(sms_sms.body)
|
||||
|
||||
def _gateway_sms_click(self, body):
|
||||
""" When clicking on a link in a SMS we actually don't have any
|
||||
easy information in body, only body. We currently click on all found
|
||||
shortened links. """
|
||||
for url in re.findall(tools.TEXT_URL_REGEX, sms_sent['body']):
|
||||
for url in re.findall(mail.TEXT_URL_REGEX, body):
|
||||
if '/r/' in url: # shortened link, like 'http://localhost:8069/r/LBG/s/53'
|
||||
parsed_url = werkzeug.urls.url_parse(url)
|
||||
path_items = parsed_url.path.split('/')
|
||||
code, sms_sms_id = path_items[2], int(path_items[4])
|
||||
trace_id = self.env['mailing.trace'].sudo().search([('sms_sms_id_int', '=', sms_sms_id)]).id
|
||||
code, sms_id_int = path_items[2], int(path_items[4])
|
||||
trace_id = self.env['mailing.trace'].sudo().search([('sms_id_int', '=', sms_id_int)]).id
|
||||
|
||||
self.env['link.tracker.click'].sudo().add_click(
|
||||
code,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
from odoo.tests.common import users
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools.urls import urljoin as url_join
|
||||
from odoo.addons.mass_mailing_sms.tests.common import MassSMSCommon
|
||||
|
||||
|
||||
@tagged('mailing_portal', 'post_install', '-at_install')
|
||||
class TestMailingListSms(MassSMSCommon):
|
||||
|
||||
@users('user_marketing')
|
||||
def test_controller_unsubscribe(self):
|
||||
""" Test unsubscribe controller from a phone number, including phone
|
||||
formatting and validation """
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Test Partner',
|
||||
'phone': '+91 1234657890',
|
||||
})
|
||||
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestMailing',
|
||||
'body_plaintext': 'Coucou hibou',
|
||||
'mailing_type': 'sms',
|
||||
'mailing_model_id': self.env['ir.model']._get_id('res.partner'),
|
||||
'mailing_domain': [('id', '=', partner.id)],
|
||||
'subject': 'Test',
|
||||
'sms_allow_unsubscribe': True,
|
||||
})
|
||||
mailing.action_send_sms()
|
||||
|
||||
trace = mailing.mailing_trace_ids.filtered(lambda t: t.res_id == partner.id)
|
||||
self.assertTrue(trace, 'Trace not found for the partner')
|
||||
self.assertEqual(trace.sms_number, '+911234657890')
|
||||
|
||||
unsubscribe_url = url_join(mailing.get_base_url(), f'/sms/{mailing.id}/unsubscribe/{trace.sms_code}')
|
||||
response = self.url_open(url=unsubscribe_url, data={'sms_number': trace.sms_number}, method='GET')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(partner.phone_blacklisted, 'Partner not unsubscribed')
|
||||
|
|
@ -35,8 +35,7 @@ class TestMassMailValues(MassSMSCommon):
|
|||
self.assertEqual(mailing.medium_id, self.env.ref('mass_mailing_sms.utm_medium_sms'))
|
||||
self.assertEqual(mailing.mailing_model_name, 'res.partner')
|
||||
self.assertEqual(mailing.mailing_model_real, 'res.partner')
|
||||
# default for partner: remove blacklisted
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('phone_sanitized_blacklisted', '=', False)])
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [])
|
||||
# update template -> update body
|
||||
mailing.write({'sms_template_id': self.sms_template_partner.id})
|
||||
self.assertEqual(mailing.body_plaintext, self.sms_template_partner.body)
|
||||
|
|
@ -81,7 +80,7 @@ class TestMassMailValues(MassSMSCommon):
|
|||
|
||||
expected = {
|
||||
'link': f'{base_url}/r/xxx{"x" if link_trackers else ""}/s/xxxxx',
|
||||
'unsubscribe': f"\nSTOP SMS : {base_url}/sms/{'x' * len(str(mailing.id))}/{'x' * self.env['mailing.trace'].CODE_SIZE}",
|
||||
'unsubscribe': f"\nSTOP SMS: {base_url}/sms/{'x' * len(str(mailing.id))}/{'x' * self.env['mailing.trace'].CODE_SIZE}",
|
||||
}
|
||||
self.assertDictEqual(mailing.get_sms_link_replacements_placeholders(), expected)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mass_mailing_sms.tests.common import MassSMSCommon
|
||||
from odoo.tests import Form, users
|
||||
|
||||
|
||||
class TestMailingListSms(MassSMSCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailingListSms, cls).setUpClass()
|
||||
cls._create_mailing_list()
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_list_action_send_sms(self):
|
||||
sms_ctx = self.mailing_list_1.action_send_mailing_sms().get('context', {})
|
||||
form = Form(self.env['mailing.mailing'].with_context(sms_ctx))
|
||||
form.sms_subject = 'Test SMS'
|
||||
form.body_plaintext = 'Test sms body'
|
||||
sms = form.save()
|
||||
# Check that mailing model and mailing list are set properly
|
||||
self.assertEqual(
|
||||
sms.mailing_model_id, self.env['ir.model']._get('mailing.list'),
|
||||
'Should have correct mailing model set')
|
||||
self.assertEqual(sms.contact_list_ids, self.mailing_list_1, 'Should have correct mailing list set')
|
||||
self.assertEqual(sms.mailing_type, 'sms', 'Should have correct mailing_type')
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import HttpCase, tagged, users
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'mail_activity')
|
||||
class TestMailingUi(HttpCase):
|
||||
|
||||
@users('admin')
|
||||
def test_tour_mailing_activities_split(self):
|
||||
""" Activities linked to mailing.mailing records can appear either in the
|
||||
'Email Marketing', either in the 'SMS Marketing' category, depending on
|
||||
the value of the field mailing_type of the record it is linked to. This
|
||||
test ensures that:
|
||||
- activities linked to records with mailing_type set to mail are listed
|
||||
in the 'Email Marketing' category
|
||||
- activities linked to records with mailing_type set to sms are listed
|
||||
in the 'SMS Marketing' category
|
||||
"""
|
||||
user_admin = self.env.ref('base.user_admin')
|
||||
user_admin.write({
|
||||
'email': 'mitchell.admin@example.com',
|
||||
})
|
||||
sms_rec, email_rec = self.env['mailing.mailing'].create([
|
||||
{
|
||||
'body_plaintext': 'Some sms spam',
|
||||
'mailing_type': 'sms',
|
||||
'name': 'SMS record with an activity',
|
||||
'subject': 'New SMS!',
|
||||
}, {
|
||||
'body_html': '<p>Some email spam</p>',
|
||||
'mailing_type': 'mail',
|
||||
'name': 'Email record with an activity',
|
||||
'subject': 'New Email!',
|
||||
}
|
||||
])
|
||||
|
||||
sms_rec.activity_schedule(act_type_xmlid='mail.mail_activity_data_todo', user_id=user_admin.id)
|
||||
email_rec.activity_schedule(act_type_xmlid='mail.mail_activity_data_todo', user_id=user_admin.id)
|
||||
|
||||
# Ensure that both activities appear in the systray and that clicking on
|
||||
# one activity opens a view where the other activity isn't listed
|
||||
self.start_tour("/odoo", 'mailing_activities_split', login="admin")
|
||||
Loading…
Add table
Add a link
Reference in a new issue