mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-21 10:42:01 +02:00
19.0 vanilla
This commit is contained in:
parent
5df8c07b59
commit
daa394e8b0
2114 changed files with 564841 additions and 299642 deletions
|
|
@ -1,24 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from contextlib import contextmanager
|
||||
from freezegun import freeze_time
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import exceptions, tools
|
||||
from odoo.tests import common
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.addons.sms.models.sms_api import SmsApi
|
||||
from odoo.addons.sms.models.sms_sms import SmsSms
|
||||
from odoo.addons.sms.tools.sms_api import SmsApi
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class MockSMS(common.BaseCase):
|
||||
class MockSMS(common.HttpCase):
|
||||
|
||||
def tearDown(self):
|
||||
super(MockSMS, self).tearDown()
|
||||
self._clear_sms_sent()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# UTILITY MOCKS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@contextmanager
|
||||
def mockSMSGateway(self, sms_allow_unlink=False, sim_error=None, nbr_t_error=None):
|
||||
def mock_datetime_and_now(self, mock_dt):
|
||||
""" Used when synchronization date (using env.cr.now()) is important
|
||||
in addition to standard datetime mocks. Used mainly to detect sync
|
||||
issues. """
|
||||
with freeze_time(mock_dt), \
|
||||
patch.object(self.env.cr, 'now', lambda: mock_dt):
|
||||
yield
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# GATEWAY MOCK
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@contextmanager
|
||||
def mockSMSGateway(self, sms_allow_unlink=False, sim_error=None, nbr_t_error=None, moderated=False, force_delivered=False):
|
||||
self._clear_sms_sent()
|
||||
sms_create_origin = SmsSms.create
|
||||
sms_send_origin = SmsSms._send
|
||||
|
|
@ -35,27 +53,52 @@ class MockSMS(common.BaseCase):
|
|||
if local_endpoint == '/iap/sms/2/send':
|
||||
result = []
|
||||
for to_send in params['messages']:
|
||||
res = {'res_id': to_send['res_id'], 'state': 'success', 'credit': 1}
|
||||
res = {'res_id': to_send['res_id'], 'state': 'delivered' if force_delivered else 'success', 'credit': 1}
|
||||
error = sim_error or (nbr_t_error and nbr_t_error.get(to_send['number']))
|
||||
if error and error == 'credit':
|
||||
res.update(credit=0, state='insufficient_credit')
|
||||
elif error and error == 'wrong_number_format':
|
||||
res.update(state='wrong_number_format')
|
||||
elif error and error == 'unregistered':
|
||||
res.update(state='unregistered')
|
||||
elif error and error == 'server_error':
|
||||
res.update(state='server_error')
|
||||
elif error and error in {'wrong_number_format', 'unregistered', 'server_error'}:
|
||||
res.update(state=error)
|
||||
elif error and error == 'jsonrpc_exception':
|
||||
raise exceptions.AccessError(
|
||||
'The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was ' + local_endpoint
|
||||
)
|
||||
result.append(res)
|
||||
if res['state'] == 'success':
|
||||
if res['state'] == 'success' or res['state'] == 'delivered':
|
||||
self._sms.append({
|
||||
'number': to_send['number'],
|
||||
'body': to_send['content'],
|
||||
})
|
||||
return result
|
||||
elif local_endpoint == '/api/sms/3/send':
|
||||
result = []
|
||||
for message in params['messages']:
|
||||
for number in message["numbers"]:
|
||||
error = sim_error or (nbr_t_error and nbr_t_error.get(number['number']))
|
||||
if error == 'jsonrpc_exception':
|
||||
raise exceptions.AccessError(
|
||||
'The url that this service requested returned an error. '
|
||||
'Please contact the author of the app. '
|
||||
'The url it tried to contact was ' + local_endpoint
|
||||
)
|
||||
elif error == 'credit':
|
||||
error = 'insufficient_credit'
|
||||
res = {
|
||||
'uuid': number['uuid'],
|
||||
'state': error or ('delivered' if force_delivered else 'success' if not moderated else 'processing'),
|
||||
'credit': 1,
|
||||
}
|
||||
if error:
|
||||
# credit is only given if the amount is known
|
||||
res.update(credit=0)
|
||||
else:
|
||||
self._sms.append({
|
||||
'number': number['number'],
|
||||
'body': message['content'],
|
||||
'uuid': number['uuid'],
|
||||
})
|
||||
result.append(res)
|
||||
return result
|
||||
|
||||
def _sms_sms_create(model, *args, **kwargs):
|
||||
res = sms_create_origin(model, *args, **kwargs)
|
||||
|
|
@ -65,23 +108,20 @@ class MockSMS(common.BaseCase):
|
|||
def _sms_sms_send(records, unlink_failed=False, unlink_sent=True, raise_exception=False):
|
||||
if sms_allow_unlink:
|
||||
return sms_send_origin(records, unlink_failed=unlink_failed, unlink_sent=unlink_sent, raise_exception=raise_exception)
|
||||
else:
|
||||
return sms_send_origin(records, unlink_failed=False, unlink_sent=False, raise_exception=raise_exception)
|
||||
return True
|
||||
return sms_send_origin(records, unlink_failed=False, unlink_sent=False, raise_exception=raise_exception)
|
||||
|
||||
try:
|
||||
with patch.object(SmsApi, '_contact_iap', side_effect=_contact_iap), \
|
||||
patch.object(SmsSms, 'create', autospec=True, wraps=SmsSms, side_effect=_sms_sms_create), \
|
||||
patch.object(SmsSms, '_send', autospec=True, wraps=SmsSms, side_effect=_sms_sms_send):
|
||||
yield
|
||||
finally:
|
||||
pass
|
||||
with patch.object(SmsApi, '_contact_iap', side_effect=_contact_iap) as _sms_api_contact_iap_mock, \
|
||||
patch.object(SmsSms, 'create', autospec=True, wraps=SmsSms, side_effect=_sms_sms_create) as sms_create, \
|
||||
patch.object(SmsSms, '_send', autospec=True, wraps=SmsSms, side_effect=_sms_sms_send):
|
||||
self._sms_api_contact_iap_mock = _sms_api_contact_iap_mock
|
||||
self._mock_sms_create = sms_create
|
||||
yield
|
||||
|
||||
def _clear_sms_sent(self):
|
||||
self._sms = []
|
||||
self._new_sms = self.env['sms.sms'].sudo()
|
||||
|
||||
def _clear_outoing_sms(self):
|
||||
def _clear_outgoing_sms(self):
|
||||
""" As SMS gateway mock keeps SMS, we may need to remove them manually
|
||||
if there are several tests in the same tx. """
|
||||
self.env['sms.sms'].sudo().search([('state', '=', 'outgoing')]).unlink()
|
||||
|
|
@ -91,17 +131,27 @@ class SMSCase(MockSMS):
|
|||
""" Main test class to use when testing SMS integrations. Contains helpers and tools related
|
||||
to notification sent by SMS. """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# This is called to make sure that an iap_account for sms already exists or if not is created.
|
||||
cls.env['iap.account'].get('sms')
|
||||
|
||||
def _find_sms_sent(self, partner, number):
|
||||
if number is None and partner:
|
||||
number = partner.phone_get_sanitized_number()
|
||||
number = partner._phone_format()
|
||||
sent_sms = next((sms for sms in self._sms if sms['number'] == number), None)
|
||||
if not sent_sms:
|
||||
raise AssertionError('sent sms not found for %s (number: %s)' % (partner, number))
|
||||
debug_info = '\n'.join(
|
||||
f"To {sms['number']}"
|
||||
for sms in self._sms
|
||||
)
|
||||
raise AssertionError(f'sent sms not found for {partner} (number: {number})\n{debug_info}')
|
||||
return sent_sms
|
||||
|
||||
def _find_sms_sms(self, partner, number, status):
|
||||
def _find_sms_sms(self, partner, number, status, content=None):
|
||||
if number is None and partner:
|
||||
number = partner.phone_get_sanitized_number()
|
||||
number = partner._phone_format()
|
||||
domain = [('id', 'in', self._new_sms.ids),
|
||||
('partner_id', '=', partner.id),
|
||||
('number', '=', number)]
|
||||
|
|
@ -109,19 +159,27 @@ class SMSCase(MockSMS):
|
|||
domain += [('state', '=', status)]
|
||||
|
||||
sms = self.env['sms.sms'].sudo().search(domain)
|
||||
if len(sms) > 1 and content:
|
||||
sms = sms.filtered(lambda s: content in (s.body or ""))
|
||||
if not sms:
|
||||
raise AssertionError('sms.sms not found for %s (number: %s / status %s)' % (partner, number, status))
|
||||
debug_info = '\n'.join(
|
||||
f"To {sms.number} ({sms.partner_id}) / state {sms.state}"
|
||||
for sms in self._new_sms
|
||||
)
|
||||
raise AssertionError(
|
||||
f'sms.sms not found for {partner} (number: {number} / status {status})\n--MOCKED DATA\n{debug_info}'
|
||||
)
|
||||
if len(sms) > 1:
|
||||
raise NotImplementedError()
|
||||
raise NotImplementedError(
|
||||
f'Found {len(sms)} sms.sms for {partner} (number: {number} / status {status})'
|
||||
)
|
||||
return sms
|
||||
|
||||
def assertNoSMS(self):
|
||||
""" Check no sms went through gateway during mock. """
|
||||
self.assertTrue(len(self._new_sms) == 0)
|
||||
|
||||
def assertSMSIapSent(self, numbers, content=None):
|
||||
""" Check sent SMS. Order is not checked. Each number should have received
|
||||
the same content. Useful to check batch sending.
|
||||
""" Check sent SMS (to IAP, but other providers like twilio should be
|
||||
mocked to fill up 'self._sms', allowing tests to pass). Order is not
|
||||
checked. Each number should have received the same content. Useful to
|
||||
check batch sending.
|
||||
|
||||
:param numbers: list of numbers;
|
||||
:param content: content to check for each number;
|
||||
|
|
@ -132,10 +190,6 @@ class SMSCase(MockSMS):
|
|||
if content is not None:
|
||||
self.assertIn(content, sent_sms['body'])
|
||||
|
||||
def assertSMSSent(self, numbers, content=None):
|
||||
""" Deprecated. Remove in 14.4 """
|
||||
return self.assertSMSIapSent(numbers, content=content)
|
||||
|
||||
def assertSMS(self, partner, number, status, failure_type=None,
|
||||
content=None, fields_values=None):
|
||||
""" Find a ``sms.sms`` record, based on given partner, number and status.
|
||||
|
|
@ -149,16 +203,16 @@ class SMSCase(MockSMS):
|
|||
:param fields_values: optional values allowing to check directly some
|
||||
values on ``sms.sms`` record;
|
||||
"""
|
||||
sms_sms = self._find_sms_sms(partner, number, status)
|
||||
sms_sms = self._find_sms_sms(partner, number, status, content=content)
|
||||
if failure_type:
|
||||
self.assertEqual(sms_sms.failure_type, failure_type)
|
||||
if content is not None:
|
||||
self.assertIn(content, sms_sms.body)
|
||||
self.assertIn(content, (sms_sms.body or ""))
|
||||
for fname, fvalue in (fields_values or {}).items():
|
||||
self.assertEqual(
|
||||
sms_sms[fname], fvalue,
|
||||
'SMS: expected %s for %s, got %s' % (fvalue, fname, sms_sms[fname]))
|
||||
if status == 'sent':
|
||||
if status == 'pending':
|
||||
self.assertSMSIapSent([sms_sms.number], content=content)
|
||||
|
||||
def assertSMSCanceled(self, partner, number, failure_type, content=None, fields_values=None):
|
||||
|
|
@ -190,7 +244,7 @@ class SMSCase(MockSMS):
|
|||
:param recipients_info: list[{
|
||||
'partner': res.partner record (may be empty),
|
||||
'number': number used for notification (may be empty, computed based on partner),
|
||||
'state': ready / sent / exception / canceled (sent by default),
|
||||
'state': ready / pending / sent / exception / canceled (pending by default),
|
||||
'failure_type': optional: sms_number_missing / sms_number_format / sms_credit / sms_server
|
||||
}, { ... }]
|
||||
:param content: SMS content
|
||||
|
|
@ -213,31 +267,58 @@ class SMSCase(MockSMS):
|
|||
self.assertEqual(notifications.mapped('res_partner_id'), partners)
|
||||
|
||||
for recipient_info in recipients_info:
|
||||
# sanity check
|
||||
extra_keys = recipient_info.keys() - {
|
||||
# notification
|
||||
'failure_reason',
|
||||
'failure_type',
|
||||
'state',
|
||||
# sms
|
||||
'sms_fields_values',
|
||||
# recipient
|
||||
'number',
|
||||
'partner',
|
||||
'recipient_check_sms',
|
||||
}
|
||||
if extra_keys:
|
||||
raise ValueError(f'Unsupported values: {extra_keys}')
|
||||
|
||||
partner = recipient_info.get('partner', self.env['res.partner'])
|
||||
number = recipient_info.get('number')
|
||||
state = recipient_info.get('state', 'sent')
|
||||
state = recipient_info.get('state', 'pending')
|
||||
if number is None and partner:
|
||||
number = partner.phone_get_sanitized_number()
|
||||
number = partner._phone_format()
|
||||
|
||||
notif = notifications.filtered(lambda n: n.res_partner_id == partner and n.sms_number == number and n.notification_status == state)
|
||||
self.assertTrue(notif, 'SMS: not found notification for %s (number: %s, state: %s)' % (partner, number, state))
|
||||
|
||||
debug_info = ''
|
||||
if not notif:
|
||||
debug_info = '\n'.join(
|
||||
f'To: {notif.sms_number} ({notif.res_partner_id}) - (State: {notif.notification_status})'
|
||||
for notif in notifications
|
||||
)
|
||||
self.assertTrue(notif, 'SMS: not found notification for %s (number: %s, state: %s)\n%s' % (partner, number, state, debug_info))
|
||||
self.assertEqual(notif.author_id, notif.mail_message_id.author_id, 'SMS: Message and notification should have the same author')
|
||||
for field_name, expected_value in (mail_message_values or {}).items():
|
||||
self.assertEqual(notif.mail_message_id[field_name], expected_value)
|
||||
if state not in ('sent', 'ready', 'canceled'):
|
||||
if 'failure_reason' in recipient_info:
|
||||
self.assertEqual(notif.failure_reason, recipient_info['failure_reason'])
|
||||
if state not in {'process', 'sent', 'ready', 'canceled', 'pending'}:
|
||||
self.assertEqual(notif.failure_type, recipient_info['failure_type'])
|
||||
if check_sms:
|
||||
if state == 'sent':
|
||||
|
||||
if recipient_info.get('recipient_check_sms', check_sms):
|
||||
fields_values = recipient_info.get('sms_fields_values') or {}
|
||||
if state in {'process', 'pending', 'sent'}:
|
||||
if sent_unlink:
|
||||
self.assertSMSIapSent([number], content=content)
|
||||
else:
|
||||
self.assertSMS(partner, number, 'sent', content=content)
|
||||
self.assertSMS(partner, number, state, content=content, fields_values=fields_values)
|
||||
elif state == 'ready':
|
||||
self.assertSMS(partner, number, 'outgoing', content=content)
|
||||
self.assertSMS(partner, number, 'outgoing', content=content, fields_values=fields_values)
|
||||
elif state == 'exception':
|
||||
self.assertSMS(partner, number, 'error', failure_type=recipient_info['failure_type'], content=content)
|
||||
self.assertSMS(partner, number, 'error', failure_type=recipient_info['failure_type'], content=content, fields_values=fields_values)
|
||||
elif state == 'canceled':
|
||||
self.assertSMS(partner, number, 'canceled', failure_type=recipient_info['failure_type'], content=content)
|
||||
self.assertSMS(partner, number, 'canceled', failure_type=recipient_info['failure_type'], content=content, fields_values=fields_values)
|
||||
else:
|
||||
raise NotImplementedError('Not implemented')
|
||||
|
||||
|
|
@ -260,11 +341,7 @@ class SMSCommon(MailCommon, SMSCase):
|
|||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(SMSCommon, cls).setUpClass()
|
||||
cls.user_employee.write({'login': 'employee'})
|
||||
|
||||
# update country to belgium in order to test sanitization of numbers
|
||||
cls.user_employee.company_id.write({'country_id': cls.env.ref('base.be').id})
|
||||
super().setUpClass()
|
||||
|
||||
# some numbers for testing
|
||||
cls.random_numbers_str = '+32456998877, 0456665544'
|
||||
|
|
@ -284,3 +361,6 @@ class SMSCommon(MailCommon, SMSCase):
|
|||
'model_id': cls.env['ir.model']._get(model).id,
|
||||
'body': body if body else 'Dear {{ object.display_name }} this is an SMS.'
|
||||
})
|
||||
|
||||
def _make_webhook_jsonrpc_request(self, statuses):
|
||||
return self.make_jsonrpc_request('/sms/status', {'message_statuses': statuses})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue