From 20c10fcd19b67cc70b64be0ee81f0582e67a1eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 21 May 2019 10:40:09 +0000 Subject: [PATCH] [ADD] sms, test_mail(_full): add basic SMS tests and improve test tools Purpose of this commit is to improve and add tools, methods and data to test mail and SMS features. We also add tests for current implementation of SMS feature, allowing to better understand future changes. [ADD] test_mail_full * have a module depending on mail sub-applications like sms or snailmail. Its purpose is to check that standard mail features work effectively with all overrides and extra behaviors activated; * add a new model specific for SMS gateway, with default recipient computation; * add SMS tests for SMS module adding SMS capabilities linked to mail feature. This commit tests SMS feature before the upcoming refactoring and improvement of SMS module in community: posting with SMS and sms composer usage; In test_mail * add necessary mobile information on test partners; * improve assertBusNotification that was not correctly asserting all items in message of bus notifications; In sms * add mock for SMS sending. Purpose is to mock the connection to IAP services by mocking the call to IAP server. It allows to perform SMS tests without having to contact (and pay) for this service; * allow some customization when calling the SMS gateway mock to simulate errors and test corner cases; Related to task 1922163 Linked to PR #34516 --- addons/sms/__init__.py | 1 + addons/sms/tests/__init__.py | 4 + addons/sms/tests/common.py | 71 +++++++++++++++ addons/test_mail/tests/common.py | 88 ++++++++++++------- addons/test_mail/tests/test_mail_activity.py | 7 +- addons/test_mail_full/__init__.py | 4 + addons/test_mail_full/__manifest__.py | 29 ++++++ addons/test_mail_full/models/__init__.py | 3 + .../test_mail_full/models/test_mail_models.py | 23 +++++ .../security/ir.model.access.csv | 3 + addons/test_mail_full/tests/__init__.py | 5 ++ addons/test_mail_full/tests/common.py | 14 +++ .../test_mail_full/tests/test_sms_composer.py | 74 ++++++++++++++++ addons/test_mail_full/tests/test_sms_post.py | 54 ++++++++++++ 14 files changed, 347 insertions(+), 33 deletions(-) create mode 100644 addons/sms/tests/__init__.py create mode 100644 addons/sms/tests/common.py create mode 100644 addons/test_mail_full/__init__.py create mode 100644 addons/test_mail_full/__manifest__.py create mode 100644 addons/test_mail_full/models/__init__.py create mode 100644 addons/test_mail_full/models/test_mail_models.py create mode 100644 addons/test_mail_full/security/ir.model.access.csv create mode 100644 addons/test_mail_full/tests/__init__.py create mode 100644 addons/test_mail_full/tests/common.py create mode 100644 addons/test_mail_full/tests/test_sms_composer.py create mode 100644 addons/test_mail_full/tests/test_sms_post.py 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)