# Copyright 2016 Tecnativa - Antonio Espinosa # Copyright 2017 Tecnativa - David Vidal # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo.tools import mute_logger from odoo.tests.common import TransactionCase from odoo.exceptions import UserError, ValidationError import mock _packagepath = 'odoo.addons.mail_tracking_mailgun' class TestMailgun(TransactionCase): def mail_send(self): mail = self.env['mail.mail'].create({ 'subject': 'Test subject', 'email_from': 'from@example.com', 'email_to': self.recipient, 'body_html': '

This is a test message

', }) mail.send() # Search tracking created tracking_email = self.env['mail.tracking.email'].search([ ('mail_id', '=', mail.id), ]) return mail, tracking_email def setUp(self): super(TestMailgun, self).setUp() self.recipient = 'to@example.com' self.mail, self.tracking_email = self.mail_send() self.api_key = 'key-12345678901234567890123456789012' self.domain = 'example.com' self.token = 'f1349299097a51b9a7d886fcb5c2735b426ba200ada6e9e149' self.timestamp = '1471021089' self.signature = ('4fb6d4dbbe10ce5d620265dcd7a3c0b8' 'ca0dede1433103891bc1ae4086e9d5b2') self.env['ir.config_parameter'].set_param( 'mailgun.apikey', self.api_key) self.env['ir.config_parameter'].set_param( 'mail.catchall.domain', self.domain) self.env['ir.config_parameter'].set_param( 'mailgun.validation_key', self.api_key) self.env['ir.config_parameter'].set_param( 'mailgun.auto_check_partner_email', '') self.event = { 'Message-Id': '', 'X-Mailgun-Sid': 'WyIwNjgxZSIsICJ0b0BleGFtcGxlLmNvbSIsICI3MG' 'I0MWYiXQ==', 'token': self.token, 'timestamp': self.timestamp, 'signature': self.signature, 'domain': 'example.com', 'message-headers': '[]', 'recipient': self.recipient, 'odoo_db': self.env.cr.dbname, 'tracking_email_id': '%s' % self.tracking_email.id } self.metadata = { 'ip': '127.0.0.1', 'user_agent': False, 'os_family': False, 'ua_family': False, } self.partner = self.env['res.partner'].create({ 'name': 'Mr. Odoo', 'email': 'mrodoo@example.com', }) self.response = { "items": [{ "log-level": "info", "id": "oXAVv5URCF-dKv8c6Sa7T", "timestamp": 1509119329.0, "message": { "headers": { "to": "test@test.com", "message-id": "test-id@f187c54734e8", "from": "Mr. Odoo ", "subject": "This is a test" }, }, "event": "delivered", "recipient": "to@example.com", }] } def event_search(self, event_type): event = self.env['mail.tracking.event'].search([ ('tracking_email_id', '=', self.tracking_email.id), ('event_type', '=', event_type), ]) self.assertTrue(event) return event def test_no_api_key(self): self.env['ir.config_parameter'].set_param('mailgun.apikey', '') self.test_event_delivered() with self.assertRaises(ValidationError): self.env['mail.tracking.email']._mailgun_values() def test_no_domain(self): self.env['ir.config_parameter'].set_param('mail.catchall.domain', '') self.test_event_delivered() with self.assertRaises(ValidationError): self.env['mail.tracking.email']._mailgun_values() # now we set an specific domain for Mailgun: # i.e: we configure new EU zone without loosing old domain statistics self.env['ir.config_parameter'].set_param( 'mailgun.domain', 'eu.example.com') self.test_event_delivered() @mute_logger('odoo.addons.mail_tracking_mailgun.models' '.mail_tracking_email') def test_bad_signature(self): self.event.update({ 'event': 'delivered', 'signature': 'bad_signature', }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('ERROR: Signature', response) @mute_logger('odoo.addons.mail_tracking_mailgun.models' '.mail_tracking_email') def test_bad_event_type(self): self.event.update({ 'event': 'bad_event', }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('ERROR: Event type not supported', response) @mute_logger('odoo.addons.mail_tracking_mailgun.models' '.mail_tracking_email') def test_bad_db(self): self.event.update({ 'event': 'delivered', 'odoo_db': 'bad_db', }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('ERROR: Invalid DB', response) def test_bad_ts(self): timestamp = '7a' # Now time will be used instead signature = ('06cc05680f6e8110e59b41152b2d1c0f' '1045d755ef2880ff922344325c89a6d4') self.event.update({ 'event': 'delivered', 'timestamp': timestamp, 'signature': signature, }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('OK', response) @mute_logger('odoo.addons.mail_tracking_mailgun.models' '.mail_tracking_email') def test_tracking_not_found(self): self.event.update({ 'event': 'delivered', 'tracking_email_id': 'bad_id', }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('ERROR: Tracking not found', response) # https://documentation.mailgun.com/user_manual.html#tracking-deliveries def test_event_delivered(self): self.event.update({ 'event': 'delivered', }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('OK', response) events = self.event_search('delivered') for event in events: self.assertEqual(event.timestamp, float(self.timestamp)) self.assertEqual(event.recipient, self.recipient) # https://documentation.mailgun.com/user_manual.html#tracking-opens def test_event_opened(self): ip = '127.0.0.1' user_agent = 'Odoo Test/8.0 Gecko Firefox/11.0' os_family = 'Linux' ua_family = 'Firefox' ua_type = 'browser' self.event.update({ 'event': 'opened', 'city': 'Mountain View', 'country': 'US', 'region': 'CA', 'client-name': ua_family, 'client-os': os_family, 'client-type': ua_type, 'device-type': 'desktop', 'ip': ip, 'user-agent': user_agent, }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('OK', response) event = self.event_search('open') self.assertEqual(event.timestamp, float(self.timestamp)) self.assertEqual(event.recipient, self.recipient) self.assertEqual(event.ip, ip) self.assertEqual(event.user_agent, user_agent) self.assertEqual(event.os_family, os_family) self.assertEqual(event.ua_family, ua_family) self.assertEqual(event.ua_type, ua_type) self.assertEqual(event.mobile, False) self.assertEqual(event.user_country_id.code, 'US') # https://documentation.mailgun.com/user_manual.html#tracking-clicks def test_event_clicked(self): ip = '127.0.0.1' user_agent = 'Odoo Test/8.0 Gecko Firefox/11.0' os_family = 'Linux' ua_family = 'Firefox' ua_type = 'browser' url = 'https://odoo-community.org' self.event.update({ 'event': 'clicked', 'city': 'Mountain View', 'country': 'US', 'region': 'CA', 'client-name': ua_family, 'client-os': os_family, 'client-type': ua_type, 'device-type': 'tablet', 'ip': ip, 'user-agent': user_agent, 'url': url, }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata, event_type='click') self.assertEqual('OK', response) event = self.event_search('click') self.assertEqual(event.timestamp, float(self.timestamp)) self.assertEqual(event.recipient, self.recipient) self.assertEqual(event.ip, ip) self.assertEqual(event.user_agent, user_agent) self.assertEqual(event.os_family, os_family) self.assertEqual(event.ua_family, ua_family) self.assertEqual(event.ua_type, ua_type) self.assertEqual(event.mobile, True) self.assertEqual(event.url, url) # https://documentation.mailgun.com/user_manual.html#tracking-unsubscribes def test_event_unsubscribed(self): ip = '127.0.0.1' user_agent = 'Odoo Test/8.0 Gecko Firefox/11.0' os_family = 'Linux' ua_family = 'Firefox' ua_type = 'browser' self.event.update({ 'event': 'unsubscribed', 'city': 'Mountain View', 'country': 'US', 'region': 'CA', 'client-name': ua_family, 'client-os': os_family, 'client-type': ua_type, 'device-type': 'mobile', 'ip': ip, 'user-agent': user_agent, }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('OK', response) event = self.event_search('unsub') self.assertEqual(event.timestamp, float(self.timestamp)) self.assertEqual(event.recipient, self.recipient) self.assertEqual(event.ip, ip) self.assertEqual(event.user_agent, user_agent) self.assertEqual(event.os_family, os_family) self.assertEqual(event.ua_family, ua_family) self.assertEqual(event.ua_type, ua_type) self.assertEqual(event.mobile, True) # https://documentation.mailgun.com/ # user_manual.html#tracking-spam-complaints def test_event_complained(self): self.event.update({ 'event': 'complained', }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('OK', response) event = self.event_search('spam') self.assertEqual(event.timestamp, float(self.timestamp)) self.assertEqual(event.recipient, self.recipient) self.assertEqual(event.error_type, 'spam') # https://documentation.mailgun.com/user_manual.html#tracking-bounces def test_event_bounced(self): code = '550' error = ("5.1.1 The email account does not exist.\n" "5.1.1 double-checking the recipient's email address") notification = "Please, check recipient's email address" self.event.update({ 'event': 'bounced', 'code': code, 'error': error, 'notification': notification, }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('OK', response) event = self.event_search('hard_bounce') self.assertEqual(event.timestamp, float(self.timestamp)) self.assertEqual(event.recipient, self.recipient) self.assertEqual(event.error_type, code) self.assertEqual(event.error_description, error) self.assertEqual(event.error_details, notification) # https://documentation.mailgun.com/user_manual.html#tracking-failures def test_event_dropped(self): reason = 'hardfail' code = '605' description = 'Not delivering to previously bounced address' self.event.update({ 'event': 'dropped', 'reason': reason, 'code': code, 'description': description, }) response = self.env['mail.tracking.email'].event_process( None, self.event, self.metadata) self.assertEqual('OK', response) event = self.event_search('reject') self.assertEqual(event.timestamp, float(self.timestamp)) self.assertEqual(event.recipient, self.recipient) self.assertEqual(event.error_type, reason) self.assertEqual(event.error_description, code) self.assertEqual(event.error_details, description) @mock.patch(_packagepath + '.models.res_partner.requests') def test_email_validity(self, mock_request): self.partner.email_bounced = False mock_request.get.return_value.apparent_encoding = 'ascii' mock_request.get.return_value.status_code = 200 mock_request.get.return_value.json.return_value = { 'is_valid': True, 'mailbox_verification': 'true', } # Trigger email auto validation in partner self.env['ir.config_parameter'].set_param( 'mailgun.auto_check_partner_email', 'True') self.partner.email = 'info@tecnativa.com' self.assertFalse(self.partner.email_bounced) self.partner.email = 'xoxoxoxo@tecnativa.com' # Not a valid mailbox mock_request.get.return_value.json.return_value = { 'is_valid': True, 'mailbox_verification': 'false', } with self.assertRaises(UserError): self.partner.check_email_validity() # Not a valid mail address mock_request.get.return_value.json.return_value = { 'is_valid': False, 'mailbox_verification': 'false', } with self.assertRaises(UserError): self.partner.check_email_validity() # Unable to fully validate mock_request.get.return_value.json.return_value = { 'is_valid': True, 'mailbox_verification': 'unknown', } with self.assertRaises(UserError): self.partner.check_email_validity() self.assertTrue(self.partner.email_bounced) @mock.patch(_packagepath + '.models.res_partner.requests') def test_email_validity_exceptions(self, mock_request): mock_request.get.return_value.status_code = 404 with self.assertRaises(UserError): self.partner.check_email_validity() self.env['ir.config_parameter'].set_param('mailgun.validation_key', '') with self.assertRaises(UserError): self.partner.check_email_validity() @mock.patch(_packagepath + '.models.res_partner.requests') def test_bounced(self, mock_request): self.partner.email_bounced = True mock_request.get.return_value.status_code = 404 self.partner.check_email_bounced() self.assertFalse(self.partner.email_bounced) mock_request.get.return_value.status_code = 200 self.partner.force_set_bounced() self.partner.check_email_bounced() self.assertTrue(self.partner.email_bounced) mock_request.delete.return_value.status_code = 200 self.partner.force_unset_bounced() self.assertFalse(self.partner.email_bounced) def test_email_bounced_set(self): message_number = len(self.partner.message_ids) + 1 self.partner._email_bounced_set('test_error', False) self.assertEqual(len(self.partner.message_ids), message_number) self.partner.email = "" self.partner._email_bounced_set('test_error', False) self.assertEqual(len(self.partner.message_ids), message_number) @mock.patch(_packagepath + '.models.mail_tracking_email.requests') def test_manual_check(self, mock_request): mock_request.get.return_value.json.return_value = self.response mock_request.get.return_value.status_code = 200 self.tracking_email.action_manual_check_mailgun() event = self.env['mail.tracking.event'].search( [('mailgun_id', '=', self.response['items'][0]['id'])]) self.assertEqual(event.event_type, self.response['items'][0]['event']) @mock.patch(_packagepath + '.models.mail_tracking_email.requests') def test_manual_check_exceptions(self, mock_request): mock_request.get.return_value.status_code = 404 with self.assertRaises(ValidationError): self.tracking_email.action_manual_check_mailgun() mock_request.get.return_value.status_code = 200 mock_request.get.return_value.json.return_value = {} with self.assertRaises(ValidationError): self.tracking_email.action_manual_check_mailgun()