250 lines
9.5 KiB
Python
250 lines
9.5 KiB
Python
import re
|
|
|
|
from contextlib import contextmanager
|
|
from requests import Response
|
|
from unittest.mock import patch
|
|
|
|
from odoo.addons.mail.tests.common import mail_new_test_user
|
|
from odoo.addons.sms.models.sms_sms import SmsSms
|
|
from odoo.addons.sms.tests.common import SMSCase
|
|
from odoo.addons.sms_twilio.tools import sms_twilio as twilio_tools
|
|
from odoo.addons.sms_twilio.tools.sms_api import SmsApiTwilio
|
|
from odoo.tests.common import TransactionCase
|
|
|
|
|
|
class MockSmsTwilioApi(SMSCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
# some test data
|
|
cls.twilio_valid_phone_number = "+12202154155"
|
|
cls.twilio_invalid_phone_number = "+3212312312"
|
|
|
|
# mock control
|
|
cls.mock_error_type = False
|
|
cls.mock_error_number_to_type = {}
|
|
cls.mock_body = False
|
|
cls.mock_company = cls.env.company
|
|
cls.mock_number = False
|
|
cls.mock_sms_uuid = 'NA'
|
|
|
|
# find details of outgoing requests
|
|
cls.twilio_request_re = re.compile(r"https://api.twilio.com/2010-04-01/Accounts/(AC[\d]{32})/(.*)")
|
|
|
|
# typical / expected responses
|
|
cls.webhook_ok_response = {
|
|
'AccountSid': 'ACfake',
|
|
'ApiVersion': '2010-04-01',
|
|
'From': '+12212341234',
|
|
'MessageSid': 'SMfake',
|
|
'MessageStatus': 'delivered',
|
|
'RawDlrDoneDate': '2504241615',
|
|
'SmsSid': 'SMfake',
|
|
'SmsStatus': 'delivered',
|
|
'To': '+32486321321',
|
|
}
|
|
cls.request_send_ok_json = {
|
|
"account_sid": "AC12345678987654321234567898765432",
|
|
"api_version": "2010-04-01",
|
|
"date_created": "Mon, 14 Apr 2025 09:27:41 +0000",
|
|
"date_sent": None,
|
|
"date_updated": "Mon, 14 Apr 2025 09:27:41 +0000",
|
|
"direction": "outbound-api",
|
|
"error_code": None,
|
|
"error_message": None,
|
|
"from": "+12212341234",
|
|
"messaging_service_sid": None,
|
|
"num_media": "0",
|
|
"num_segments": "1",
|
|
"price": None,
|
|
"price_unit": "USD",
|
|
"sid": "SMfake",
|
|
"status": "queued",
|
|
"subresource_uris": {
|
|
"media": "/2010-04-01/Accounts/ACfake/Messages/SMfake/Media.json"
|
|
},
|
|
"uri": "/2010-04-01/Accounts/ACfake/Messages/SMfake.json",
|
|
}
|
|
cls.request_send_nok_json = {
|
|
'code': 21211,
|
|
'more_info': 'https://www.twilio.com/docs/errors/21211',
|
|
'status': 400,
|
|
}
|
|
|
|
@classmethod
|
|
def _request_handler(cls, session, request, **kwargs):
|
|
url = request.url
|
|
matching = cls.twilio_request_re.match(url)
|
|
if matching:
|
|
_sid = matching.group(1)
|
|
right_part = matching.group(2)
|
|
response = Response()
|
|
response.status_code = 200
|
|
if right_part == "IncomingPhoneNumbers.json":
|
|
response.json = lambda: {
|
|
'incoming_phone_numbers': [
|
|
{'phone_number': '+32455998877'},
|
|
{'phone_number': '+32455665544'},
|
|
],
|
|
}
|
|
return response
|
|
elif right_part == "Messages.json":
|
|
error_type = cls.mock_error_number_to_type.get(cls.mock_number) or cls.mock_error_type
|
|
if not error_type and not cls.mock_number:
|
|
error_type = "sms_number_missing"
|
|
error_codes = {
|
|
'wrong_number_format': 21211,
|
|
'sms_number_missing': 21604,
|
|
'twilio_acc_unverified': 21608,
|
|
'twilio_callback': 21609,
|
|
'unknown': 1,
|
|
'other': 1,
|
|
}
|
|
if not error_type:
|
|
request_send_ok_json = cls.request_send_ok_json.copy()
|
|
request_send_ok_json['body'] = cls.mock_body or 'body'
|
|
request_send_ok_json['sid'] = f'twilio_{cls.mock_company.name}_{cls.mock_sms_uuid}' if cls.mock_sms_uuid else 'SMFake'
|
|
request_send_ok_json['to_number'] = cls.mock_number or 'to_number'
|
|
response.json = lambda: request_send_ok_json
|
|
else:
|
|
if error_type not in error_codes:
|
|
raise ValueError('Unsupported error code')
|
|
error_code = error_codes.get(error_type) if error_type else False
|
|
|
|
request_send_nok_json = cls.request_send_nok_json.copy()
|
|
request_send_nok_json['body'] = cls.mock_body or 'body'
|
|
request_send_nok_json['code'] = error_code
|
|
request_send_nok_json['to_number'] = cls.mock_number or 'to_number'
|
|
response.json = lambda: request_send_nok_json
|
|
response.status_code = 400
|
|
return response
|
|
return super()._request_handler(session, request, **kwargs)
|
|
|
|
@classmethod
|
|
def _setup_sms_twilio(cls, company):
|
|
company.sudo().write({
|
|
"sms_provider": "twilio",
|
|
"sms_twilio_account_sid": "AC12345678987654321234567898765432",
|
|
"sms_twilio_auth_token": "grimgorironhide",
|
|
"sms_twilio_number_ids": [
|
|
(5, 0),
|
|
(0, 0, {
|
|
"country_id": cls.env.ref("base.be").id,
|
|
"number": "+32455998877",
|
|
"sequence": 0,
|
|
}),
|
|
(0, 0, {
|
|
"country_id": cls.env.ref("base.us").id,
|
|
"number": "+15056998877",
|
|
"sequence": 1,
|
|
}),
|
|
],
|
|
})
|
|
|
|
@classmethod
|
|
def _update_mock(cls, error_type=None, error_number_to_type=None,
|
|
body=None, number=False, sms_uuid=False,
|
|
company=False):
|
|
if error_type is not None:
|
|
cls.mock_error_type = error_type
|
|
if error_number_to_type is not None:
|
|
cls.mock_error_number_to_type = error_number_to_type
|
|
# various data, used notably to forge better simulated responses
|
|
if body is not None:
|
|
cls.mock_body = body
|
|
if company is not False:
|
|
cls.mock_company = company
|
|
if number is not False:
|
|
cls.mock_number = number
|
|
if sms_uuid is not False:
|
|
cls.mock_sms_uuid = sms_uuid
|
|
|
|
@contextmanager
|
|
def mock_sms_twilio_send(self, error_type=False, error_number_to_type=None):
|
|
self._clear_sms_sent()
|
|
self._update_mock(
|
|
error_type=error_type,
|
|
error_number_to_type=error_number_to_type,
|
|
company=self.env.company,
|
|
)
|
|
sms_twilio_send_request_origin = SmsApiTwilio._sms_twilio_send_request
|
|
|
|
def _sms_api_twilio_sms_twilio_send_request(model, *args, **kwargs):
|
|
(_session, to_number, body, uuid) = args
|
|
self._update_mock(
|
|
error_type=self.mock_error_type,
|
|
error_number_to_type=self.mock_error_number_to_type,
|
|
body=body, number=to_number, sms_uuid=uuid,
|
|
company=model.company,
|
|
)
|
|
res = sms_twilio_send_request_origin(model, *args, **kwargs)
|
|
self._sms += [{
|
|
'body': body,
|
|
'number': to_number,
|
|
'uuid': uuid,
|
|
}]
|
|
return res
|
|
|
|
with patch.object(SmsApiTwilio, '_sms_twilio_send_request', autospec=True, side_effect=_sms_api_twilio_sms_twilio_send_request) as _sms_twilio_send_mock:
|
|
self._sms_twilio_send_mock = _sms_twilio_send_mock
|
|
yield
|
|
|
|
@contextmanager
|
|
def mock_sms_twilio_gateway(self, error_type=False, error_number_to_type=None):
|
|
self._clear_sms_sent()
|
|
sms_create_origin = SmsSms.create
|
|
|
|
def _sms_sms_create(model, *args, **kwargs):
|
|
res = sms_create_origin(model, *args, **kwargs)
|
|
self._new_sms += res.sudo()
|
|
return res
|
|
|
|
with (
|
|
patch.object(SmsSms, 'create', autospec=True, wraps=SmsSms, side_effect=_sms_sms_create),
|
|
self.mock_sms_twilio_send(error_type=error_type, error_number_to_type=error_number_to_type),
|
|
):
|
|
yield
|
|
|
|
def simulate_sms_twilio_status(self, sms_batch, company):
|
|
""" Simulate callback webhook called by Twilio """
|
|
for sms in sms_batch:
|
|
expected_signature = twilio_tools.generate_twilio_sms_callback_signature(
|
|
self.user_admin.company_id,
|
|
sms.uuid,
|
|
self.webhook_ok_response,
|
|
)
|
|
_response = self.url_open(
|
|
f"/sms_twilio/status/{sms.uuid}", self.webhook_ok_response,
|
|
headers={
|
|
"X-Twilio-Signature": expected_signature,
|
|
},
|
|
)
|
|
|
|
|
|
class MockSmsTwilio(MockSmsTwilioApi, TransactionCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.user_admin = cls.env.ref('base.user_admin')
|
|
cls.company_admin = cls.user_admin.company_id
|
|
cls.basic_user = mail_new_test_user(
|
|
cls.env,
|
|
company_id=cls.company_admin.id,
|
|
country_id=cls.env.ref('base.be').id,
|
|
groups='base.group_user,base.group_partner_manager',
|
|
login='employee',
|
|
)
|
|
|
|
cls.valid_partner = cls.env['res.partner'].create({
|
|
'name': 'ValidPartner',
|
|
'phone': cls.twilio_valid_phone_number,
|
|
})
|
|
cls.invalid_partner = cls.env['res.partner'].create({
|
|
'name': 'InvalidPartner',
|
|
'phone': cls.twilio_invalid_phone_number,
|
|
})
|