odoo18/addons/sms_twilio/tests/common.py

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,
})