[IMP] test_mail{..mailing}: improve tooling (recipient / email check)

Improve low-level checks of recipients in mail asserts. Sometimes 'email_to'
cannot be easily deduced from given input (partners, records customers, ...)
when some record -> email transformations are involved e.g. when dealing
with multiple emails input, double encapsulation, ...

Those tools are about to be used in upcoming tests and fixes.

Task-3438381 (TestMail: backport tools)
Prepares Task-2612945 (Mail: Defensive email formatting)

X-original-commit: bdbe846329ed51e4c5e3b75740e8f594edf7b138
Part-of: odoo/odoo#129839
This commit is contained in:
Thibault Delavallée
2023-07-27 14:47:16 +02:00
parent 4257ae03cd
commit e86ae05208
2 changed files with 94 additions and 31 deletions
+69 -25
View File
@@ -270,7 +270,13 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
if mail.id == mail_id:
break
else:
raise AssertionError('mail.mail not found for ID %s' % (mail_id))
debug_info = '\n'.join(
f'From: {mail.author_id} ({mail.email_from}) - ID {mail.id} (State: {mail.state})'
for mail in filtered
)
raise AssertionError(
f'mail.mail not found for ID {mail_id} / message {mail_message} / status {status} / author {author}\n{debug_info}'
)
return mail
def _find_mail_mail_wpartners(self, recipients, status, mail_message=None, author=None):
@@ -288,7 +294,14 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
if all(p in mail.recipient_ids for p in recipients):
break
else:
raise AssertionError('mail.mail not found for message %s / status %s / recipients %s / author %s' % (mail_message, status, recipients.ids, author))
debug_info = '\n'.join(
f'From: {mail.author_id} ({mail.email_from}) - To: {sorted(mail.recipient_ids.ids)} (State: {mail.state})'
for mail in filtered
)
recipients_info = f'Missing: {[r.name for r in recipients if r.id not in filtered.recipient_ids.ids]}'
raise AssertionError(
f'mail.mail not found for message {mail_message} / status {status} / recipients {sorted(recipients.ids)} / author {author}\n{recipients_info}\n{debug_info}'
)
return mail
def _find_mail_mail_wemail(self, email_to, status, mail_message=None, author=None):
@@ -306,7 +319,13 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
if (mail.email_to == email_to and not mail.recipient_ids) or (not mail.email_to and mail.recipient_ids.email == email_to):
break
else:
raise AssertionError('mail.mail not found for email_to %s / status %s in %s' % (email_to, status, repr([m.email_to for m in self._new_mails])))
debug_info = '\n'.join(
f'From: {mail.author_id} ({mail.email_from}) - To: {mail.email_to} / {sorted(mail.recipient_ids.mapped("email"))} (State: {mail.state})'
for mail in filtered
)
raise AssertionError(
f'mail.mail not found for message {mail_message} / status {status} / email_to {email_to} / author {author}\n{debug_info}'
)
return mail
def _find_mail_mail_wrecord(self, record, status=None, mail_message=None, author=None):
@@ -319,7 +338,13 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
if mail.model == record._name and mail.res_id == record.id:
break
else:
raise AssertionError('mail.mail not found for record %s in %s' % (record, repr([m.email_to for m in self._new_mails])))
debug_info = '\n'.join(
f'From: {mail.author_id} ({mail.email_from}) - Model{mail.model} / ResId {mail.res_id} (State: {mail.state})'
for mail in filtered
)
raise AssertionError(
f'mail.mail not found for message {mail_message} / status {status} / record {record.model}, {record.id} / author {author}\n{debug_info}'
)
return mail
def _find_sent_email(self, email_from, emails_to, subject=None):
@@ -343,8 +368,8 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
# GATEWAY ASSERTS
# ------------------------------------------------------------
def _assertMailMail(self, mail, recipients_list,
status, author=None,
def _assertMailMail(self, mail, recipients_list, status,
email_to_recipients=None, author=None,
content=None, fields_values=None, email_values=None):
""" Assert mail.mail record values and maybe related emails. Allow
asserting their content. Records to check are the one generated when
@@ -356,6 +381,9 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
``_find_mail_mail_wemail``);
:param status: mail.mail state used to filter mails. If ``sent`` this method
also check that emails have been sent trough gateway;
:param email_to_recipients: used for assertSentEmail to find email based
on 'email_to' when doing the match directly based on recipients_list
being partners it nos easy (e.g. multi emails, ...);
:param author: see ``_find_mail_mail_wpartners``;
:param content: if given, check it is contained within mail html body;
:param fields_values: if given, should be a dictionary of field names /
@@ -375,15 +403,20 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
'Mail: expected %s for %s, got %s' % (fvalue, fname, mail[fname])
)
if status == 'sent':
for recipient in recipients_list:
if email_to_recipients:
recipients = email_to_recipients # already formatted
else:
recipients = [[r] for r in recipients_list] # one partner -> list of a single email
for recipient in recipients:
with self.subTest(recipient=recipient):
self.assertSentEmail(
email_values['email_from'] if email_values and email_values.get('email_from') else author,
[recipient],
recipient,
**(email_values or {})
)
def assertMailMail(self, recipients, status,
email_to_recipients=None,
mail_message=None, author=None,
content=None, fields_values=None, email_values=None):
""" Assert mail.mail records are created and maybe sent as emails. This
@@ -399,13 +432,15 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
found_mail = self._find_mail_mail_wpartners(recipients, status, mail_message=mail_message, author=author)
self.assertTrue(bool(found_mail))
self._assertMailMail(
found_mail, recipients,
status, author=author,
content=content, fields_values=fields_values, email_values=email_values,
found_mail, recipients, status,
email_to_recipients=email_to_recipients,
author=author, content=content,
fields_values=fields_values, email_values=email_values,
)
return found_mail
def assertMailMailWEmails(self, emails, status,
email_to_recipients=None,
mail_message=None, author=None,
content=None, fields_values=None, email_values=None):
""" Assert mail.mail records are created and maybe sent as emails. This
@@ -422,13 +457,15 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
found_mail = self._find_mail_mail_wemail(email_to, status, mail_message=mail_message, author=author)
self.assertTrue(bool(found_mail))
self._assertMailMail(
found_mail, [email_to],
status, author=author,
content=content, fields_values=fields_values, email_values=email_values,
found_mail, [email_to], status,
email_to_recipients=email_to_recipients,
author=author, content=content,
fields_values=fields_values, email_values=email_values,
)
return found_mail
def assertMailMailWRecord(self, record, recipients, status,
email_to_recipients=None,
mail_message=None, author=None,
content=None, fields_values=None, email_values=None):
""" Assert mail.mail records are created and maybe sent as emails. This
@@ -444,13 +481,15 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
found_mail = self._find_mail_mail_wrecord(record, mail_message=mail_message, author=author)
self.assertTrue(bool(found_mail))
self._assertMailMail(
found_mail, recipients,
status, author=author,
content=content, fields_values=fields_values, email_values=email_values,
found_mail, recipients, status,
email_to_recipients=email_to_recipients,
author=author, content=content,
fields_values=fields_values, email_values=email_values,
)
return found_mail
def assertMailMailWId(self, mail_id, status,
email_to_recipients=None,
author=None,
content=None, fields_values=None):
""" Assert mail.mail records are created and maybe sent as emails. Allow
@@ -466,8 +505,10 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
self.assertTrue(bool(found_mail))
self._assertMailMail(
found_mail, [], # generally used when recipients are Falsy
status, author=author,
content=content, fields_values=fields_values,
status,
email_to_recipients=email_to_recipients,
author=author, content=content,
fields_values=fields_values,
)
return found_mail
@@ -546,12 +587,15 @@ class MockEmail(common.BaseCase, MockSmtplibCase):
else:
expected['email_from'] = author
email_to_list = []
for email_to in recipients:
if isinstance(email_to, self.env['res.partner'].__class__):
email_to_list.append(formataddr((email_to.name, email_to.email)))
else:
email_to_list.append(email_to)
if 'email_to' in values:
email_to_list = values['email_to']
else:
email_to_list = []
for email_to in recipients:
if isinstance(email_to, self.env['res.partner'].__class__):
email_to_list.append(formataddr((email_to.name, email_to.email or 'False')))
else:
email_to_list.append(email_to)
expected['email_to'] = email_to_list
# fetch mail
+25 -6
View File
@@ -36,8 +36,8 @@ class MassMailCase(MailCase, MockLinkTracker):
)
def assertMailTraces(self, recipients_info, mailing, records,
check_mail=True, sent_unlink=False, author=None,
mail_links_info=None):
check_mail=True, sent_unlink=False,
author=None, mail_links_info=None):
""" Check content of traces. Traces are fetched based on a given mailing
and records. Their content is compared to recipients_info structure that
holds expected information. Links content may be checked, notably to
@@ -52,6 +52,7 @@ class MassMailCase(MailCase, MockLinkTracker):
'record: linked record,
# MAIL.MAIL
'content': optional content that should be present in mail.mail body_html;
'email_to_recipients': optional, see '_assertMailMail';
'failure_type': optional failure reason;
}, { ... }]
@@ -82,6 +83,12 @@ class MassMailCase(MailCase, MockLinkTracker):
('mass_mailing_id', 'in', mailing.ids),
('res_id', 'in', records.ids)
])
debug_info = '\n'.join(
(
f'Trace: to {t.email} - state {t.trace_status}'
for t in traces
)
)
# ensure trace coherency
self.assertTrue(all(s.model == records._name for s in traces))
@@ -93,6 +100,7 @@ class MassMailCase(MailCase, MockLinkTracker):
for recipient_info, link_info, record in zip(recipients_info, mail_links_info, records):
partner = recipient_info.get('partner', self.env['res.partner'])
email = recipient_info.get('email')
email_to_recipients = recipient_info.get('email_to_recipients')
status = recipient_info.get('trace_status', 'sent')
record = record or recipient_info.get('record')
content = recipient_info.get('content')
@@ -106,7 +114,9 @@ class MassMailCase(MailCase, MockLinkTracker):
)
self.assertTrue(
len(recipient_trace) == 1,
'MailTrace: email %s (recipient %s, status: %s, record: %s): found %s records (1 expected)' % (email, partner, status, record, len(recipient_trace))
'MailTrace: email %s (recipient %s, status: %s, record: %s): found %s records (1 expected)\n%s' % (
email, partner, status, record,
len(recipient_trace), debug_info)
)
self.assertTrue(bool(recipient_trace.mail_mail_id_int))
if 'failure_type' in recipient_info or status in ('error', 'cancel', 'bounce'):
@@ -131,18 +141,27 @@ class MassMailCase(MailCase, MockLinkTracker):
else:
self.assertMailMail(
partner, state_mapping[status],
author=author, content=content, fields_values=fields_values
author=author,
content=content,
email_to_recipients=email_to_recipients,
fields_values=fields_values,
)
# specific if email is False -> could have troubles finding it if several falsy traces
elif not email and status in ('cancel', 'bounce'):
self.assertMailMailWId(
recipient_trace.mail_mail_id_int, state_mapping[status],
author=author, content=content, fields_values=fields_values
author=author,
content=content,
email_to_recipients=email_to_recipients,
fields_values=fields_values,
)
else:
self.assertMailMailWEmails(
[email], state_mapping[status],
author=author, content=content, fields_values=fields_values
author=author,
content=content,
email_to_recipients=email_to_recipients,
fields_values=fields_values,
)
if link_info: