# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import psycopg2 from ast import literal_eval from odoo import exceptions from odoo.addons.mail.tests.common import MailCommon from odoo.tests import tagged from odoo.tests.common import users from odoo.tools import formataddr, mute_logger class TestMailAliasCommon(MailCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.test_alias_mc = cls.env['mail.alias'].create({ 'alias_domain_id': cls.mail_alias_domain.id, 'alias_model_id': cls.env['ir.model']._get('mail.test.container.mc').id, 'alias_name': 'test.alias', }) @tagged('mail_gateway', 'mail_alias', 'multi_company') class TestMailAlias(TestMailAliasCommon): """ Test alias model features, constraints and behavior. """ @users('admin') def test_alias_domain_allowed_validation(self): """ Check the validation of `mail.catchall.domain.allowed` system parameter""" for value in [',', ',,', ', ,']: with self.assertRaises(exceptions.ValidationError): self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', value) for value, expected in [ ('', False), ('hello.com', 'hello.com'), ('hello.com,,', 'hello.com'), ('hello.com,bonjour.com', 'hello.com,bonjour.com'), ('hello.COM, BONJOUR.com', 'hello.com,bonjour.com'), ]: self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', value) self.assertEqual(self.env['ir.config_parameter'].get_param('mail.catchall.domain.allowed'), expected) @users('erp_manager') def test_alias_domain_company_check(self): """ Check constraint trying to avoid ill-defined company setup aka having an alias domain on parent record / record to update that does not match the alias domain. """ misc_alias_domain = self.env['mail.alias.domain'].create({'name': 'misc.com'}) record_mc_c1, record_mc_c2 = self.env['mail.test.container.mc'].create([ { 'alias_name': 'Test1', 'company_id': self.company_admin.id, }, { 'alias_name': 'Test2', 'company_id': self.company_2.id, } ]) alias_mc_c1, alias_mc_c2 = record_mc_c1.alias_id, record_mc_c2.alias_id self.assertEqual( (alias_mc_c1 + alias_mc_c2).alias_parent_model_id, self.env['ir.model']._get('mail.test.container.mc')) self.assertEqual( (alias_mc_c1 + alias_mc_c2).mapped('alias_parent_thread_id'), (record_mc_c1 + record_mc_c2).ids) self.assertEqual(alias_mc_c1.alias_domain_id, self.mail_alias_domain) self.assertEqual(alias_mc_c2.alias_domain_id, self.mail_alias_domain_c2) # mail_alias_domain_c2 is linked to a conflicting company with self.assertRaises(exceptions.ValidationError): record_mc_c1.alias_domain_id = self.mail_alias_domain_c2 with self.assertRaises(exceptions.ValidationError): alias_mc_c1.sudo().alias_domain_id = self.mail_alias_domain_c2 # misc_alias_domain is not linked to any company, therefore ok record_mc_c1.alias_domain_id = misc_alias_domain # alias updating records record_upd_c1, record_upd_c2 = self.env['mail.test.alias.optional'].sudo().create([ { 'alias_name': 'Update C1', 'company_id': self.company_admin.id, }, { 'alias_name': 'Update C2', 'company_id': self.company_2.id, } ]) alias_update_c1, alias_update_c2 = record_upd_c1.alias_id, record_upd_c2.alias_id self.assertEqual( (alias_update_c1 + alias_update_c2).mapped('alias_force_thread_id'), (record_upd_c1 + record_upd_c2).ids) self.assertEqual(alias_update_c1.alias_domain_id, self.mail_alias_domain) self.assertEqual(alias_update_c2.alias_domain_id, self.mail_alias_domain_c2) # mail_alias_domain_c2 is linked to a conflicting company with self.assertRaises(exceptions.ValidationError): record_upd_c1.alias_domain_id = self.mail_alias_domain_c2 with self.assertRaises(exceptions.ValidationError): alias_update_c1.sudo().alias_domain_id = self.mail_alias_domain_c2 # misc_alias_domain is not linked to any company, therefore ok record_upd_c1.alias_domain_id = misc_alias_domain @users('admin') def test_alias_name_unique(self): """ Check uniqueness constraint on alias names, at create and update. Also check conflict management with bounce / catchall defined on alias domains. """ mail_alias_domain = self.mail_alias_domain.with_env(self.env) mail_alias_domain_c2 = self.mail_alias_domain_c2.with_env(self.env) alias_model_id = self.env['ir.model']._get('mail.test.gateway').id new_mail_alias = self.env['mail.alias'].create({ 'alias_model_id': alias_model_id, 'alias_name': 'unused.test.alias', }) other_alias = self.env['mail.alias'].create({ 'alias_model_id': alias_model_id, 'alias_name': 'other.test.alias', }) self.assertEqual((new_mail_alias + other_alias).alias_domain_id, mail_alias_domain) # test you cannot create or update aliases matching bounce / catchall of same alias domain with self.assertRaises(exceptions.ValidationError), self.cr.savepoint(): self.env['mail.alias'].create({ 'alias_model_id': alias_model_id, 'alias_name': mail_alias_domain.catchall_alias, }) with self.assertRaises(exceptions.ValidationError), self.cr.savepoint(): self.env['mail.alias'].create({ 'alias_model_id': alias_model_id, 'alias_name': mail_alias_domain.bounce_alias, }) with self.assertRaises(exceptions.UserError), self.cr.savepoint(): new_mail_alias.write({'alias_name': mail_alias_domain.catchall_alias}) with self.assertRaises(exceptions.UserError), self.cr.savepoint(): new_mail_alias.write({'alias_name': mail_alias_domain.bounce_alias}) # other domains bounce / catchall do not prevent new_aliases = self.env['mail.alias'].create([ {'alias_model_id': alias_model_id, 'alias_name': self.alias_catchall_c2}, {'alias_model_id': alias_model_id, 'alias_name': self.alias_bounce_c2}, ]) self.assertEqual(new_aliases.alias_domain_id, mail_alias_domain) new_aliases.unlink() # bounce/catchall of another domain is ok new_mail_alias.write({'alias_name': mail_alias_domain_c2.bounce_alias}) other_alias.write({'alias_name': mail_alias_domain_c2.catchall_alias}) # changing domain would clash with existing catchall with self.assertRaises(exceptions.UserError), self.cr.savepoint(): new_mail_alias.write({'alias_domain_id': mail_alias_domain_c2.id,}) new_mail_alias.write({'alias_name': 'unused.test.alias'}) # test that alias {name, alias_domain_id} should be unique with self.assertRaises(exceptions.UserError), self.cr.savepoint(): self.env['mail.alias'].create({ 'alias_model_id': alias_model_id, 'alias_name': 'unused.test.alias', }) with self.assertRaises(exceptions.UserError), self.cr.savepoint(): self.env['mail.alias'].create([ { 'alias_model_id': alias_model_id, 'alias_name': alias_name, } for alias_name in ('new.alias.1', 'new.alias.2', 'new.alias.1') ]) with self.assertRaises(exceptions.UserError), self.cr.savepoint(): other_alias.write({'alias_name': 'unused.test.alias'}) # also valid for void domain nodom_alias = self.env['mail.alias'].create({ 'alias_domain_id': False, 'alias_model_id': alias_model_id, 'alias_name': 'no.domain', }) self.assertFalse(nodom_alias.alias_domain_id) with self.assertRaises(exceptions.UserError), self.cr.savepoint(): self.env['mail.alias'].create({ 'alias_domain_id': False, 'alias_model_id': alias_model_id, 'alias_name': 'no.domain', }) with self.assertRaises(exceptions.UserError), self.cr.savepoint(): self.env['mail.alias'].create([ { 'alias_domain_id': False, 'alias_model_id': alias_model_id, 'alias_name': 'dupes.wo.domain', } for _x in range(2) ]) with self.assertRaises(exceptions.UserError), self.cr.savepoint(): other_alias.write({ 'alias_domain_id': False, 'alias_name': 'no.domain', }) # test that alias name can be duplicated in case of different alias domains other_domain_alias = self.env['mail.alias'].create({ 'alias_domain_id': mail_alias_domain_c2.id, 'alias_model_id': alias_model_id, 'alias_name': 'unused.test.alias' }) self.assertEqual(other_domain_alias.alias_domain_id, mail_alias_domain_c2) # changing domain would violate uniqueness with self.assertRaises(exceptions.UserError), self.cr.savepoint(): other_domain_alias.write({'alias_domain_id': mail_alias_domain.id}) @users('admin') def test_alias_name_unique_copy(self): """ Check uniqueness constraint check when copying aliases """ mail_alias_domain = self.mail_alias_domain.with_env(self.env) alias_model_id = self.env['ir.model']._get('mail.test.gateway').id new_mail_alias = self.env['mail.alias'].create({ 'alias_model_id': alias_model_id, 'alias_name': 'unused.test.alias' }) with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint(): new_mail_alias.copy({'alias_name': 'unused.test.alias'}) # test that duplicating an alias should have blank name copy_1 = new_mail_alias.copy() self.assertFalse(copy_1.alias_name) self.assertEqual(copy_1.alias_domain_id, mail_alias_domain) # test sanitize of copy with new name copy_2 = new_mail_alias.copy({'alias_name': 'test.alias.2.éè#'}) self.assertEqual(copy_2.alias_name, 'test.alias.2.ee#') self.assertEqual(copy_2.alias_domain_id, mail_alias_domain) # cannot batch update, would create duplicates with self.assertRaises(exceptions.UserError): (copy_1 + copy_2).write({'alias_name': 'test.alias.other'}) @users('admin') @mute_logger('odoo.models.unlink') def test_alias_name_sanitize(self): """ Check sanitizer, at both create, copy and write on alias name. """ alias_names = [ 'bidule...inc.', 'b4r+=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~', 'hélène.prôvâïder', '😊', 'Déboulonneur 😊', 'ぁ', ] expected_names = [ 'bidule.inc', 'b4r+=*r3wl_#_-$-{}-~|-/!?&%^\'-`~', 'helene.provaider', '-', 'deboulonneur-', '?', ] msgs = [ 'Emails cannot start or end with a dot, there cannot be a sequence of dots.', 'Disallowed chars should be replaced by hyphens', 'Email alias should be unaccented', 'Only a subset of unaccented latin chars are valid, others are replaced', 'Only a subset of unaccented latin chars are valid, others are replaced', 'Only a subset of unaccented latin chars are valid, others are replaced', ] for alias_name, expected, msg in zip(alias_names, expected_names, msgs): with self.subTest(alias_name=alias_name): alias = self.env['mail.alias'].create({ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, 'alias_name': alias_name, }) self.assertEqual(alias.alias_name, expected, msg) alias.unlink() alias = self.env['mail.alias'].create({ 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, }) # check at write for alias_name, expected, msg in zip(alias_names, expected_names, msgs): with self.subTest(alias_name=alias_name): alias.write({'alias_name': alias_name}) self.assertEqual(alias.alias_name, expected, msg) @users('admin') def test_alias_name_sanitize_false(self): """ Check empty-like aliases are forced to False, as otherwise unique constraint might fail with empty strings. """ aliases = self.env['mail.alias'].create([ { 'alias_model_id': self.env['ir.model']._get('mail.test.container').id, 'alias_name': falsy_name, } # '.' -> not allowed to start with a "." hence False for falsy_name in [False, None, '', ' ', '.'] ]) for alias in aliases: with self.subTest(alias_name=alias.alias_name): self.assertFalse(alias.alias_name, 'Void values should resolve to False') # try to reset names in batch: should work for idx, alias in enumerate(aliases): alias.write({'alias_name': f'unique-{idx}'}) aliases.write({'alias_name': ''}) for alias in aliases: self.assertEqual(alias.alias_name, False) @users('admin') def test_search(self): """ Test search on aliases, notably searching on display_name which should be split on searching on alias_name and alias_domain_id. """ # ensure existing aliases to ease future asserts existing = self.env['mail.alias'].search([('alias_domain_id', '!=', False)]) self.assertEqual(existing.alias_domain_id, self.mail_alias_domain) existing.write({'alias_name': False}) # don't be annoyed by existing aliases mail_alias_domain = self.mail_alias_domain.with_env(self.env) mail_alias_domain_c2 = self.mail_alias_domain_c2.with_env(self.env) self.assertEqual(mail_alias_domain.name, 'test.mycompany.com') self.assertEqual(mail_alias_domain_c2.name, 'test.mycompany2.com') aliases = self.env['mail.alias'].create([ { 'alias_model_id': self.env['ir.model']._get('mail.test.container.mc').id, 'alias_name': f'test.search.{idx}', 'alias_domain_id': domain.id, } for idx in range(5) for domain in (mail_alias_domain + mail_alias_domain_c2) ]) aliases_d1 = aliases.filtered(lambda a: a.alias_domain_id == mail_alias_domain) aliases_d2 = aliases.filtered(lambda a: a.alias_domain_id == mail_alias_domain_c2) # search on alias_name: classic search self.assertEqual( self.env['mail.alias'].search([('alias_name', 'ilike', 'test.search')]), aliases ) # search on alias_fullname: search on aggregated of {name}@{domain} for search_term, expected, msg in [ ('mycompany', aliases, 'Match all aliases on both domains as "mycompany" is contained in those two'), (mail_alias_domain.name, aliases_d1, 'Exact match on domain 1: should find all aliases in that domain'), (mail_alias_domain_c2.name, aliases_d2, 'Exact match on domain 2: should find all aliases in that domain'), ('search.0@test.mycompany', aliases.filtered(lambda a: a.alias_name == 'test.search.0'), 'Match in both domains'), ('search.0@test.mycompany.com', aliases.filtered(lambda a: a.alias_name == 'test.search.0' and a.alias_domain_id == mail_alias_domain), 'Match only in domain 1'), ('search@test.mycompany.com', self.env['mail.alias'], 'Does not match even as ilike'), ]: with self.subTest(search_term=search_term): self.assertEqual( self.env['mail.alias'].search([('alias_full_name', 'ilike', search_term)]), expected, msg ) # search using IN operator for search_term, expected, msg in [ (['mycompany'], self.env['mail.alias'], 'mycompany is too vague: does not match a left- and right- part (!= ilike)'), ([mail_alias_domain.name], self.env['mail.alias'], 'Match only right-part of aliases emails'), ]: with self.subTest(search_term=search_term): self.assertEqual(self.env['mail.alias'].search([('alias_full_name', 'in', search_term)]), expected, msg ) @users('admin') def test_alias_setup(self): """ Test various constraints / configuration of alias model""" alias = self.env['mail.alias'].create({ 'alias_model_id': self.env['ir.model']._get('mail.test.container.mc').id, 'alias_name': 'unused.test.alias' }) self.assertEqual(alias.alias_status, 'not_tested') # validation of alias_defaults with self.assertRaises(exceptions.ValidationError): alias.write({'alias_defaults': "{'custom_field': brokendict"}) alias.write({'alias_defaults': "{'custom_field': 'validdict'}"}) @tagged('mail_alias', 'multi_company') class TestAliasCompany(TestMailAliasCommon): """ Test company / alias domain and configuration synchronization """ def test_alias_domain_setup_archived_company(self): """Test initialization of alias domain with at least one archived company and at least one mail.alias record points to one mail.thread of the archived company""" # add archived company to multi company setup self.company_archived = self.env['res.company'].create({ 'country_id': self.env.ref('base.be').id, 'currency_id': self.env.ref('base.EUR').id, 'email': 'company_archived@test.example.com', 'name': 'Company Archived Test', }) self.company_archived.action_archive() # create record inheriting from mail.thread to be used as owner/target thread test_record_archived_company = self.env['mail.test.simple.unfollow'].create({ 'name': 'Test record (mail.thread) specific to archived company', 'company_id': self.company_archived.id, }) unfollow_model_id = self.env['ir.model']._get_id('mail.test.simple.unfollow') mc_archived_parent = self.env['mail.alias'].create({ 'alias_name': 'alias_parent_specific_to_archived_company', 'alias_parent_model_id': unfollow_model_id, 'alias_model_id': unfollow_model_id, 'alias_parent_thread_id': test_record_archived_company.id, }) # case where the parent thread is specific to archived company mc_archived_target = self.env['mail.alias'].create({ 'alias_name': 'alias_target_specific_to_archived_company', 'alias_parent_model_id': unfollow_model_id, 'alias_model_id': unfollow_model_id, 'alias_force_thread_id': test_record_archived_company.id, }) # case where the target thread is specific to archived company # eject linked aliases then remove all alias domains; should # trigger the init condition at next create() call all_mail_aliases = self.env['mail.alias'].search([]) all_mail_aliases.write({'alias_domain_id': False}) self.env['mail.alias.domain'].search([]).unlink() self.assertFalse(any(all_mail_aliases.mapped("alias_domain_id")), 'Mail aliases should have no linked alias domain at this stage') # since we nuked all alias domain records, creating a new alias domain # will initialize it as the default for all mail.alias records. # Should not raise any errors (see _check_alias_domain_id_mc) mc_alias_domain = self.env['mail.alias.domain'].create({ 'bounce_alias': 'bounce.mc.archived', 'catchall_alias': 'catchall.bounce.mc.archived', 'name': 'test.init.mc.archived.com', }) self.assertEqual(mc_archived_parent.alias_domain_id.id, mc_alias_domain.id, 'Parent thread has the wrong alias domain') self.assertEqual(mc_archived_target.alias_domain_id.id, mc_alias_domain.id, 'Target thread has the wrong alias domain') self.assertEqual(self.company_archived.alias_domain_id.id, mc_alias_domain.id, 'Archived company was attributed wrong alias domain') @mute_logger('odoo.models.unlink') @users('erp_manager') def test_alias_domain_setup(self): """ Test synchronization of alias domain with companies when adding / updating / removing alias domains """ mail_alias_domain = self.mail_alias_domain.with_env(self.env) mail_alias_domain_c2 = self.mail_alias_domain_c2.with_env(self.env) self.assertEqual(self.company_admin.alias_domain_id, mail_alias_domain) self.assertEqual(self.company_2.alias_domain_id, mail_alias_domain_c2) # cannot unlink alias domain as there are aliases linked to it with self.assertRaises(psycopg2.errors.ForeignKeyViolation), self.cr.savepoint(), mute_logger('odoo.sql_db'): mail_alias_domain.unlink() # eject linked aliases then remove alias domain of first company; should # not impact second company self.env['mail.alias'].sudo().search([]).write({'alias_domain_id': False}) mail_alias_domain.unlink() self.assertFalse(self.company_admin.alias_domain_id) self.assertEqual(self.company_2.alias_domain_id, mail_alias_domain_c2) self.assertFalse(self.test_alias_mc.alias_domain_id) # remove all alias domains self.env['mail.alias.domain'].search([]).unlink() self.assertFalse(self.company_2.alias_domain_id) self.assertEqual(self.company_2.bounce_email, '') self.assertEqual(self.company_2.bounce_formatted, '') self.assertEqual(self.company_2.catchall_email, '') self.assertEqual(self.company_2.catchall_formatted, '') self.assertFalse(self.company_2.default_from_email, '') self.assertFalse(self.company_3.alias_domain_id) # create a new alias domain -> consider as re-init, populate all companies alias_domain_new = self.env['mail.alias.domain'].create({ 'bounce_alias': 'bounce.new', 'catchall_alias': 'catchall.new', 'name': 'test.global.bitnurk.com', }) self.assertEqual(self.company_admin.alias_domain_id, alias_domain_new, 'MC Alias: first domain should populate void companies') self.assertEqual(self.company_2.alias_domain_id, alias_domain_new, 'MC Alias: should take alias domain with lower sequence') self.assertEqual(self.company_3.alias_domain_id, alias_domain_new, 'MC Alias: should take alias domain with lower sequence') self.assertEqual(self.test_alias_mc.alias_domain_id, alias_domain_new, 'MC Alias: first domain should populate void aliases') # manual update self.company_2.alias_domain_id = alias_domain_new.id self.assertEqual(self.company_2.alias_domain_id, alias_domain_new) self.assertEqual(self.company_2.bounce_email, 'bounce.new@test.global.bitnurk.com') self.assertEqual(self.company_2.catchall_email, 'catchall.new@test.global.bitnurk.com') def test_assert_initial_values(self): """ Test initial setup values: currently all companies share the same alias configuration as it is unique. """ self.assertEqual(self.test_alias_mc.alias_domain_id, self.mail_alias_domain) self.assertEqual(self.company_admin.alias_domain_id, self.mail_alias_domain) self.assertEqual(self.company_admin.bounce_email, f'{self.alias_bounce}@{self.alias_domain}') self.assertEqual( self.company_admin.bounce_formatted, formataddr((self.company_admin.name, f'{self.alias_bounce}@{self.alias_domain}')) ) self.assertEqual(self.company_admin.catchall_email, f'{self.alias_catchall}@{self.alias_domain}') self.assertEqual( self.company_admin.catchall_formatted, formataddr((self.company_admin.name, f'{self.alias_catchall}@{self.alias_domain}')) ) self.assertEqual(self.company_admin.default_from_email, f'{self.default_from}@{self.alias_domain}') self.assertEqual(self.company_2.alias_domain_id, self.mail_alias_domain_c2) self.assertEqual(self.company_2.bounce_email, f'{self.alias_bounce_c2}@{self.alias_domain_c2_name}') self.assertEqual( self.company_2.bounce_formatted, formataddr((self.company_2.name, f'{self.alias_bounce_c2}@{self.alias_domain_c2_name}')) ) self.assertEqual(self.company_2.catchall_email, f'{self.alias_catchall_c2}@{self.alias_domain_c2_name}') self.assertEqual( self.company_2.catchall_formatted, formataddr((self.company_2.name, f'{self.alias_catchall_c2}@{self.alias_domain_c2_name}')) ) self.assertEqual(self.company_2.default_from_email, f'{self.alias_default_from_c2}@{self.alias_domain_c2_name}') self.assertEqual(self.company_3.alias_domain_id, self.mail_alias_domain_c3) self.assertEqual(self.company_3.bounce_email, f'{self.alias_bounce_c3}@{self.alias_domain_c3_name}') self.assertEqual( self.company_3.bounce_formatted, formataddr((self.company_3.name, f'{self.alias_bounce_c3}@{self.alias_domain_c3_name}')) ) self.assertEqual(self.company_3.catchall_email, f'{self.alias_catchall_c3}@{self.alias_domain_c3_name}') self.assertEqual( self.company_3.catchall_formatted, formataddr((self.company_3.name, f'{self.alias_catchall_c3}@{self.alias_domain_c3_name}')) ) self.assertEqual(self.company_3.default_from_email, f'{self.alias_default_from_c3}@{self.alias_domain_c3_name}') @users('erp_manager') def test_res_company_creation_alias_domain(self): """ Test alias domain configuration when creating new companies """ company = self.env['res.company'].create({ 'email': '"Super Company" ', 'name': 'Super Company', }) company.flush_recordset() self.assertEqual( company.alias_domain_id, self.mail_alias_domain, 'Default alias domain: sequence based') # respect forced value company = self.env['res.company'].create({ 'alias_domain_id': self.mail_alias_domain_c2.id, 'email': '"Yet Another Company" ', 'name': 'Yet Another Company', }) self.assertEqual(company.alias_domain_id, self.mail_alias_domain_c2) @tagged('mail_gateway', 'mail_alias', 'multi_company') class TestMailAliasDomain(TestMailAliasCommon): @users('admin') def test_alias_domain_config_alias_clash(self): """ Domain names are not unique e.g. owning multiple gmail.com accounts. However bounce / catchall should not clash with aliases. """ alias_domain = self.mail_alias_domain.with_env(self.env) for domain_config in {'bounce_alias', 'catchall_alias'}: with self.subTest(domain_config=domain_config): with self.assertRaises(exceptions.ValidationError): self.env['mail.alias.domain'].create({ domain_config: self.test_alias_mc.alias_name, 'name': self.test_alias_mc.alias_domain_id.name, }) # left-part should not clech self.env['mail.alias.domain'].create({ domain_config: self.test_alias_mc.alias_name, 'name': 'another.domain.name.com', }) # should not clash with existing aliases, to avoid valid aliases be # considered as bounce / catchall with self.assertRaises(exceptions.UserError), self.cr.savepoint(): alias_domain.write({'bounce_alias': self.test_alias_mc.alias_name}) with self.assertRaises(exceptions.UserError), self.cr.savepoint(): alias_domain.write({'catchall_alias': self.test_alias_mc.alias_name}) @users('admin') def test_alias_domain_config_unique(self): """ Domain names are not unique e.g. owning multiple gmail.com accounts. However bounce / catchall should be unique. """ alias_domain = self.mail_alias_domain.with_env(self.env) # copying directly would duplicate bounce / catchall emails with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint(): new_alias_domain = alias_domain.copy() # same domain name is authorized if bounce and catchall are different new_alias_domain = alias_domain.copy({ 'bounce_alias': 'new.bounce', 'catchall_alias': 'new.catchall', }) self.assertEqual(new_alias_domain.bounce_email, f'new.bounce@{alias_domain.name}') self.assertEqual(new_alias_domain.catchall_email, f'new.catchall@{alias_domain.name}') self.assertEqual(new_alias_domain.name, alias_domain.name) # check bounce / catchall are unique at create self.env['mail.alias.domain'].create({ 'bounce_alias': 'unique.bounce', 'catchall_alias': 'unique.catchall', 'name': alias_domain.name, }) # any not unique should raise UniqueViolation (SQL constraint fired after check) with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint(): self.env['mail.alias.domain'].create({ 'bounce_alias': alias_domain.bounce_alias, 'name': alias_domain.name, }) with mute_logger('odoo.sql_db'), self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint(): self.env['mail.alias.domain'].create({ 'catchall_alias': alias_domain.catchall_alias, 'name': alias_domain.name, }) # also check write operation with self.assertRaises(exceptions.ValidationError): new_alias_domain.write({'bounce_alias': alias_domain.bounce_alias}) with self.assertRaises(exceptions.ValidationError): new_alias_domain.write({'catchall_alias': alias_domain.catchall_alias}) @users('admin') def test_alias_domain_parameters_validation(self): """ Test validation of bounce and catchall fields of alias domain as well as sanitization. """ alias_domain = self.mail_alias_domain.with_env(self.env) # sanitization of name (both create and write) for failing_name in [ 'outlook.fr, gmail.com', # accents 'provaïder', 'provaïder.cöm', # fail '', ' ', ]: with self.subTest(failing_name=failing_name): with self.assertRaises(exceptions.ValidationError): _new_domain = self.env['mail.alias.domain'].create({'name': failing_name}) with self.assertRaises(exceptions.ValidationError): alias_domain.write({'name': failing_name}) # sanitization of bounce / catchall for ( (bounce_alias, catchall_alias, default_from), (exp_bounce, exp_catchall, exp_default_from), (exp_bounce_email, exp_catchall_email, exp_default_from_email), ) in zip( [ ( 'bounce+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~', 'catchall+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~', 'notifications+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~', ), ('bounce+😊', 'catchall+😊', 'notifications+😊'), ('Bouncâïde 😊', 'Catchôïee 😊', 'Notificâtïons 😊'), ('ぁ', 'ぁぁ', 'ぁぁぁ'), # only default_from can be a valid email and taken as such ( 'bounce@wrong.complete.com', 'catchall@wrong.complete.com', 'notifications@valid.complete.com', ), ], [ ( 'bounce+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~', 'catchall+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~', 'notifications+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~', ), ('bounce+-', 'catchall+-', 'notifications+-'), ('bouncaide-', 'catchoiee-', 'notifications-'), ('?', '??', '???'), # only default_from can be a valid email and taken as such ( 'bounce', 'catchall', 'notifications@valid.complete.com', ), ], [ ( f'bounce+b4r=*r3wl_#_-$-{{}}-~|-/!?&%^\'-`~@{alias_domain.name}', f'catchall+b4r=*r3wl_#_-$-{{}}-~|-/!?&%^\'-`~@{alias_domain.name}', f'notifications+b4r=*r3wl_#_-$-{{}}-~|-/!?&%^\'-`~@{alias_domain.name}', ), ( f'bounce+-@{alias_domain.name}', f'catchall+-@{alias_domain.name}', f'notifications+-@{alias_domain.name}'), ( f'bouncaide-@{alias_domain.name}', f'catchoiee-@{alias_domain.name}', f'notifications-@{alias_domain.name}' ), ( f'?@{alias_domain.name}', f'??@{alias_domain.name}', f'???@{alias_domain.name}' ), # only default_from can be a valid email and taken as such ( f'bounce@{alias_domain.name}', f'catchall@{alias_domain.name}', 'notifications@valid.complete.com', ), ] ): with self.subTest(bounce_alias=bounce_alias): alias_domain.write({'bounce_alias': bounce_alias}) self.assertEqual(alias_domain.bounce_alias, exp_bounce) self.assertEqual(alias_domain.bounce_email, exp_bounce_email) with self.subTest(catchall_alias=catchall_alias): alias_domain.write({'catchall_alias': catchall_alias}) self.assertEqual(alias_domain.catchall_alias, exp_catchall) self.assertEqual(alias_domain.catchall_email, exp_catchall_email) with self.subTest(default_from=default_from): alias_domain.write({'default_from': default_from}) self.assertEqual(alias_domain.default_from, exp_default_from) self.assertEqual(alias_domain.default_from_email, exp_default_from_email) # falsy values for config_value in [False, None, '', ' ']: with self.subTest(config_value=config_value): alias_domain.write({'bounce_alias': config_value}) self.assertFalse(alias_domain.bounce_alias) alias_domain.write({'catchall_alias': config_value}) self.assertFalse(alias_domain.catchall_alias) alias_domain.write({'default_from': config_value}) self.assertFalse(alias_domain.default_from) # check successive param set, should not raise for unicity against itself for _ in range(2): alias_domain.write({ 'bounce_alias': 'bounce+double.test', 'catchall_alias': 'catchall+double.test', }) self.assertEqual(alias_domain.bounce_alias, 'bounce+double.test') self.assertEqual(alias_domain.catchall_alias, 'catchall+double.test') @tagged('mail_gateway', 'mail_alias', 'mail_alias_mixin', 'multi_company') class TestMailAliasMixin(TestMailAliasCommon): """ Test alias mixin implementation, synchronization of alias records based on owner records. """ @users('employee') @mute_logger('odoo.addons.base.models.ir_model') def test_alias_mixin(self): """ Various base checks on alias mixin behavior """ self.assertEqual(self.env.company.alias_domain_id, self.mail_alias_domain) record = self.env['mail.test.gateway.groups'].create({ 'name': 'Test Record', 'alias_name': 'alias.test', 'alias_contact': 'followers', }) self.assertEqual(record.alias_id.alias_domain_id, self.mail_alias_domain) self.assertEqual(record.alias_id.alias_model_id, self.env['ir.model']._get('mail.test.gateway.groups')) self.assertEqual(record.alias_id.alias_force_thread_id, record.id) self.assertEqual(record.alias_id.alias_parent_model_id, self.env['ir.model']._get('mail.test.gateway.groups')) self.assertEqual(record.alias_id.alias_parent_thread_id, record.id) self.assertEqual(record.alias_id.alias_name, 'alias.test') self.assertEqual(record.alias_id.alias_contact, 'followers') record.write({ 'alias_domain_id': self.mail_alias_domain_c2.id, 'alias_name': 'better.alias.test', 'alias_defaults': "{'default_name': 'defaults'}" }) self.assertEqual(record.alias_domain, self.mail_alias_domain_c2.name) self.assertEqual(record.alias_id.alias_name, 'better.alias.test') self.assertEqual(record.alias_id.alias_defaults, "{'default_name': 'defaults'}") with self.assertRaises(exceptions.AccessError): record.write({ 'alias_force_thread_id': 0, }) with self.assertRaises(exceptions.AccessError): record.write({ 'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id, }) with self.assertRaises(exceptions.ValidationError): record.write({'alias_defaults': "{'custom_field': brokendict"}) rec = self.env['mail.test.gateway.groups'].create({ 'name': 'Test Record2', 'alias_name': 'alias.test', 'alias_domain_id': self.mail_alias_domain_c2.id, }) self.assertEqual(rec.alias_id.alias_domain_id, self.mail_alias_domain_c2, "Should use the provided alias domain in priority") @users('erp_manager') def test_alias_mixin_alias_email(self): """ Test 'alias_email' mixin field computation and search capability """ Model = self.env['mail.test.container.mc'] records = Model.create([ { 'alias_name': f'alias.email.{idx}', # will be present in all companies 'company_id': company.id, 'name': f'Test {company.id} {idx}', } for company in (self.company_admin, self.company_2) for idx in range(5) ]) self.assertEqual( Model.search([('alias_email', 'ilike', 'alias.email')]), records, 'Search: partial search: any domain, matching all left parts') self.assertEqual( Model.search([('alias_email', 'ilike', 'alias.email.0')]), records[0] + records[5], 'Search: partial search: any domain, matching some left parts') self.assertEqual( Model.search([('alias_email', '=', self.mail_alias_domain.name)]), Model, 'Search: partial search: does not match any complete email') self.assertEqual( Model.search([('alias_email', '=', f'alias.email.1@{self.mail_alias_domain.name}')]), records[1], 'Search: both part search: search on name + domain') @users('employee') @mute_logger('odoo.addons.base.models.ir_model') def test_alias_mixin_alias_id_management(self): """ Test alias_id being not mandatory """ record_wo_alias, record_w_alias = self.env['mail.test.alias.optional'].create([ { 'name': 'Test WoAlias Name', }, { 'alias_name': 'Alias Name', 'name': 'Test WoAlias Name', } ]) self.assertFalse(record_wo_alias.alias_id, 'Alias record not created if not necessary (no alias_name)') self.assertFalse(record_wo_alias.alias_id.alias_name) self.assertFalse(record_wo_alias.alias_id.alias_defaults) self.assertFalse(record_wo_alias.alias_name) self.assertTrue(record_w_alias.alias_id, 'Alias record created as alias_name was given') self.assertEqual(record_w_alias.alias_id.alias_name, 'alias-name', 'Alias name should go through sanitize') self.assertEqual( literal_eval(record_w_alias.alias_id.alias_defaults), {'company_id': self.env.company.id} ) self.assertEqual(record_w_alias.alias_name, 'alias-name', 'Alias name should go through sanitize') self.assertEqual( literal_eval(record_w_alias.alias_defaults), {'company_id': self.env.company.id} ) # update existing alias record_w_alias.write({'alias_contact': 'followers', 'alias_name': 'Updated Alias Name'}) self.assertEqual(record_w_alias.alias_id.alias_contact, 'followers') self.assertEqual(record_w_alias.alias_id.alias_name, 'updated-alias-name') self.assertEqual(record_w_alias.alias_name, 'updated-alias-name') # update non existing alias -> creates alias record_wo_alias.write({'alias_name': 'trying a name'}) self.assertTrue(record_wo_alias.alias_id, 'Alias record should have been created to store the name') self.assertEqual(record_wo_alias.alias_id.alias_name, 'trying-a-name') self.assertEqual( literal_eval(record_wo_alias.alias_id.alias_defaults), {'company_id': self.env.company.id} ) self.assertEqual(record_wo_alias.alias_name, 'trying-a-name') self.assertEqual( literal_eval(record_wo_alias.alias_defaults), {'company_id': self.env.company.id} ) # reset alias -> keep the alias as void, don't remove it existing_aliases = record_wo_alias.alias_id + record_w_alias.alias_id (record_wo_alias + record_w_alias).write({'alias_name': False}) self.assertEqual((record_wo_alias + record_w_alias).alias_id, existing_aliases) self.assertFalse(list(filter(None, existing_aliases.mapped('alias_name')))) @users('employee') def test_copy_content(self): self.assertFalse( self.env.user.has_group('base.group_system'), 'Test user should not have Administrator access') record = self.env['mail.test.gateway.groups'].create({ 'name': 'Test Record', 'alias_name': 'test.record', 'alias_contact': 'followers', 'alias_bounced_content': False, }) record_alias = record.alias_id self.assertFalse(record.alias_bounced_content) record_copy = record.copy() record_alias_copy = record_copy.alias_id self.assertNotEqual(record_alias, record_alias_copy) self.assertEqual(record_alias.alias_force_thread_id, record.id) self.assertEqual(record_alias_copy.alias_force_thread_id, record_copy.id) self.assertFalse(record_copy.alias_bounced_content) self.assertEqual(record_copy.alias_contact, record.alias_contact) self.assertFalse(record_copy.alias_name, 'Copy should not duplicate name') new_content = '

Bounced Content

' record_copy.write({'alias_bounced_content': new_content}) self.assertEqual(record_copy.alias_bounced_content, new_content) record_copy2 = record_copy.copy() self.assertEqual(record_copy2.alias_bounced_content, new_content) @users('employee') def test_copy_optional_alias_model(self): """ Do not propagate alias_id to duplicate record as it could lead to overwriting alias_name of old record. """ record = self.env['mail.test.alias.optional'].create({ 'name': 'Test Optional Alias Record', 'alias_name': 'test.optional.alias.record', }) self.assertTrue(record.alias_id) record_copy = record.copy() self.assertFalse(record_copy.alias_id) @users('erp_manager') def test_multi_company_setup(self): """ Test company impact on alias domains when creating or updating records in a MC environment. """ counter = 0 for create_cid, exp_company, exp_alias_domain in [ (None, self.company_2, self.mail_alias_domain_c2), (False, self.env['res.company'], self.mail_alias_domain_c2), (self.env.user.company_id.id, self.company_2, self.mail_alias_domain_c2), (self.company_admin.id, self.company_admin, self.mail_alias_domain), ]: with self.subTest(create_cid=create_cid, exp_company=exp_company, exp_alias_domain=exp_alias_domain): counter += 1 base_values = { 'name': f'Test Record {counter}', 'alias_name': f'alias.test.{counter}', 'alias_contact': 'followers', } if create_cid is not None: base_values['company_id'] = create_cid record = self.env['mail.test.container.mc'].create(base_values) self.assertEqual(record.alias_domain_id, exp_alias_domain) self.assertEqual(record.company_id, exp_company) # copy: keep company record_copy = record.copy( default={ 'alias_name': f'alias.copy.{counter}', 'name': f'Copy of {record.name}', } ) self.assertEqual(record_copy.alias_domain_id, exp_alias_domain) self.assertEqual(record_copy.company_id, record.company_id) # copy: force company record_copy_2 = record.copy( default={ 'alias_name': f'alias.copy.{counter}.2', 'company_id': self.company_admin.id, 'name': f'Copy 2 of {record.name}', } ) self.assertEqual(record_copy_2.alias_domain_id, self.mail_alias_domain) self.assertEqual(record_copy_2.company_id, self.company_admin) # updating company: force same alias domain record.write({'company_id': self.company_admin.id}) self.assertEqual(record.alias_domain_id, self.mail_alias_domain) self.assertEqual(record.company_id, self.company_admin) # reset company: should not impact alias_domain if set record.write({'company_id': False}) self.assertEqual(record.alias_domain_id, self.mail_alias_domain) self.assertFalse(record.company_id)