diff --git a/addons/sms/__init__.py b/addons/sms/__init__.py index 2ae6446f9dc..f7a591caed7 100644 --- a/addons/sms/__init__.py +++ b/addons/sms/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import models +from . import tests from . import wizard diff --git a/addons/sms/tests/__init__.py b/addons/sms/tests/__init__.py new file mode 100644 index 00000000000..511e649c9e2 --- /dev/null +++ b/addons/sms/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import common diff --git a/addons/sms/tests/common.py b/addons/sms/tests/common.py new file mode 100644 index 00000000000..25ca2dfdf7f --- /dev/null +++ b/addons/sms/tests/common.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +from contextlib import contextmanager +from unittest.mock import patch + +from odoo import exceptions +from odoo.tests import common +from odoo.addons.sms.models.sms_api import SmsApi + + +class MockSMS(common.BaseCase): + + def tearDown(self): + super(MockSMS, self).tearDown() + self._clear_sms_sent + + @contextmanager + def mockSMSGateway(self, sim_error=None, nbr_t_error=None): + self._sms = [] + + def _contact_iap(local_endpoint, params): + # mock single sms sending + if local_endpoint == '/iap/message_send': + self._sms += [{ + 'number': number, + 'body': params['message'], + } for number in params['numbers']] + return True # send_message v0 API returns always True + # mock batch sending + if local_endpoint == '/iap/sms/1/send': + result = [] + for to_send in params['messages']: + res = {'res_id': to_send['res_id'], 'state': '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_format_number': + res.update(state='wrong_format_number') + 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': + self._sms.append({ + 'number': to_send['number'], + 'body': to_send['content'], + }) + return result + + try: + with patch.object(SmsApi, '_contact_iap', side_effect=_contact_iap) as contact_iap_mock: + yield + finally: + pass + + def assertSMSSent(self, numbers, content): + """ Check sent SMS. Order is not checked. Each number should have received + the same content. Usefull to check batch sending. + + :param numbers: list of numbers; + :param content: content to check for each number; + """ + self.assertEqual(len(self._sms), len(numbers)) + for number in numbers: + sent_sms = next((sms for sms in self._sms if sms['number'] == number), None) + self.assertTrue(bool(sent_sms), 'Number %s not found in %s' % (number, repr([s['number'] for s in self._sms]))) + self.assertEqual(sent_sms['body'], content) + + def _clear_sms_sent(self): + self._sms = [] diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py index fd6ec9ce627..7df3aa0c96a 100644 --- a/addons/test_mail/tests/common.py +++ b/addons/test_mail/tests/common.py @@ -37,6 +37,17 @@ class BaseFunctionalTest(common.SavepointCase): cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + @classmethod + def _reset_mail_context(cls, record): + return record.with_context( + mail_create_nolog=False, + mail_create_nosubscribe=False, + mail_notrack=False + ) + + def _clear_bus(self): + self.env['bus.bus'].search([]).unlink() + @contextmanager def assertNotifications(self, **counters): """ Counters: 'partner_attribute': 'inbox' or 'email' """ @@ -83,41 +94,57 @@ class BaseFunctionalTest(common.SavepointCase): if hasattr(self, 'assertEmails') and len(new_messages) == 1: self.assertEmails(new_messages.author_id, new_notifications.filtered(lambda n: n.is_email).mapped('res_partner_id')) - def assertBusNotification(self, channels, message_dicts=None, init=True): + def assertBusNotification(self, channels, message_items=None, init=True): """ Check for bus notifications. Basic check is about used channels. Verifying content is optional. :param channels: list of channel - :param messages: if given, list of message making a valid pair (channel, + :param message_items: if given, list of message making a valid pair (channel, message) to be found in bus.bus """ + def check_content(returned_value, expected_value): + if isinstance(expected_value, list): + done = [] + for expected_item in expected_value: + for returned_item in returned_value: + if check_content(returned_item, expected_item): + done.append(expected_item) + break + else: + return False + return len(done) == len(expected_value) + elif isinstance(expected_value, dict): + return all(k in returned_value for k in expected_value.keys()) and all( + check_content(returned_value[key], val) + for key, val in expected_value.items() + ) + else: + return returned_value == expected_value + if init: self.assertEqual(len(self.env['bus.bus'].search([])), len(channels)) notifications = self.env['bus.bus'].search([('channel', 'in', [json_dump(channel) for channel in channels])]) + notif_messages = [json.loads(n.message) for n in notifications] self.assertEqual(len(notifications), len(channels)) - if message_dicts: - notif_messages = [json.loads(n.message) for n in notifications] - for expected in message_dicts: - found = False - for returned in notif_messages: - for key, val in expected.items(): - if key not in returned: - continue - if isinstance(returned[key], list): - if set(returned[key]) != set(val): - continue - else: - if returned[key] != val: - continue - found = True - break - if found: - break - if not found: - raise AssertionError("Bus notification content %s not found" % (repr(expected))) + + for expected in message_items or []: + for notification in notif_messages: + found_keys, not_found_keys = [], [] + if not all(k in notification for k in expected.keys()): + continue + for expected_key, expected_value in expected.items(): + done = check_content(notification[expected_key], expected_value) + if done: + found_keys.append(expected_key) + else: + not_found_keys.append(expected_key) + if set(found_keys) == set(expected.keys()): + break + else: + raise AssertionError('Keys %s not found (expected: %s - returned: %s)' % (not_found_keys, repr(expected), repr(notif_messages))) @contextmanager - def sudoAs(self, login): + def sudo(self, login): old_uid = self.uid try: user = self.env['res.users'].sudo().search([('login', '=', login)]) @@ -125,13 +152,11 @@ class BaseFunctionalTest(common.SavepointCase): # switch user self.uid = user.id self.env = self.env(user=self.uid) - self.test_record = self.test_record.sudo(self.uid) yield finally: # back self.uid = old_uid self.env = self.env(user=self.uid) - self.test_record = self.test_record_old class TestRecipients(common.SavepointCase): @@ -147,10 +172,16 @@ class TestRecipients(common.SavepointCase): }) cls.partner_1 = Partner.create({ 'name': 'Valid Lelitre', - 'email': 'valid.lelitre@agrolait.com'}) + 'email': 'valid.lelitre@agrolait.com', + 'country_id': cls.env.ref('base.be').id, + 'mobile': '0475001122', + }) cls.partner_2 = Partner.create({ 'name': 'Valid Poilvache', - 'email': 'valid.other@gmail.com'}) + 'email': 'valid.other@gmail.com', + 'country_id': cls.env.ref('base.be').id, + 'mobile': '+32 475 22 11 00', + }) class MockEmails(common.SingleTransactionCase): @@ -308,6 +339,3 @@ class Moderation(MockEmails, BaseFunctionalTest): 'subtype_id': self.env['mail.message.subtype'].search([('name', '=', 'Discussions')]).id }) return message - - def _clear_bus(self): - self.env['bus.bus'].search([]).unlink() diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py index 2d4a0ef3faf..2ac76c21423 100644 --- a/addons/test_mail/tests/test_mail_activity.py +++ b/addons/test_mail/tests/test_mail_activity.py @@ -90,7 +90,7 @@ class TestActivityRights(TestActivityCommon): class TestActivityFlow(TestActivityCommon): def test_activity_flow_employee(self): - with self.sudoAs('ernest'): + with self.sudo('ernest'): test_record = self.env['mail.test.activity'].browse(self.test_record.id) self.assertEqual(test_record.activity_ids, self.env['mail.activity']) @@ -121,7 +121,7 @@ class TestActivityFlow(TestActivityCommon): def test_activity_flow_portal(self): portal_user = mail_new_test_user(self.env, login='chell', groups='base.group_portal', name='Chell Gladys') - with self.sudoAs('chell'): + with self.sudo('chell'): test_record = self.env['mail.test.activity'].browse(self.test_record.id) with self.assertRaises(exceptions.AccessError): self.env['mail.activity'].create({ @@ -164,7 +164,8 @@ class TestActivityMixin(TestActivityCommon): def test_activity_mixin(self): self.user_employee.tz = self.user_admin.tz - with self.sudoAs('ernest'): + with self.sudo('ernest'): + self.test_record = self.env['mail.test.activity'].browse(self.test_record.id) self.assertEqual(self.test_record.env.user, self.user_employee) now_utc = datetime.now(pytz.UTC) diff --git a/addons/test_mail_full/__init__.py b/addons/test_mail_full/__init__.py new file mode 100644 index 00000000000..567548c78f5 --- /dev/null +++ b/addons/test_mail_full/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import tests diff --git a/addons/test_mail_full/__manifest__.py b/addons/test_mail_full/__manifest__.py new file mode 100644 index 00000000000..dab4550230a --- /dev/null +++ b/addons/test_mail_full/__manifest__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Mail Tests (Full)', + 'version': '1.0', + 'category': 'Hidden', + 'sequence': 9876, + 'summary': 'Mail Tests: performances and tests specific to mail with all sub-modules', + 'description': """This module contains tests related to various mail features +and mail-related sub modules. Those tests are contained in a separate module as it +contains models used only to perform tests independently to functional aspects of +real applications. """, + 'depends': [ + 'test_mail', + 'mail', + 'mail_bot', + # 'snailmail', + 'mass_mailing', + 'phone_validation', + 'sms', + ], + 'data': [ + 'security/ir.model.access.csv', + ], + 'demo': [ + ], + 'installable': True, + 'application': False, +} diff --git a/addons/test_mail_full/models/__init__.py b/addons/test_mail_full/models/__init__.py new file mode 100644 index 00000000000..26ed2919633 --- /dev/null +++ b/addons/test_mail_full/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_mail_models diff --git a/addons/test_mail_full/models/test_mail_models.py b/addons/test_mail_full/models/test_mail_models.py new file mode 100644 index 00000000000..b351ba8ce45 --- /dev/null +++ b/addons/test_mail_full/models/test_mail_models.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class MailTestSMS(models.Model): + """ A model inheriting from mail.thread with some fields used for SMS + gateway, like a partner, a specific mobile phone, ... """ + _description = 'Chatter Model for SMS Gateway' + _name = 'mail.test.sms' + _inherit = ['mail.thread'] + + name = fields.Char() + subject = fields.Char() + email_from = fields.Char() + phone_nbr = fields.Char() + mobile_nbr = fields.Char() + customer_id = fields.Many2one('res.partner', 'Customer') + + @api.multi + def _get_default_sms_recipients(self): + return self.mapped('customer_id') diff --git a/addons/test_mail_full/security/ir.model.access.csv b/addons/test_mail_full/security/ir.model.access.csv new file mode 100644 index 00000000000..2506e6ac804 --- /dev/null +++ b/addons/test_mail_full/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mail_test_sms_all,mail.test.sms.all,model_mail_test_sms,,0,0,0,0 +access_mail_test_sms_user,mail.test.sms.user,model_mail_test_sms,base.group_user,1,1,1,1 diff --git a/addons/test_mail_full/tests/__init__.py b/addons/test_mail_full/tests/__init__.py new file mode 100644 index 00000000000..3eac1b648f9 --- /dev/null +++ b/addons/test_mail_full/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import common +from . import test_sms_composer +from . import test_sms_post diff --git a/addons/test_mail_full/tests/common.py b/addons/test_mail_full/tests/common.py new file mode 100644 index 00000000000..b9abbbec7dc --- /dev/null +++ b/addons/test_mail_full/tests/common.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from odoo.addons.test_mail.tests import common as test_mail_common + + +class BaseFunctionalTest(test_mail_common.BaseFunctionalTest): + + @classmethod + def setUpClass(cls): + super(BaseFunctionalTest, 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}) diff --git a/addons/test_mail_full/tests/test_sms_composer.py b/addons/test_mail_full/tests/test_sms_composer.py new file mode 100644 index 00000000000..bcdcbd32f1e --- /dev/null +++ b/addons/test_mail_full/tests/test_sms_composer.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.sms.tests import common as sms_common +from odoo.addons.test_mail.tests import common as test_mail_common +from odoo.addons.test_mail_full.tests import common as test_mail_full_common +from odoo.addons.phone_validation.tools.phone_validation import phone_format + + +class TestSMSComposer(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients): + + @classmethod + def setUpClass(cls): + super(TestSMSComposer, cls).setUpClass() + cls._test_body = 'VOID CONTENT' + cls.partner_numbers = [ + phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164') + for partner in (cls.partner_1 | cls.partner_2) + ] + cls.random_numbers_str = '+32475998877, 0475997788' + cls.random_numbers = [phone_format(number, 'BE', '32', force_format='E164') for number in ['+32475998877', '0475997788']] + + def test_composer_no_model(self): + composer = self.env['sms.composer'].with_context().create({ + 'message': self._test_body, + 'recipients': '+32475998877, 0475997788' + }) + + with self.mockSMSGateway(): + composer.action_send_sms() + # self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3) + self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body) + + def test_composer_no_mail_thread(self): + composer = self.env['sms.composer'].with_context(active_model='test_performance.base').create({ + 'message': self._test_body, + 'recipients': '+32475998877, 0475997788' + }) + + with self.mockSMSGateway(): + composer.action_send_sms() + # self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3) + self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body) + + def test_composer_partners_active_domain(self): + partners = self.partner_1 | self.partner_2 + composer = self.env['sms.composer'].with_context( + active_model='res.partner', + active_domain=[('id', 'in', partners.ids)] + ).create({ + 'message': self._test_body, + }) + + with self.mockSMSGateway(): + composer.action_send_sms() + self.assertSMSSent(self.partner_numbers, self._test_body) + + def test_composer_partners_sanitize(self): + partner_incorrect = self.env['res.partner'].create({ + 'name': 'Jean-Claude Incorrect', + 'email': 'jean.claude@example.com', + 'mobile': 'coincoin', + }) + partners = self.partner_1 | self.partner_2 | partner_incorrect + composer = self.env['sms.composer'].with_context( + active_model='res.partner', + active_domain=[('id', 'in', partners.ids)] + ).create({ + 'message': self._test_body, + }) + + with self.mockSMSGateway(): + composer.action_send_sms() + # self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body) # TDE FIXME: actually sanitizer does not work in current master (saas 12.23)) \ No newline at end of file diff --git a/addons/test_mail_full/tests/test_sms_post.py b/addons/test_mail_full/tests/test_sms_post.py new file mode 100644 index 00000000000..a12855341a0 --- /dev/null +++ b/addons/test_mail_full/tests/test_sms_post.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.sms.tests import common as sms_common +from odoo.addons.test_mail.tests import common as test_mail_common +from odoo.addons.test_mail_full.tests import common as test_mail_full_common + + +class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients): + + @classmethod + def setUpClass(cls): + super(TestSMSPost, cls).setUpClass() + cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({ + 'name': 'Test', + 'customer_id': cls.partner_1.id, + }) + cls.test_record = cls._reset_mail_context(cls.test_record) + + def test_message_post_with_sms_on_partners(self): + test_body = 'Void body' + with self.mockSMSGateway(): + (self.partner_1 | self.partner_2).message_post_send_sms(test_body) + self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body) + + def test_message_post_with_sms_w_default(self): + test_body = 'Void body' + with self.mockSMSGateway(): + self.test_record.message_post_send_sms(test_body) + self.assertSMSSent(self.partner_1.mapped('mobile'), test_body) + self.assertIn(test_body, self.test_record.message_ids.body) + + def test_message_post_with_sms_w_numbers(self): + test_body = 'Void body' + test_numbers = ['0475114477', '0475225588'] + with self.mockSMSGateway(): + self.test_record.message_post_send_sms(test_body, numbers=test_numbers) + self.assertSMSSent(test_numbers, test_body) + self.assertIn(test_body, self.test_record.message_ids.body) + + def test_message_post_with_sms_w_numbers_duplicate(self): + test_body = 'Void body' + test_numbers = ['0475114477', '0475225588', '0475114477'] + with self.mockSMSGateway(): + self.test_record.message_post_send_sms(test_body, numbers=test_numbers) + self.assertSMSSent(test_numbers, test_body) + self.assertIn(test_body, self.test_record.message_ids.body) + + def test_message_post_with_sms_w_partners(self): + test_body = 'Void body' + with self.mockSMSGateway(): + self.test_record.message_post_send_sms(test_body, partners=self.partner_1 | self.partner_2) + self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body) + self.assertIn(test_body, self.test_record.message_ids.body)