19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:39 +01:00
parent 38c6088dcc
commit d9452d2060
243 changed files with 30797 additions and 10815 deletions

View file

@ -1,10 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_odoobot
from . import test_controller_attachment
from . import test_controller_reaction
from . import test_controller_update
from . import test_controller_thread
from . import test_ir_mail_server
from . import test_mail_bot
from . import test_mail_performance
from . import test_mail_thread_internals
from . import test_mass_mailing
from . import test_portal
from . import test_rating
from . import test_res_users
from . import test_ui

View file

@ -0,0 +1,39 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
from odoo.addons.mail.tests.common_controllers import MailControllerAttachmentCommon
@odoo.tests.tagged("-at_install", "post_install", "mail_controller")
class TestPortalAttachmentController(MailControllerAttachmentCommon):
def test_attachment_upload_portal(self):
"""Test access to upload an attachment on portal"""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, _ = self._get_sign_token_params(record)
self._execute_subtests_upload(
record,
(
(self.user_public, False),
(self.user_public, True, token),
(self.user_public, True, sign),
(self.guest, False),
(self.guest, True, token),
(self.guest, True, sign),
(self.user_portal, False),
(self.user_portal, False, bad_token),
(self.user_portal, False, bad_sign),
(self.user_portal, True, token),
(self.user_portal, True, sign),
(self.user_employee, True),
(self.user_employee, True, bad_token),
(self.user_employee, True, bad_sign),
(self.user_employee, True, token),
(self.user_employee, True, sign),
(self.user_admin, True),
(self.user_admin, True, bad_token),
(self.user_admin, True, bad_sign),
(self.user_admin, True, token),
(self.user_admin, True, sign),
),
)

View file

@ -0,0 +1,80 @@
from odoo.addons.mail.tests.common_controllers import MailControllerReactionCommon
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestPortalMessageReactionController(MailControllerReactionCommon):
def test_message_reaction_nomsg(self):
"""Test access of message reaction for a non-existing message."""
self._execute_subtests(
self.fake_message,
((user, False) for user in [self.user_public, self.guest, self.user_portal, self.user_employee]),
)
def test_message_reaction_portal_no_partner(self):
"""Test access of message reaction for portal without partner."""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
message = record.message_post(body="portal no partner")
self._execute_subtests(
message,
(
(self.user_public, False),
(self.user_public, False, bad_token),
(self.user_public, False, bad_sign),
# False because no portal partner, no guest
(self.user_public, False, token),
(self.user_public, True, sign, {"partner": partner}),
(self.guest, False),
(self.guest, False, bad_token),
(self.guest, False, bad_sign),
(self.guest, True, token),
(self.guest, True, sign, {"partner": partner}),
(self.user_portal, False),
(self.user_portal, False, bad_token),
(self.user_portal, False, bad_sign),
(self.user_portal, True, token),
(self.user_portal, True, sign),
(self.user_employee, True),
(self.user_employee, True, bad_token),
(self.user_employee, True, bad_sign),
(self.user_employee, True, token),
(self.user_employee, True, sign),
),
)
def test_message_reaction_portal_assigned_partner(self):
"""Test access of message reaction for portal with partner."""
rec_partner = self.env["res.partner"].create({"name": "Record Partner"})
record = self.env["mail.test.portal"].create({"name": "Test", "partner_id": rec_partner.id})
message = record.message_post(body="portal with partner")
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
self._execute_subtests(
message,
(
(self.user_public, False),
(self.user_public, False, bad_token),
(self.user_public, False, bad_sign),
(self.user_public, True, token, {"partner": rec_partner}),
(self.user_public, True, sign, {"partner": partner}),
# sign has priority over token when both are provided
(self.user_public, True, token | sign, {"partner": partner}),
(self.guest, False),
(self.guest, False, bad_token),
(self.guest, False, bad_sign),
(self.guest, True, token, {"partner": rec_partner}),
(self.guest, True, sign, {"partner": partner}),
(self.guest, True, token | sign, {"partner": partner}),
(self.user_portal, False),
(self.user_portal, False, bad_token),
(self.user_portal, False, bad_sign),
(self.user_portal, True, token),
(self.user_portal, True, sign),
(self.user_employee, True),
(self.user_employee, True, bad_token),
(self.user_employee, True, bad_sign),
(self.user_employee, True, token),
(self.user_employee, True, sign),
),
)

View file

@ -0,0 +1,171 @@
from odoo.addons.mail.tests.common_controllers import MailControllerThreadCommon, MessagePostSubTestData
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestPortalThreadController(MailControllerThreadCommon):
def test_message_post_portal_no_partner(self):
"""Test access of message post for portal without partner."""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
def test_access(user, allowed, route_kw=None, exp_author=None):
return MessagePostSubTestData(user, allowed, route_kw=route_kw, exp_author=exp_author)
self._execute_message_post_subtests(
record,
(
test_access(self.user_public, False),
test_access(self.user_public, False, route_kw=bad_token),
test_access(self.user_public, False, route_kw=bad_sign),
test_access(self.user_public, True, route_kw=token),
test_access(self.user_public, True, route_kw=sign, exp_author=partner),
test_access(self.guest, False),
test_access(self.guest, False, route_kw=bad_token),
test_access(self.guest, False, route_kw=bad_sign),
test_access(self.guest, True, route_kw=token),
test_access(self.guest, True, route_kw=sign, exp_author=partner),
test_access(self.user_portal, False),
test_access(self.user_portal, False, route_kw=bad_token),
test_access(self.user_portal, False, route_kw=bad_sign),
test_access(self.user_portal, True, route_kw=token),
test_access(self.user_portal, True, route_kw=sign),
test_access(self.user_employee, True),
test_access(self.user_employee, True, route_kw=bad_token),
test_access(self.user_employee, True, route_kw=bad_sign),
test_access(self.user_employee, True, route_kw=token),
test_access(self.user_employee, True, route_kw=sign),
),
)
def test_message_post_portal_with_partner(self):
"""Test access of message post for portal with partner."""
rec_partner = self.env["res.partner"].create({"name": "Record Partner"})
record = self.env["mail.test.portal"].create({"name": "Test", "partner_id": rec_partner.id})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
def test_access(user, allowed, route_kw=None, exp_author=None):
return MessagePostSubTestData(user, allowed, route_kw=route_kw, exp_author=exp_author)
self._execute_message_post_subtests(
record,
(
test_access(self.user_public, False),
test_access(self.user_public, False, route_kw=bad_token),
test_access(self.user_public, False, route_kw=bad_sign),
test_access(self.user_public, True, route_kw=token, exp_author=rec_partner),
test_access(self.user_public, True, route_kw=sign, exp_author=partner),
# sign has priority over token when both are provided
test_access(self.user_public, True, route_kw=token | sign, exp_author=partner),
test_access(self.guest, False),
test_access(self.guest, False, route_kw=bad_token),
test_access(self.guest, False, route_kw=bad_sign),
test_access(self.guest, True, route_kw=token, exp_author=rec_partner),
test_access(self.guest, True, route_kw=sign, exp_author=partner),
test_access(self.guest, True, route_kw=token | sign, exp_author=partner),
test_access(self.user_portal, False),
test_access(self.user_portal, False, route_kw=bad_token),
test_access(self.user_portal, False, route_kw=bad_sign),
test_access(self.user_portal, True, route_kw=token),
test_access(self.user_portal, True, route_kw=sign),
test_access(self.user_employee, True),
test_access(self.user_employee, True, route_kw=bad_token),
test_access(self.user_employee, True, route_kw=bad_sign),
test_access(self.user_employee, True, route_kw=token),
test_access(self.user_employee, True, route_kw=sign),
),
)
def test_message_post_partner_ids_mention_token(self):
"""Test partner_ids of message_post for portal record without partner.
All users are allowed to mention with specific message_mention token."""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
all_partners = (
self.user_portal + self.user_employee + self.user_admin
).partner_id
record.message_subscribe(partner_ids=self.user_employee.partner_id.ids)
def test_partners(user, allowed, exp_partners, route_kw=None, exp_author=None):
return MessagePostSubTestData(
user,
allowed,
partners=all_partners,
route_kw=route_kw,
exp_author=exp_author,
exp_partners=exp_partners,
add_mention_token=True,
)
self._execute_message_post_subtests(
record,
(
test_partners(self.user_public, False, all_partners),
test_partners(self.user_public, False, all_partners, route_kw=bad_token),
test_partners(self.user_public, False, all_partners, route_kw=bad_sign),
test_partners(self.user_public, True, all_partners, route_kw=token),
test_partners(self.user_public, True, all_partners, route_kw=sign, exp_author=partner),
test_partners(self.guest, False, all_partners),
test_partners(self.guest, False, all_partners, route_kw=bad_token),
test_partners(self.guest, False, all_partners, route_kw=bad_sign),
test_partners(self.guest, True, all_partners, route_kw=token),
test_partners(self.guest, True, all_partners, route_kw=sign, exp_author=partner),
test_partners(self.user_portal, False, all_partners),
test_partners(self.user_portal, False, all_partners, route_kw=bad_token),
test_partners(self.user_portal, False, all_partners, route_kw=bad_sign),
test_partners(self.user_portal, True, all_partners, route_kw=token),
test_partners(self.user_portal, True, all_partners, route_kw=sign),
test_partners(self.user_employee, True, all_partners),
test_partners(self.user_employee, True, all_partners, route_kw=bad_token),
test_partners(self.user_employee, True, all_partners, route_kw=bad_sign),
test_partners(self.user_employee, True, all_partners, route_kw=token),
test_partners(self.user_employee, True, all_partners, route_kw=sign),
),
)
def test_message_post_partner_ids_portal(self):
"""Test partner_ids of message_post for portal record without partner.
Only internal users are allowed to mention without specific message_mention token."""
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, partner = self._get_sign_token_params(record)
all_partners = (
self.user_portal + self.user_employee + self.user_admin
).partner_id
record.message_subscribe(partner_ids=self.user_employee.partner_id.ids)
def test_partners(user, allowed, exp_partners, route_kw=None, exp_author=None):
return MessagePostSubTestData(
user,
allowed,
partners=all_partners,
route_kw=route_kw,
exp_author=exp_author,
exp_partners=exp_partners,
)
self._execute_message_post_subtests(
record,
(
test_partners(self.user_public, False, self.env["res.partner"]),
test_partners(self.user_public, False, self.env["res.partner"], route_kw=bad_token),
test_partners(self.user_public, False, self.env["res.partner"], route_kw=bad_sign),
test_partners(self.user_public, True, self.env["res.partner"], route_kw=token),
test_partners(self.user_public, True, self.env["res.partner"], route_kw=sign, exp_author=partner),
test_partners(self.guest, False, self.env["res.partner"]),
test_partners(self.guest, False, self.env["res.partner"], route_kw=bad_token),
test_partners(self.guest, False, self.env["res.partner"], route_kw=bad_sign),
test_partners(self.guest, True, self.env["res.partner"], route_kw=token),
test_partners(self.guest, True, self.env["res.partner"], route_kw=sign, exp_author=partner),
test_partners(self.user_portal, False, self.env["res.partner"]),
test_partners(self.user_portal, False, self.env["res.partner"], route_kw=bad_token),
test_partners(self.user_portal, False, self.env["res.partner"], route_kw=bad_sign),
test_partners(self.user_portal, True, self.env["res.partner"], route_kw=token),
test_partners(self.user_portal, True, self.env["res.partner"], route_kw=sign),
test_partners(self.user_employee, True, all_partners),
test_partners(self.user_employee, True, all_partners, route_kw=bad_token),
test_partners(self.user_employee, True, all_partners, route_kw=bad_sign),
test_partners(self.user_employee, True, all_partners, route_kw=token),
test_partners(self.user_employee, True, all_partners, route_kw=sign),
),
)

View file

@ -0,0 +1,48 @@
from odoo.addons.mail.tests.common_controllers import MailControllerUpdateCommon
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestPortalMessageUpdateController(MailControllerUpdateCommon):
def test_message_update_no_message(self):
"""Test update a non-existing message."""
self._execute_subtests(
self.fake_message,
((user, False) for user in [self.guest, self.user_admin, self.user_employee, self.user_portal, self.user_public]),
)
def test_message_update_portal(self):
"""Test only admin and author can modify content of a message, works if
author is a portal user. """
record = self.env["mail.test.portal.no.partner"].create({"name": "Test"})
token, bad_token, sign, bad_sign, _ = self._get_sign_token_params(record)
message = record.message_post(
body=self.message_body,
author_id=self.user_portal.partner_id.id,
message_type="comment",
)
self._execute_subtests(
message,
(
(self.user_public, False),
(self.user_public, False, token),
(self.user_public, False, sign),
(self.guest, False),
(self.guest, False, token),
(self.guest, False, sign),
(self.user_portal, False),
(self.user_portal, False, bad_token),
(self.user_portal, False, bad_sign),
(self.user_portal, True, token),
(self.user_portal, True, sign),
(self.user_employee, False),
(self.user_employee, False, token),
(self.user_employee, False, sign),
(self.user_admin, True),
(self.user_admin, True, bad_token),
(self.user_admin, True, bad_sign),
(self.user_admin, True, token),
(self.user_admin, True, sign),
),
)

View file

@ -0,0 +1,122 @@
from contextlib import contextmanager
from unittest.mock import patch
from odoo.addons.mail.tests.common import MailCommon
from odoo.tests import tagged, users
from odoo.addons.base.models.ir_mail_server import IrMail_Server
from odoo.exceptions import UserError, ValidationError
@tagged('mail_server')
class TestIrMailServerPersonal(MailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['ir.config_parameter'].sudo().set_param('mail.disable_personal_mail_servers', False)
cls.user_admin.email = 'admin@test.lan'
cls.user_employee.email = 'employee@test.lan'
cls.user_employee.group_ids += cls.env.ref('mass_mailing.group_mass_mailing_user')
cls.test_partner = cls.env['res.partner'].create({
'name': 'test partner', 'email': 'test.partner@test.lan'
})
cls.mail_server_user.write({
'from_filter': cls.user_employee.email,
'owner_user_id': cls.user_employee,
'smtp_user': cls.user_employee.email,
})
cls.user_employee.invalidate_recordset(['outgoing_mail_server_id'])
@contextmanager
def mock_mail_connect(self):
original_connect = IrMail_Server._connect__
self.connected_server_ids = []
def patched_connect(mail_server, *args, **kwargs):
self.connected_server_ids.append(kwargs.get('mail_server_id'))
original_connect(mail_server, *args, **kwargs)
with patch.object(IrMail_Server, '_connect__', autospec=True, wraps=IrMail_Server, side_effect=patched_connect):
yield
@users('admin', 'employee')
def test_personal_mail_server_allowed_post(self):
"""Check that only the owner of the mail server can create mails that will be sent from it."""
test_record = self.test_partner.with_user(self.env.user)
with self.mock_mail_connect():
test_record.message_post(
body='hello',
author_id=self.user_employee.partner_id.id, email_from=self.user_employee.email,
partner_ids=test_record.ids,
)
self.assertEqual(len(self.connected_server_ids), 1)
if self.env.user == self.mail_server_user.owner_user_id:
self.assertEqual(self.connected_server_ids[0], self.mail_server_user.id)
else:
self.assertNotEqual(self.connected_server_ids[0], self.mail_server_user.id)
# check disallowed exceptions
if self.env.user != self.mail_server_user.owner_user_id:
# check raise on invalid server at create
with self.assertRaises(ValidationError):
test_record.message_post(
body='hello',
author_id=self.user_employee.partner_id.id, email_from=self.user_employee.email,
mail_server_id=self.mail_server_user.id,
partner_ids=test_record.ids,
)
# check raise on invalid server at send (should not happen in normal flow)
mail = self.env['mail.mail'].sudo().create({
'body_html': 'hello',
'email_from': self.user_employee.email,
'author_id': self.user_employee.partner_id.id,
'partner_ids': test_record.ids,
})
with self.mock_mail_gateway(), self.assertRaisesRegex(UserError, "Unauthorized server for some of the sending mails."):
mail._send(self, mail_server=self.mail_server_user)
def test_personal_mail_server_find_mail_server(self):
"""Check that _find_mail_server only finds 'public' servers unless otherwise allowed."""
IrMailServer = self.env['ir.mail_server']
all_servers = IrMailServer.search([])
test_cases = [
(None, False),
(all_servers, True),
]
for mail_servers, should_find_personal in test_cases:
with self.subTest(mail_servers=mail_servers):
found_server, found_email_from = IrMailServer._find_mail_server(self.user_employee.email, mail_servers=mail_servers)
if should_find_personal:
self.assertEqual(
(found_server, found_email_from), (self.mail_server_user, self.user_employee.email),
'Passing in a server that is owned should allow finding it.'
)
else:
self.assertNotEqual(
found_server, self.mail_server_user,
'Finding a server for an email_from without specifying a list of servers should not find owned servers.'
)
@users('employee')
def test_immutable_create_uid(self):
"""Make sure create_uid is not writable, as it's a security assumption for these tests."""
message = self.test_partner.with_user(self.env.user).message_post(
body='hello',
author_id=self.user_employee.partner_id.id, email_from=self.user_employee.email,
partner_ids=self.test_partner.ids,
)
self.assertEqual(message.create_uid, self.user_employee)
message.create_uid = self.user_admin
self.assertEqual(message.create_uid, self.user_employee)
def test_personal_mail_server_mail_for_existing_message(self):
"""Crons should be able to send a mail from a personal server for an existing message."""
message = self.test_partner.with_user(self.user_employee).message_post(body='hello')
message.partner_ids += self.test_partner
with self.mock_mail_connect():
self.test_partner.with_user(self.env.ref('base.user_root'))._notify_thread(message)
self.assertEqual(self.connected_server_ids, [self.mail_server_user.id], "Should have used message creator's server.")

View file

@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.test_mail.tests.common import TestRecipients
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged("odoobot")
class TestOdoobot(TestMailCommon, TestRecipients):
class TestOdoobot(MailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestOdoobot, cls).setUpClass()
super().setUpClass()
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
cls.odoobot = cls.env.ref("base.partner_root")
@ -24,14 +22,14 @@ class TestOdoobot(TestMailCommon, TestRecipients):
'partner_ids': [],
'subtype_xmlid': 'mail.mt_comment'
}
cls.odoobot_ping_body = '<a href="http://odoo.com/web#model=res.partner&amp;id=%s" class="o_mail_redirect" data-oe-id="%s" data-oe-model="res.partner" target="_blank">@OdooBot</a>' % (cls.odoobot.id, cls.odoobot.id)
cls.odoobot_ping_body = f'<a href="http://odoo.com/odoo/res.partner/{cls.odoobot.id}" class="o_mail_redirect" data-oe-id="{cls.odoobot.id}" data-oe-model="res.partner" target="_blank">@OdooBot</a>'
cls.test_record_employe = cls.test_record.with_user(cls.user_employee)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_fetch_listener(self):
channel = self.user_employee.with_user(self.user_employee)._init_odoobot()
odoobot = self.env.ref("base.partner_root")
odoobot_in_fetch_listeners = self.env['mail.channel.member'].search([('channel_id', '=', channel.id), ('partner_id', '=', odoobot.id)])
odoobot_in_fetch_listeners = self.env['discuss.channel.member'].search([('channel_id', '=', channel.id), ('partner_id', '=', odoobot.id)])
self.assertEqual(len(odoobot_in_fetch_listeners), 1, 'odoobot should appear only once in channel_fetch_listeners')
@mute_logger('odoo.addons.mail.models.mail_mail')

View file

@ -1,76 +1,68 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from markupsafe import Markup
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.test_mail.tests.test_performance import BaseMailPerformance
from odoo import Command
from odoo.addons.test_mail.tests.test_performance import BaseMailPostPerformance
from odoo.tests.common import users, warmup
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('mail_performance', 'post_install', '-at_install')
class TestMailPerformance(BaseMailPerformance):
class FullBaseMailPerformance(BaseMailPostPerformance):
@classmethod
def setUpClass(cls):
super(TestMailPerformance, cls).setUpClass()
super().setUpClass()
# users / followers
cls.user_emp_email = mail_new_test_user(
cls.env,
company_id=cls.user_admin.company_id.id,
company_ids=[(4, cls.user_admin.company_id.id)],
email='user.emp.email@test.example.com',
login='user_emp_email',
groups='base.group_user,base.group_partner_manager',
name='Emmanuel Email',
notification_type='email',
signature='--\nEmmanuel',
)
cls.user_portal = mail_new_test_user(
cls.env,
company_id=cls.user_admin.company_id.id,
company_ids=[(4, cls.user_admin.company_id.id)],
email='user.portal@test.example.com',
login='user_portal',
groups='base.group_portal',
name='Paul Portal',
)
cls.customers = cls.env['res.partner'].create([
{'country_id': cls.env.ref('base.be').id,
'email': 'customer.full.test.1@example.com',
'name': 'Test Full Customer 1',
'mobile': '0456112233',
'phone': '0456112233',
# records
cls.record_containers = cls.env['mail.test.container.mc'].create([
{
'alias_name': 'test-alias-0',
'customer_id': cls.customers[0].id,
'name': 'Test Container 1',
},
{'country_id': cls.env.ref('base.be').id,
'email': 'customer.full.test.2@example.com',
'name': 'Test Full Customer 2',
'mobile': '0456223344',
'phone': '0456112233',
{
'alias_name': 'test-alias-1',
'customer_id': cls.customers[1].id,
'name': 'Test Container 2',
},
])
# record
cls.record_container = cls.env['mail.test.container.mc'].create({
'alias_name': 'test-alias',
'customer_id': cls.customer.id,
'name': 'Test Container',
})
cls.record_ticket = cls.env['mail.test.ticket.mc'].create({
cls.record_ticket_mc = cls.env['mail.test.ticket.mc'].create({
'email_from': 'email.from@test.example.com',
'container_id': cls.record_container.id,
'container_id': cls.record_containers[0].id,
'customer_id': False,
'name': 'Test Ticket',
'user_id': cls.user_emp_email.id,
'user_id': cls.user_follower_emp_email.id,
})
cls.record_ticket.message_subscribe(cls.customers.ids + cls.user_admin.partner_id.ids + cls.user_portal.partner_id.ids)
cls.record_ticket_mc.message_subscribe(
cls.customers.ids + cls.user_admin.partner_id.ids + cls.user_follower_portal.partner_id.ids
)
def test_initial_values(self):
cls.tracking_values_ids = [
(0, 0, {
'field_id': cls.env['ir.model.fields']._get(cls.record_ticket._name, 'email_from').id,
'new_value_char': 'new_value',
'old_value_char': 'old_value',
}),
(0, 0, {
'field_id': cls.env['ir.model.fields']._get(cls.record_ticket._name, 'customer_id').id,
'new_value_char': 'New Fake',
'new_value_integer': 2,
'old_value_char': 'Old Fake',
'old_value_integer': 1,
}),
]
@tagged('mail_performance', 'post_install', '-at_install')
class TestMailPerformance(FullBaseMailPerformance):
def test_assert_initial_values(self):
""" Simply ensure some values through all tests """
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids)
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket_mc.ids)
self.assertEqual(record_ticket.message_partner_ids,
self.user_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_portal.partner_id)
self.user_follower_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_follower_portal.partner_id)
self.assertEqual(len(record_ticket.message_ids), 1)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@ -78,19 +70,384 @@ class TestMailPerformance(BaseMailPerformance):
@warmup
def test_message_post_w_followers(self):
""" Aims to cover as much features of message_post as possible """
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids)
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket_mc.ids)
attachments = self.env['ir.attachment'].create(self.test_attachments_vals)
self.push_to_end_point_mocked.reset_mock() # reset as executed twice
self.flush_tracking()
with self.assertQueryCount(employee=91): # tmf: 60
with self.assertQueryCount(employee=108): # test_mail_full: 106
new_message = record_ticket.message_post(
attachment_ids=attachments.ids,
body='<p>Test Content</p>',
body=Markup('<p>Test Content</p>'),
email_add_signature=True,
mail_auto_delete=True,
message_type='comment',
subject='Test Subject',
subtype_xmlid='mail.mt_comment',
tracking_value_ids=self.tracking_values_ids,
)
self.assertEqual(
new_message.notified_partner_ids,
self.user_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_portal.partner_id
self.user_follower_emp_email.partner_id + self.user_admin.partner_id + self.customers + self.user_follower_portal.partner_id
)
self.assertEqual(self.push_to_end_point_mocked.call_count, 8, "Not sure why 8")
@tagged('mail_performance', 'post_install', '-at_install')
class TestPortalFormatPerformance(FullBaseMailPerformance):
"""Test performance of `portal_message_format` with multiple messages
with multiple attachments, with ratings.
Those messages might not make sense functionally but they are crafted to
cover as much of the code as possible in regard to number of queries.
Setup :
* 5 records (self.containers -> 5 mail.test.rating records, with
a different customer_id each)
* 2 messages / record
* 2 attachments / message
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_users = cls.user_employee + cls.user_emp_inbox + cls.user_emp_email + cls.user_follower_emp_email + cls.user_follower_portal
# rating-enabled test records
with cls.mock_push_to_end_point(cls):
cls.record_ratings = cls.env['mail.test.rating'].create([
{
'customer_id': cls.customers[idx].id,
'name': f'TestRating_{idx}',
'user_id': cls.test_users[idx].id,
}
for idx in range(5)
])
# messages and ratings
user_id_field = cls.env['ir.model.fields']._get(cls.record_ratings._name, 'user_id')
comment_subtype_id = cls.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
cls.link_previews = cls.env["mail.link.preview"].create(
[
{"source_url": "https://www.odoo.com"},
{"source_url": "https://www.example.com"},
]
)
cls.messages_all = cls.env['mail.message'].sudo().create([
{
'attachment_ids': [
(0, 0, {
'datas': 'data',
'name': f'Test file {att_idx}',
'res_id': record.id,
'res_model': record._name,
})
for att_idx in range(2)
],
'author_id': record.customer_id.id,
'body': f'<p>Test {msg_idx}</p>',
'date': datetime(2023, 5, 15, 10, 30, 5),
'email_from': record.customer_id.email_formatted,
"message_link_preview_ids": [
Command.create({"link_preview_id": cls.link_previews[0].id}),
Command.create({"link_preview_id": cls.link_previews[1].id}),
],
'notification_ids': [
(0, 0, {
'is_read': False,
'notification_type': 'inbox',
'res_partner_id': cls.customers[(msg_idx * 2)].id,
}),
(0, 0, {
'is_read': True,
'notification_type': 'email',
'notification_status': 'sent',
'res_partner_id': cls.customers[(msg_idx * 2) + 1].id,
}),
],
'message_type': 'comment',
'model': record._name,
'partner_ids': [
(4, cls.customers[(msg_idx * 2)].id),
(4, cls.customers[record_idx].id),
],
'reaction_ids': [
(0, 0, {
'content': 'https://www.odoo.com',
'partner_id': cls.customers[(msg_idx * 2) + 1].id
}), (0, 0, {
'content': 'https://www.example.com',
'partner_id': cls.customers[record_idx].id
}),
],
'res_id': record.id,
'subject': f'Test Rating {msg_idx}',
'subtype_id': comment_subtype_id,
'starred_partner_ids': [
(4, cls.customers[(msg_idx * 2)].id),
(4, cls.customers[(msg_idx * 2) + 1].id),
],
'tracking_value_ids': [
(0, 0, {
'field_id': user_id_field.id,
'new_value_char': 'new 1',
'new_value_integer': record.user_id.id,
'old_value_char': 'old 1',
'old_value_integer': cls.user_admin.id,
}),
]
}
for msg_idx in range(2)
for record_idx, record in enumerate(cls.record_ratings)
])
cls.messages_records = [cls.env[message.model].browse(message.res_id) for message in cls.messages_all]
# ratings values related to rating-enabled records
cls.ratings_all = cls.env['rating.rating'].sudo().create([
{
'consumed': True,
'message_id': message.id,
'partner_id': record.customer_id.id,
'publisher_comment': 'Comment',
'publisher_id': cls.user_admin.partner_id.id,
'publisher_datetime': datetime(2023, 5, 15, 10, 30, 5) - timedelta(days=2),
'rated_partner_id': record.user_id.partner_id.id,
'rating': 4,
'res_id': message.res_id,
'res_model_id': cls.env['ir.model']._get_id(message.model),
}
for rating_idx in range(2)
for message, record in zip(cls.messages_all, cls.messages_records)
])
def test_assert_initial_values(self):
self.assertEqual(len(self.messages_all), 5 * 2)
self.assertEqual(len(self.ratings_all), len(self.messages_all) * 2)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_norating(self):
messages_all = self.messages_all.with_user(self.env.user)
with self.assertQueryCount(employee=14):
# res = messages_all.portal_message_format(options=None)
res = messages_all.portal_message_format(options={'rating_include': False})
comment_subtype = self.env.ref('mail.mt_comment')
self.assertEqual(len(res), len(messages_all))
for format_res, message, record in zip(res, messages_all, self.messages_records):
self.assertEqual(len(format_res['attachment_ids']), 2)
self.maxDiff = None
self.assertEqual(
format_res['attachment_ids'],
[
{
'checksum': message.attachment_ids[0].checksum,
'filename': 'Test file 1',
'id': message.attachment_ids[0].id,
'mimetype': 'text/plain',
'name': 'Test file 1',
'raw_access_token': message.attachment_ids[0]._get_raw_access_token(),
'res_id': record.id,
'res_model': record._name,
}, {
'checksum': message.attachment_ids[1].checksum,
'filename': 'Test file 0',
'id': message.attachment_ids[1].id,
'mimetype': 'text/plain',
'name': 'Test file 0',
'raw_access_token': message.attachment_ids[1]._get_raw_access_token(),
'res_id': record.id,
'res_model': record._name,
}
]
)
self.assertEqual(format_res["author_id"]["id"], record.customer_id.id)
self.assertEqual(format_res["author_id"]["name"], record.customer_id.display_name)
self.assertEqual(format_res['author_avatar_url'], f'/web/image/mail.message/{message.id}/author_avatar/50x50')
self.assertEqual(format_res['date'], datetime(2023, 5, 15, 10, 30, 5))
self.assertEqual(' '.join(format_res['published_date_str'].split()), '05/15/2023 10:30:05 AM')
self.assertEqual(format_res['id'], message.id)
self.assertFalse(format_res['is_internal'])
self.assertFalse(format_res['is_message_subtype_note'])
self.assertEqual(format_res['subtype_id'], (comment_subtype.id, comment_subtype.name))
# should not be in, not asked
self.assertNotIn('rating_id', format_res)
self.assertNotIn('rating_stats', format_res)
self.assertNotIn('rating_value', format_res)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_rating(self):
messages_all = self.messages_all.with_user(self.env.user)
with self.assertQueryCount(employee=28): # sometimes +1
res = messages_all.portal_message_format(options={'rating_include': True})
self.assertEqual(len(res), len(messages_all))
for format_res, _message, _record in zip(res, messages_all, self.messages_records):
self.assertEqual(format_res['rating_id']['publisher_avatar'], f'/web/image/res.partner/{self.partner_admin.id}/avatar_128/50x50')
self.assertEqual(format_res['rating_id']['publisher_comment'], 'Comment')
self.assertEqual(format_res['rating_id']['publisher_id'], self.partner_admin.id)
self.assertEqual(" ".join(format_res['rating_id']['publisher_datetime'].split()), '05/13/2023 10:30:05 AM')
self.assertEqual(format_res['rating_id']['publisher_name'], self.partner_admin.display_name)
self.assertDictEqual(
format_res['rating_stats'],
{'avg': 4.0, 'total': 4, 'percent': {1: 0.0, 2: 0.0, 3: 0.0, 4: 100.0, 5: 0.0}}
)
self.assertEqual(format_res['rating_value'], 4)
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
@users('employee')
@warmup
def test_portal_message_format_monorecord(self):
message = self.messages_all[0].with_user(self.env.user)
with self.assertQueryCount(employee=19): # randomness: 18+1
res = message.portal_message_format(options={'rating_include': True})
self.assertEqual(len(res), 1)
@mute_logger("odoo.tests", "odoo.addons.mail.models.mail_mail", "odoo.models.unlink")
@users("employee")
@warmup
def test_portal_attachment_as_author(self):
message = self.env["mail.message"].create(
{
"attachment_ids": [Command.create({"name": "test attachment"})],
"author_id": self.user_employee.partner_id.id,
}
)
res = message.portal_message_format()
self.assertEqual(
res[0]["attachment_ids"][0]["ownership_token"],
message.attachment_ids[0]._get_ownership_token(),
)
@tagged('rating', 'mail_performance', 'post_install', '-at_install')
class TestRatingPerformance(FullBaseMailPerformance):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.RECORD_COUNT = 20
cls.partners = cls.env['res.partner'].sudo().create([
{'name': 'Jean-Luc %s' % (idx), 'email': 'jean-luc-%s@opoo.com' % (idx)}
for idx in range(cls.RECORD_COUNT)])
# create records with 2 ratings to check batch statistics on them
responsibles = [cls.user_admin, cls.user_employee, cls.env['res.users']]
with cls.mock_push_to_end_point(cls):
cls.record_ratings = cls.env['mail.test.rating'].create([{
'customer_id': cls.partners[idx].id,
'name': f'Test Rating {idx}',
'user_id': responsibles[idx % 3].id,
} for idx in range(cls.RECORD_COUNT)])
rates = [enum % 5 for enum, _rec in enumerate(cls.record_ratings)]
# create rating from 1 -> 5 for each record
for rate, record in zip(rates, cls.record_ratings, strict=True):
record.rating_apply(rate + 1, token=record._rating_get_access_token())
# create rating with 4 or 5 (half records)
for record in cls.record_ratings[:10]:
record.rating_apply(4, token=record._rating_get_access_token())
for record in cls.record_ratings[10:]:
record.rating_apply(5, token=record._rating_get_access_token())
def apply_ratings(self, rate):
for record in self.record_ratings:
access_token = record._rating_get_access_token()
record.rating_apply(rate, token=access_token)
self.flush_tracking()
def create_ratings(self, model):
self.record_ratings = self.env[model].create([{
'customer_id': self.partners[idx].id,
'name': 'Test Rating',
'user_id': self.user_admin.id,
} for idx in range(self.RECORD_COUNT)])
self.flush_tracking()
@users('employee')
@warmup
def test_rating_api_rating_get_operator(self):
user_names = []
with self.assertQueryCount(employee=4): # tmf: 4
ratings = self.record_ratings.with_env(self.env)
for rating in ratings:
user_names.append(rating._rating_get_operator().name)
expected_names = ['Mitchell Admin', 'Ernest Employee', False] * 6 + ['Mitchell Admin', 'Ernest Employee']
for partner_name, expected_name in zip(user_names, expected_names, strict=True):
self.assertEqual(partner_name, expected_name)
@users('employee')
@warmup
def test_rating_api_rating_get_partner(self):
partner_names = []
with self.assertQueryCount(employee=3): # tmf: 3
ratings = self.record_ratings.with_env(self.env)
for rating in ratings:
partner_names.append(rating._rating_get_partner().name)
for partner_name, expected in zip(partner_names, self.partners, strict=True):
self.assertEqual(partner_name, expected.name)
@users('employee')
@warmup
def test_rating_get_grades_perfs(self):
with self.assertQueryCount(employee=1):
ratings = self.record_ratings.with_env(self.env)
grades = ratings.rating_get_grades()
self.assertDictEqual(grades, {'great': 28, 'okay': 4, 'bad': 8})
@users('employee')
@warmup
def test_rating_get_stats_perfs(self):
with self.assertQueryCount(employee=1):
ratings = self.record_ratings.with_env(self.env)
stats = ratings.rating_get_stats()
self.assertDictEqual(stats, {'avg': 3.75, 'total': 40, 'percent': {1: 10.0, 2: 10.0, 3: 10.0, 4: 35.0, 5: 35.0}})
@users('employee')
@warmup
def test_rating_last_value_perfs(self):
with self.assertQueryCount(employee=274): # tmf: 274
self.create_ratings('mail.test.rating.thread')
with self.assertQueryCount(employee=283): # tmf: 283
self.apply_ratings(1)
with self.assertQueryCount(employee=242): # tmf: 242
self.apply_ratings(5)
@users('employee')
@warmup
def test_rating_last_value_perfs_with_rating_mixin(self):
with self.assertQueryCount(employee=317): # tmf: 317
self.create_ratings('mail.test.rating')
with self.assertQueryCount(employee=325): # tmf: 325
self.apply_ratings(1)
with self.assertQueryCount(employee=304): # tmf: 304
self.apply_ratings(5)
with self.assertQueryCount(employee=1):
self.record_ratings._compute_rating_last_value()
vals = (val == 5 for val in self.record_ratings.mapped('rating_last_value'))
self.assertTrue(all(vals), "The last rating is kept.")
@users('employee')
@warmup
def test_rating_stat_fields(self):
expected_texts = ['ok', 'ok', 'ok', 'top', 'top'] * 2 + ['ok', 'ok', 'top', 'top', 'top'] * 2
expected_satis = [50.0, 50.0, 50.0, 100.0, 100.0] * 4
with self.assertQueryCount(employee=2):
ratings = self.record_ratings.with_env(self.env)
for rating, text, satisfaction in zip(ratings, expected_texts, expected_satis, strict=True):
self.assertEqual(rating.rating_avg_text, text)
self.assertEqual(rating.rating_percentage_satisfaction, satisfaction)

View file

@ -48,7 +48,7 @@ class TestMailThreadInternals(TestMailThreadInternalsCommon):
with self.subTest(test_record=test_record):
is_portal = test_record._name != 'mail.test.simple'
has_customer = test_record._name != 'mail.test.portal.no.partner'
partner_fnames = test_record._mail_get_partner_fields()
partner_fnames = test_record._mail_get_partner_fields(introspect_fields=False)
if is_portal:
self.assertFalse(
@ -56,7 +56,9 @@ class TestMailThreadInternals(TestMailThreadInternalsCommon):
'By default access tokens are False with portal'
)
groups = test_record._notify_get_recipients_groups()
groups = test_record._notify_get_recipients_groups(
self.env['mail.message'], False,
)
portal_customer_group = next(
(group for group in groups if group[0] == 'portal_customer'),
False

View file

@ -23,22 +23,23 @@ class TestMassMailing(TestMailFullCommon):
# optout records 1 and 2
(recipients[1] | recipients[2]).write({'opt_out': True})
recipients[1].email_from = f'"Format Me" <{recipients[1].email_from}>'
recipients[1].email_from = f'"Format Me" <{recipients[1].email_normalized}>'
# blacklist records 3 and 4
self.env['mail.blacklist'].create({'email': recipients[3].email_normalized})
self.env['mail.blacklist'].create({'email': recipients[4].email_normalized})
recipients[3].email_from = f'"Format Me" <{recipients[3].email_from}>'
recipients[3].email_from = f'"Format Me" <{recipients[3].email_normalized}>'
# have a duplicate email for 9
recipients[9].email_from = f'"Format Me" <{recipients[9].email_from}>'
recipients[9].email_from = f'"Format Me" <{recipients[9].email_normalized}>'
recipient_dup_1 = recipients[9].copy()
recipient_dup_1.email_from = f'"Format Me" <{recipient_dup_1.email_from}>'
recipient_dup_1.email_from = f'"Format Me" <{recipient_dup_1.email_normalized}>'
# have another duplicate for 9, but with multi emails already done
recipient_dup_2 = recipients[9].copy()
recipient_dup_2.email_from += f'; "TestDupe" <{recipients[8].email_from}>'
recipient_dup_2.email_from += f'; "TestDupe" <{recipients[8].email_normalized}>'
# have another duplicate for 9, but with multi emails, one is different
recipient_dup_3 = recipients[9].copy() # this one will passthrough (best-effort)
recipient_dup_3.email_from += '; "TestMulti" <test.multi@test.example.com>'
recipient_dup_4 = recipient_dup_2.copy() # this one will be discarded (youpi)
# have a void mail
recipient_void_1 = self.env['mailing.test.optout'].create({'name': 'TestRecord_void_1'})
# have a falsy mail
@ -57,71 +58,79 @@ class TestMassMailing(TestMailFullCommon):
mailing.action_send_mail()
for recipient in recipients_all:
recipient_info = {
'email': recipient.email_normalized,
'content': f'Hello {recipient.name}',
'mail_values': {
'subject': f'Subject {recipient.name}',
},
}
# opt-out: cancel (cancel mail)
if recipient in recipients[1] | recipients[2]:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_optout"
# blacklisted: cancel (cancel mail)
elif recipient in recipients[3] | recipients[4]:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_bl"
# duplicates: cancel (cancel mail)
elif recipient in (recipient_dup_1, recipient_dup_2, recipient_dup_4):
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_dup"
# void: error (failed mail)
elif recipient == recipient_void_1:
recipient_info['trace_status'] = 'cancel'
recipient_info['failure_type'] = "mail_email_missing"
# falsy: error (failed mail)
elif recipient == recipient_falsy_1:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_email_invalid"
recipient_info['email'] = recipient.email_from # normalized is False but email should be falsymail
else:
# multi email -> outgoing email contains all emails
with self.subTest(recipient_from=recipient.email_from):
recipient_info = {
'content': f'Hello {recipient.name}',
'email': recipient.email_normalized or '',
'email_to_mail': recipient.email_from or '',
'email_to_recipients': [[recipient.email_from]],
'mail_values': {
'subject': f'Subject {recipient.name}',
},
}
# ; transformed into comma
if recipient == recipient_dup_2:
recipient_info['email_to_mail'] = '"Format Me" <test.record.09@test.example.com>,"TestDupe" <test.record.08@test.example.com>'
if recipient == recipient_dup_3:
email = self._find_sent_email(self.user_marketing.email_formatted, ['test.record.09@test.example.com', 'test.multi@test.example.com'])
recipient_info['email_to_mail'] = '"Format Me" <test.record.09@test.example.com>,"TestMulti" <test.multi@test.example.com>'
# multi email -> outgoing email contains all emails
recipient_info['email_to_recipients'] = [['"Format Me" <test.record.09@test.example.com>', '"TestMulti" <test.multi@test.example.com>']]
if recipient == recipient_dup_4:
recipient_info['email_to_mail'] = '"Format Me" <test.record.09@test.example.com>,"TestDupe" <test.record.08@test.example.com>'
# opt-out: cancel (cancel mail)
if recipient in recipients[1] | recipients[2]:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_optout"
# blacklisted: cancel (cancel mail)
elif recipient in recipients[3] | recipients[4]:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_bl"
# duplicates: cancel (cancel mail)
elif recipient in (recipient_dup_1, recipient_dup_2, recipient_dup_4):
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_dup"
# void: cancel (cancel mail)
elif recipient == recipient_void_1:
recipient_info['trace_status'] = 'cancel'
recipient_info['failure_type'] = "mail_email_missing"
# falsy: cancel (cancel mail)
elif recipient == recipient_falsy_1:
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_email_invalid"
recipient_info['email'] = recipient.email_from # normalized is False but email should be falsymail
else:
email = self._find_sent_email(self.user_marketing.email_formatted, [recipient.email_normalized])
# preview correctly integrated rendered qweb
self.assertIn(
'Hi %s :)' % recipient.name,
email['body'])
# rendered unsubscribe
self.assertIn(
'%s/mailing/%s/confirm_unsubscribe' % (mailing.get_base_url(), mailing.id),
email['body'])
unsubscribe_href = self._get_href_from_anchor_id(email['body'], "url6")
unsubscribe_url = werkzeug.urls.url_parse(unsubscribe_href)
unsubscribe_params = unsubscribe_url.decode_query().to_dict(flat=True)
self.assertEqual(int(unsubscribe_params['res_id']), recipient.id)
self.assertEqual(unsubscribe_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._unsubscribe_token(unsubscribe_params['res_id'], (unsubscribe_params['email'])),
unsubscribe_params['token']
)
# rendered view
self.assertIn(
'%s/mailing/%s/view' % (mailing.get_base_url(), mailing.id),
email['body'])
view_href = self._get_href_from_anchor_id(email['body'], "url6")
view_url = werkzeug.urls.url_parse(view_href)
view_params = view_url.decode_query().to_dict(flat=True)
self.assertEqual(int(view_params['res_id']), recipient.id)
self.assertEqual(view_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._unsubscribe_token(view_params['res_id'], (view_params['email'])),
view_params['token']
)
email = self._find_sent_email(self.user_marketing.email_formatted, recipient_info['email_to_recipients'][0])
# preview correctly integrated rendered qweb
self.assertIn(
'Hi %s :)' % recipient.name,
email['body'])
# rendered unsubscribe
self.assertIn(
'%s/mailing/%s/confirm_unsubscribe' % (mailing.get_base_url(), mailing.id),
email['body'])
unsubscribe_href = self._get_href_from_anchor_id(email['body'], "url6")
unsubscribe_url = werkzeug.urls.url_parse(unsubscribe_href)
unsubscribe_params = unsubscribe_url.decode_query().to_dict(flat=True)
self.assertEqual(int(unsubscribe_params['document_id']), recipient.id)
self.assertEqual(unsubscribe_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._generate_mailing_recipient_token(unsubscribe_params['document_id'], (unsubscribe_params['email'])),
unsubscribe_params['hash_token']
)
# rendered view
self.assertIn(
'%s/mailing/%s/view' % (mailing.get_base_url(), mailing.id),
email['body'])
view_href = self._get_href_from_anchor_id(email['body'], "url6")
view_url = werkzeug.urls.url_parse(view_href)
view_params = view_url.decode_query().to_dict(flat=True)
self.assertEqual(int(view_params['document_id']), recipient.id)
self.assertEqual(view_params['email'], recipient.email_normalized)
self.assertEqual(
mailing._generate_mailing_recipient_token(view_params['document_id'], (view_params['email'])),
view_params['hash_token']
)
self.assertMailTraces(
[recipient_info], mailing, recipient,

View file

@ -2,28 +2,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.urls import url_parse, url_decode, url_encode
import json
from odoo import http
from odoo.addons.auth_signup.models.res_partner import ResPartner
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.test_mail_full.tests.common import TestMailFullCommon
from odoo.addons.test_mail_sms.tests.common import TestSMSRecipients
from odoo.exceptions import AccessError
from odoo.tests import tagged, users
from odoo.tests.common import HttpCase
from odoo.tools import mute_logger
from odoo.tools import html_escape, mute_logger
@tagged('portal')
class TestPortal(HttpCase, TestMailFullCommon, TestSMSRecipients):
class TestPortal(TestMailFullCommon, TestSMSRecipients):
def setUp(self):
super(TestPortal, self).setUp()
super().setUp()
self.record_portal = self.env['mail.test.portal'].create({
'partner_id': self.partner_1.id,
'name': 'Test Portal Record',
})
self.record_portal._portal_ensure_token()
@ -36,12 +35,22 @@ class TestPortalControllers(TestPortal):
'model': self.record_portal._name,
'res_id': self.record_portal.id,
})
response = self.url_open(f'/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={self.record_portal.access_token}')
token = self.record_portal.access_token
formatted_record = mail_record.portal_message_format(options={"token": token})[0]
self.assertEqual(
formatted_record.get("author_avatar_url"),
f"/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={token}",
)
response = self.url_open(
f"/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={token}"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers.get('Content-Type'), 'image/png')
self.assertRegex(response.headers.get('Content-Disposition', ''), r'mail_message-\d+-author_avatar\.png')
self.assertEqual(response.headers.get('Content-Type'), 'image/svg+xml; charset=utf-8')
self.assertRegex(response.headers.get('Content-Disposition', ''), r'mail_message-\d+-author_avatar\.svg')
placeholder_response = self.url_open(f'/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={self.record_portal.access_token + "a"}') # false token
placeholder_response = self.url_open(
f'/mail/avatar/mail.message/{mail_record.id}/author_avatar/50x50?access_token={token + "a"}'
) # false token
self.assertEqual(placeholder_response.status_code, 200)
self.assertEqual(placeholder_response.headers.get('Content-Type'), 'image/png')
self.assertRegex(placeholder_response.headers.get('Content-Disposition', ''), r'placeholder\.png')
@ -53,107 +62,71 @@ class TestPortalControllers(TestPortal):
def test_portal_avatar_with_hash_pid(self):
self.authenticate(None, None)
post_url = f"{self.record_portal.get_base_url()}/mail/chatter_post"
res = self.opener.post(
post_url = f"{self.record_portal.get_base_url()}/mail/message/post"
pid = self.partner_2.id
_hash = self.record_portal._sign_token(pid)
res = self.url_open(
url=post_url,
json={
'params': {
'csrf_token': http.Request.csrf_token(self),
'message': 'Test',
'res_model': self.record_portal._name,
'res_id': self.record_portal.id,
'hash': self.record_portal._sign_token(self.partner_2.id),
'pid': self.partner_2.id,
'thread_model': self.record_portal._name,
'thread_id': self.record_portal.id,
'post_data': {'body': "Test"},
'hash': _hash,
'pid': pid,
},
},
)
res.raise_for_status()
self.assertNotIn("error", res.json())
message = self.record_portal.message_ids[0]
formatted_message = message.portal_message_format(options={"hash": _hash, "pid": pid})[0]
self.assertEqual(
formatted_message.get("author_avatar_url"),
f"/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={_hash}&pid={pid}",
)
response = self.url_open(
f'/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={self.record_portal._sign_token(self.partner_2.id)}&pid={self.partner_2.id}')
f"/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={_hash}&pid={pid}"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers.get('Content-Type'), 'image/png')
self.assertRegex(response.headers.get('Content-Disposition', ''), r'mail_message-\d+-author_avatar\.png')
self.assertEqual(response.headers.get('Content-Type'), 'image/svg+xml; charset=utf-8')
self.assertRegex(response.headers.get('Content-Disposition', ''), r'mail_message-\d+-author_avatar\.svg')
placeholder_response = self.url_open(
f'/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={self.record_portal._sign_token(self.partner_2.id) + "a"}&pid={self.partner_2.id}') # false hash
f'/mail/avatar/mail.message/{message.id}/author_avatar/50x50?_hash={_hash + "a"}&pid={pid}'
) # false hash
self.assertEqual(placeholder_response.status_code, 200)
self.assertEqual(placeholder_response.headers.get('Content-Type'), 'image/png')
self.assertRegex(placeholder_response.headers.get('Content-Disposition', ''), r'placeholder\.png')
def test_portal_message_fetch(self):
"""Test retrieving chatter messages through the portal controller"""
self.authenticate(None, None)
message_fetch_url = '/mail/chatter_fetch'
payload = json.dumps({
'jsonrpc': '2.0',
'method': 'call',
'id': 0,
'params': {
'res_model': 'mail.test.portal',
'res_id': self.record_portal.id,
'token': self.record_portal.access_token,
},
})
def get_chatter_message_count():
res = self.url_open(
url=message_fetch_url,
data=payload,
headers={'Content-Type': 'application/json'}
)
return res.json().get('result', {}).get('message_count', 0)
self.assertEqual(get_chatter_message_count(), 0)
for _ in range(8):
self.record_portal.message_post(
body='Test',
author_id=self.partner_1.id,
message_type='comment',
subtype_id=self.env.ref('mail.mt_comment').id,
)
self.assertEqual(get_chatter_message_count(), 8)
# Empty the body of a few messages
for i in (2, 5, 6):
self.record_portal.message_ids[i].body = ""
# Empty messages should be ignored
self.assertEqual(get_chatter_message_count(), 5)
def test_portal_share_comment(self):
""" Test posting through portal controller allowing to use a hash to
post wihtout access rights. """
self.authenticate(None, None)
post_url = f"{self.record_portal.get_base_url()}/mail/chatter_post"
post_url = f"{self.record_portal.get_base_url()}/mail/message/post"
# test as not logged
self.opener.post(
self.url_open(
url=post_url,
json={
'params': {
'csrf_token': http.Request.csrf_token(self),
'hash': self.record_portal._sign_token(self.partner_2.id),
'message': 'Test',
'pid': self.partner_2.id,
'redirect': '/',
'res_model': self.record_portal._name,
'res_id': self.record_portal.id,
'thread_model': self.record_portal._name,
'thread_id': self.record_portal.id,
'post_data': {'body': "Test"},
'token': self.record_portal.access_token,
'hash': self.record_portal._sign_token(self.partner_2.id),
'pid': self.partner_2.id,
},
},
)
message = self.record_portal.message_ids[0]
# Only messages from the current user not OdooBot
messages = self.record_portal.message_ids.filtered(lambda msg: msg.author_id == self.partner_2)
self.assertIn('Test', message.body)
self.assertEqual(message.author_id, self.partner_2)
self.assertIn('Test', messages[0].body)
@tagged('-at_install', 'post_install', 'portal', 'mail_controller')
class TestPortalFlow(TestMailFullCommon, HttpCase):
class TestPortalFlow(MailCommon, HttpCase):
""" Test shared links, mail/view links and redirection (backend, customer
portal or frontend for specific addons). """
@ -164,7 +137,6 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
'country_id': cls.env.ref('base.fr').id,
'email': 'mdelvaux34@example.com',
'lang': 'en_US',
'mobile': '+33639982325',
'name': 'Mathias Delvaux',
'phone': '+33353011823',
})
@ -198,6 +170,16 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
})
cls._create_portal_user()
# The test relies on `record_access_url` to check the validity of mails being sent,
# however, when auth_signup is installed, a new token is generated each time the url
# is being requested.
# By removing the time-based hashing from this function we can ensure the stability of
# the url during the tests.
def patched_generate_signup_token(self, *_, **__):
self.ensure_one()
return str([self.id, self._get_login_date(), self.signup_type])
cls.classPatch(ResPartner, '_generate_signup_token', patched_generate_signup_token)
# prepare access URLs on self to ease tests
# ------------------------------------------------------------
base_url = cls.record_portal.get_base_url()
@ -220,7 +202,9 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
cls.record_url_no_model = f'{cls.record_portal.get_base_url()}/mail/view?model=this.should.not.exists&res_id=1'
# find portal + auth data url
for group_name, group_func, group_data in cls.record_portal.sudo()._notify_get_recipients_groups(False):
for group_name, group_func, group_data in cls.record_portal.sudo()._notify_get_recipients_groups(
cls.env['mail.message'], False
):
if group_name == 'portal_customer' and group_func(cls.customer):
cls.record_portal_url_auth = group_data['button_access']['url']
break
@ -244,25 +228,25 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
cls.portal_web_url = f'{base_url}/my/test_portal/{cls.record_portal.id}'
cls.portal_web_url_with_token = f'{base_url}/my/test_portal/{cls.record_portal.id}?{url_encode({"access_token": cls.record_portal.access_token, "pid": cls.customer.id, "hash": cls.record_portal_hash}, sort=True)}'
cls.public_act_url_share = f'{base_url}/test_portal/public_type/{cls.record_public_act_url.id}'
cls.internal_backend_local_url = f'/web#{url_encode({"model": cls.record_internal._name, "id": cls.record_internal.id, "active_id": cls.record_internal.id, "cids": cls.company_admin.id}, sort=True)}'
cls.portal_backend_local_url = f'/web#{url_encode({"model": cls.record_portal._name, "id": cls.record_portal.id, "active_id": cls.record_portal.id, "cids": cls.company_admin.id}, sort=True)}'
cls.read_backend_local_url = f'/web#{url_encode({"model": cls.record_read._name, "id": cls.record_read.id, "active_id": cls.record_read.id, "cids": cls.company_admin.id}, sort=True)}'
cls.public_act_url_backend_local_url = f'/web#{url_encode({"model": cls.record_public_act_url._name, "id": cls.record_public_act_url.id, "active_id": cls.record_public_act_url.id, "cids": cls.company_admin.id}, sort=True)}'
cls.discuss_local_url = '/web#action=mail.action_discuss'
cls.internal_backend_local_url = f'/odoo/{cls.record_internal._name}/{cls.record_internal.id}'
cls.portal_backend_local_url = f'/odoo/{cls.record_portal._name}/{cls.record_portal.id}'
cls.read_backend_local_url = f'/odoo/{cls.record_read._name}/{cls.record_read.id}'
cls.public_act_url_backend_local_url = f'/odoo/{cls.record_public_act_url._name}/{cls.record_public_act_url.id}'
cls.discuss_local_url = '/odoo/action-mail.action_discuss'
def test_assert_initial_data(self):
""" Test some initial values. Test that record_access_url is a valid URL
""" Test some initial values. Test that record_portal_url_auth is a valid URL
to view the record_portal and that record_access_url_wrong_token only differs
from record_access_url by a different access_token. """
self.record_internal.with_user(self.user_employee).check_access_rule('read')
self.record_portal.with_user(self.user_employee).check_access_rule('read')
self.record_read.with_user(self.user_employee).check_access_rule('read')
from record_portal_url_auth by a different access_token. """
self.record_internal.with_user(self.user_employee).check_access('read')
self.record_portal.with_user(self.user_employee).check_access('read')
self.record_read.with_user(self.user_employee).check_access('read')
with self.assertRaises(AccessError):
self.record_internal.with_user(self.user_portal).check_access_rights('read')
self.record_internal.with_user(self.user_portal).check_access('read')
with self.assertRaises(AccessError):
self.record_portal.with_user(self.user_portal).check_access_rights('read')
self.record_read.with_user(self.user_portal).check_access_rights('read')
self.record_portal.with_user(self.user_portal).check_access('read')
self.record_read.with_user(self.user_portal).check_access('read')
self.assertNotEqual(self.record_portal_url_auth, self.record_portal_url_auth_wrong_token)
url_params = []
@ -331,7 +315,7 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
# std url, read record -> redirect to my with parameters being record portal action parameters (???)
(
'Access record (no customer portal)', self.record_read_url_base,
f'{self.test_base_url}/my#{url_encode({"model": self.record_read._name, "id": self.record_read.id, "active_id": self.record_read.id, "cids": self.company_admin.id}, sort=True)}',
f'{self.test_base_url}/my?{url_encode({"subpath": f"{self.record_read._name}/{self.record_read.id}"})}',
),
# std url, no access to record -> redirect to my
(
@ -445,6 +429,63 @@ class TestPortalFlow(TestMailFullCommon, HttpCase):
'Failed with %s - %s' % (model, res_id)
)
def assert_URL(self, url, expected_path, expected_fragment_params=None, expected_query=None):
"""Asserts that the URL has the expected path and if set, the expected fragment parameters and query."""
parsed_url = url_parse(url)
fragment_params = url_decode(parsed_url.fragment)
self.assertEqual(parsed_url.path, expected_path)
if expected_fragment_params:
for key, expected_value in expected_fragment_params.items():
self.assertEqual(fragment_params.get(key), expected_value,
f'Expected: "{key}={expected_value}" (for path: {expected_path})')
if expected_query:
self.assertEqual(expected_query, parsed_url.query,
f'Expected: query="{expected_query}" (for path: {expected_path})')
@users('employee')
def test_send_message_to_customer(self):
"""Same as test_send_message_to_customer_using_template but without a template."""
composer = self.env['mail.compose.message'].with_context(
self._get_mail_composer_web_context(
self.record_portal,
default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
)
).create({
'body': '<p>Hello Mathias Delvaux, your quotation is ready for review.</p>',
'partner_ids': self.customer.ids,
'subject': 'Your Quotation "a white table"',
})
with self.mock_mail_gateway(mail_unlink_sent=True):
composer._action_send_mail()
self.assertEqual(len(self._mails), 1)
self.assertIn(f'"{html_escape(self.record_portal_url_auth)}"', self._mails[0].get('body'))
# Check that the template is not used (not the same subject)
self.assertEqual('Your Quotation "a white table"', self._mails[0].get('subject'))
self.assertIn('Hello Mathias Delvaux', self._mails[0].get('body'))
@users('employee')
def test_send_message_to_customer_using_template(self):
"""Send a mail to a customer without an account and check that it contains a link to view the record.
Other tests below check that that same link has the correct behavior.
This test follows the common use case by using a template while the next send the mail without a template."""
composer = self.env['mail.compose.message'].with_context(
self._get_mail_composer_web_context(
self.record_portal,
default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
default_template_id=self.mail_template.id,
)
).create({})
with self.mock_mail_gateway(mail_unlink_sent=True):
composer._action_send_mail()
self.assertEqual(len(self._mails), 1)
self.assertIn(f'"{html_escape(self.record_portal_url_auth)}"', self._mails[0].get('body'))
self.assertEqual(f'Your quotation "{self.record_portal.name}"', self._mails[0].get('subject')) # Check that the template is used
@tagged('portal')
class TestPortalMixin(TestPortal):

View file

@ -8,11 +8,12 @@ from odoo import http
from odoo.addons.test_mail_full.tests.common import TestMailFullCommon
from odoo.addons.test_mail_sms.tests.common import TestSMSRecipients
from odoo.tests import tagged
from odoo.tests.common import HttpCase, users, warmup
from odoo.tests.common import users, warmup
from odoo.tools import mute_logger
class TestRatingCommon(TestMailFullCommon, TestSMSRecipients):
@classmethod
def setUpClass(cls):
super(TestRatingCommon, cls).setUpClass()
@ -22,88 +23,104 @@ class TestRatingCommon(TestMailFullCommon, TestSMSRecipients):
'name': 'Test Rating',
'user_id': cls.user_admin.id,
})
cls.record_rating_thread = cls.env['mail.test.rating.thread'].create({
'customer_id': cls.partner_1.id,
'name': 'Test rating without rating mixin',
'user_id': cls.user_admin.id,
})
@tagged('rating')
class TestRatingFlow(TestRatingCommon):
def test_initial_values(self):
record_rating = self.record_rating.with_env(self.env)
self.assertFalse(record_rating.rating_ids)
self.assertEqual(record_rating.message_partner_ids, self.partner_admin)
self.assertEqual(len(record_rating.message_ids), 1)
for record_rating in [self.record_rating, self.record_rating_thread]:
record_rating = record_rating.with_env(self.env)
self.assertFalse(record_rating.rating_ids)
self.assertEqual(record_rating.message_partner_ids, self.partner_admin)
self.assertEqual(len(record_rating.message_ids), 1)
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_rating_prepare(self):
record_rating = self.record_rating.with_env(self.env)
for record_rating, desc in ((self.record_rating, 'With rating mixin'),
(self.record_rating_thread, 'Without rating mixin')):
with self.subTest(desc):
record_rating = record_rating.with_env(self.env)
# prepare rating token
access_token = record_rating._rating_get_access_token()
# prepare rating token
access_token = record_rating._rating_get_access_token()
# check rating creation
rating = record_rating.rating_ids
self.assertEqual(rating.access_token, access_token)
self.assertFalse(rating.consumed)
self.assertFalse(rating.is_internal)
self.assertEqual(rating.partner_id, self.partner_1)
self.assertEqual(rating.rated_partner_id, self.user_admin.partner_id)
self.assertFalse(rating.rating)
# check rating creation
rating = record_rating.rating_ids
self.assertEqual(rating.access_token, access_token)
self.assertFalse(rating.consumed)
self.assertFalse(rating.is_internal)
self.assertEqual(rating.partner_id, self.partner_1)
self.assertEqual(rating.rated_partner_id, self.user_admin.partner_id)
self.assertFalse(rating.rating)
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_rating_rating_apply(self):
record_rating = self.record_rating.with_env(self.env)
record_messages = record_rating.message_ids
for record_rating, expected_subtype, is_rating_mixin_test in (
(self.record_rating_thread, self.env.ref('mail.mt_comment'), False),
(self.record_rating, self.env.ref('test_mail_full.mt_mail_test_rating_rating_done'), True),
):
with self.subTest('With rating mixin' if is_rating_mixin_test else 'Without rating mixin'):
record_rating = record_rating.with_env(self.env)
record_messages = record_rating.message_ids
# prepare rating token
access_token = record_rating._rating_get_access_token()
# prepare rating token
access_token = record_rating._rating_get_access_token()
# simulate an email click: notification should be delayed
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
record_rating.rating_apply(5, token=access_token, feedback='Top Feedback', notify_delay_send=True)
message = record_rating.message_ids[0]
rating = record_rating.rating_ids
# simulate an email click: notification should be delayed
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
record_rating.rating_apply(5, token=access_token, feedback='Top Feedback', notify_delay_send=True)
message = record_rating.message_ids[0]
rating = record_rating.rating_ids
# check posted message
self.assertEqual(record_rating.message_ids, record_messages + message)
self.assertIn('Top Feedback', message.body)
self.assertIn('/rating/static/src/img/rating_5.png', message.body)
self.assertEqual(message.author_id, self.partner_1)
self.assertEqual(message.rating_ids, rating)
self.assertFalse(message.notified_partner_ids)
self.assertEqual(message.subtype_id, self.env.ref('test_mail_full.mt_mail_test_rating_rating_done'))
# check posted message
self.assertEqual(record_rating.message_ids, record_messages + message)
self.assertIn('Top Feedback', message.body)
self.assertIn('/rating/static/src/img/rating_5.png', message.body)
self.assertEqual(message.author_id, self.partner_1)
self.assertEqual(message.rating_ids, rating)
self.assertFalse(message.notified_partner_ids)
self.assertEqual(message.subtype_id, expected_subtype)
# check rating update
self.assertTrue(rating.consumed)
self.assertEqual(rating.feedback, 'Top Feedback')
self.assertEqual(rating.message_id, message)
self.assertEqual(rating.rating, 5)
self.assertEqual(record_rating.rating_last_value, 5)
# check rating update
self.assertTrue(rating.consumed)
self.assertEqual(rating.feedback, 'Top Feedback')
self.assertEqual(rating.message_id, message)
self.assertEqual(rating.rating, 5)
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 5)
# give a feedback: send notifications (notify_delay_send set to False)
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
record_rating.rating_apply(1, token=access_token, feedback='Bad Feedback')
# give a feedback: send notifications (notify_delay_send set to False)
with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app():
record_rating.rating_apply(1, token=access_token, feedback='Bad Feedback')
# check posted message: message is updated
update_message = record_rating.message_ids[0]
self.assertEqual(update_message, message, 'Should update first message')
self.assertEqual(record_rating.message_ids, record_messages + update_message)
self.assertIn('Bad Feedback', update_message.body)
self.assertIn('/rating/static/src/img/rating_1.png', update_message.body)
self.assertEqual(update_message.author_id, self.partner_1)
self.assertEqual(update_message.rating_ids, rating)
self.assertEqual(update_message.notified_partner_ids, self.partner_admin)
self.assertEqual(update_message.subtype_id, self.env.ref("test_mail_full.mt_mail_test_rating_rating_done"))
# check posted message: message is updated
update_message = record_rating.message_ids[0]
self.assertEqual(update_message, message, 'Should update first message')
self.assertEqual(record_rating.message_ids, record_messages + update_message)
self.assertIn('Bad Feedback', update_message.body)
self.assertIn('/rating/static/src/img/rating_1.png', update_message.body)
self.assertEqual(update_message.author_id, self.partner_1)
self.assertEqual(update_message.rating_ids, rating)
self.assertEqual(update_message.notified_partner_ids, self.partner_admin)
self.assertEqual(update_message.subtype_id, expected_subtype)
# check rating update
new_rating = record_rating.rating_ids
self.assertEqual(new_rating, rating, 'Should update first rating')
self.assertTrue(new_rating.consumed)
self.assertEqual(new_rating.feedback, 'Bad Feedback')
self.assertEqual(new_rating.message_id, update_message)
self.assertEqual(new_rating.rating, 1)
self.assertEqual(record_rating.rating_last_value, 1)
# check rating update
new_rating = record_rating.rating_ids
self.assertEqual(new_rating, rating, 'Should update first rating')
self.assertTrue(new_rating.consumed)
self.assertEqual(new_rating.feedback, 'Bad Feedback')
self.assertEqual(new_rating.message_id, update_message)
self.assertEqual(new_rating.rating, 1)
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 1)
@tagged('rating')
@ -131,107 +148,141 @@ class TestRatingMixin(TestRatingCommon):
self.assertEqual(record_rating.rating_avg, 3, "The average should be equal to 3")
@tagged('rating', 'mail_performance', 'post_install', '-at_install')
class TestRatingPerformance(TestRatingCommon):
@users('employee')
@warmup
def test_rating_last_value_perfs(self):
RECORD_COUNT = 100
partners = self.env['res.partner'].sudo().create([
{'name': 'Jean-Luc %s' % (idx), 'email': 'jean-luc-%s@opoo.com' % (idx)} for idx in range(RECORD_COUNT)])
with self.assertQueryCount(employee=1516): # tmf 1516 / com 5510
record_ratings = self.env['mail.test.rating'].create([{
'customer_id': partners[idx].id,
'name': 'Test Rating',
'user_id': self.user_admin.id,
} for idx in range(RECORD_COUNT)])
self.flush_tracking()
with self.assertQueryCount(employee=2004): # tmf 2004
for record in record_ratings:
access_token = record._rating_get_access_token()
record.rating_apply(1, token=access_token)
self.flush_tracking()
with self.assertQueryCount(employee=2003): # tmf 2003
for record in record_ratings:
access_token = record._rating_get_access_token()
record.rating_apply(5, token=access_token)
self.flush_tracking()
with self.assertQueryCount(employee=1):
record_ratings._compute_rating_last_value()
vals = [val == 5 for val in record_ratings.mapped('rating_last_value')]
self.assertTrue(all(vals), "The last rating is kept.")
@tagged('rating')
class TestRatingRoutes(HttpCase, TestRatingCommon):
@tagged("rating", "rating_portal")
class TestRatingRoutes(TestRatingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._create_portal_user()
def test_open_rating_route(self):
"""
16.0 + expected behavior
1) Clicking on the smiley image triggers the /rate/<string:token>/<int:rate>
route should not update the rating of the record but simply redirect
to the feedback form
2) Customer interacts with webpage and submits FORM. Triggers /rate/<string:token>/submit_feedback
route. Should update the rating of the record with the data in the POST request
"""
self.authenticate(None, None) # set up session for public user
access_token = self.record_rating._rating_get_access_token()
for record_rating, is_rating_mixin_test in ((self.record_rating_thread, False),
(self.record_rating, True)):
with self.subTest('With rating mixin' if is_rating_mixin_test else 'Without rating mixin'):
"""
16.0 + expected behavior
1) Clicking on the smiley image triggers the /rate/<string:token>/<int:rate>
route should not update the rating of the record but simply redirect
to the feedback form
2) Customer interacts with webpage and submits FORM. Triggers /rate/<string:token>/submit_feedback
route. Should update the rating of the record with the data in the POST request
"""
self.authenticate(None, None) # set up session for public user
access_token = record_rating._rating_get_access_token()
# First round of clicking the URL and then submitting FORM data
response_click_one = self.url_open(f"/rate/{access_token}/5")
response_click_one.raise_for_status()
# First round of clicking the URL and then submitting FORM data
response_click_one = self.url_open(f"/rate/{access_token}/5")
response_click_one.raise_for_status()
# there should be a form to post to validate the feedback and avoid one-click anyway
forms = lxml.html.fromstring(response_click_one.content).xpath('//form')
self.assertEqual(forms[0].get('method'), 'post')
self.assertEqual(forms[0].get('action', ''), f'/rate/{access_token}/submit_feedback')
# there should be a form to post to validate the feedback and avoid one-click anyway
forms = lxml.html.fromstring(response_click_one.content).xpath('//form')
matching_rate_form = next((form for form in forms if form.get("action", "").startswith("/rate")), None)
self.assertEqual(matching_rate_form.get('method'), 'post')
self.assertEqual(matching_rate_form.get('action', ''), f'/rate/{access_token}/submit_feedback')
# rating should not change, i.e. default values
rating = self.record_rating.rating_ids
self.assertFalse(rating.consumed)
self.assertEqual(rating.rating, 0)
self.assertFalse(rating.feedback)
self.assertEqual(self.record_rating.rating_last_value, 0)
# rating should not change, i.e. default values
rating = record_rating.rating_ids
self.assertFalse(rating.consumed)
self.assertEqual(rating.rating, 0)
self.assertFalse(rating.feedback)
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 0)
response_submit_one = self.url_open(
f"/rate/{access_token}/submit_feedback",
data={
"rate": 5,
"csrf_token": http.Request.csrf_token(self),
"feedback": "good",
response_submit_one = self.url_open(
f"/rate/{access_token}/submit_feedback",
data={
"rate": 5,
"csrf_token": http.Request.csrf_token(self),
"feedback": "good",
}
)
response_submit_one.raise_for_status()
rating_post_submit_one = record_rating.rating_ids
self.assertTrue(rating_post_submit_one.consumed)
self.assertEqual(rating_post_submit_one.rating, 5)
self.assertEqual(rating_post_submit_one.feedback, "good")
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 5)
# Second round of clicking the URL and then submitting FORM data
response_click_two = self.url_open(f"/rate/{access_token}/1")
response_click_two.raise_for_status()
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 5) # should not be updated to 1
# check returned form
forms = lxml.html.fromstring(response_click_two.content).xpath('//form')
matching_rate_form = next((form for form in forms if form.get("action", "").startswith("/rate")), None)
self.assertEqual(matching_rate_form.get('method'), 'post')
self.assertEqual(matching_rate_form.get('action', ''), f'/rate/{access_token}/submit_feedback')
response_submit_two = self.url_open(
f"/rate/{access_token}/submit_feedback",
data={
"rate": 1,
"csrf_token": http.Request.csrf_token(self),
"feedback": "bad job"
}
)
response_submit_two.raise_for_status()
rating_post_submit_second = record_rating.rating_ids
self.assertTrue(rating_post_submit_second.consumed)
self.assertEqual(rating_post_submit_second.rating, 1)
self.assertEqual(rating_post_submit_second.feedback, "bad job")
if is_rating_mixin_test:
self.assertEqual(record_rating.rating_last_value, 1)
def test_portal_user_can_post_message_with_rating(self):
"""Test portal user can post a message with a rating on a thread with
_mail_post_access as read. In this case, sudo() is not necessary for
message_post itself, but it is necessary for adding the rating. This
tests covers the rating part is properly allowed."""
record_rating = self.env["mail.test.rating.thread.read"].create(
{
"customer_id": self.partner_1.id,
"name": "Test read access post + rating",
"user_id": self.user_admin.id,
}
)
response_submit_one.raise_for_status()
rating_post_submit_one = self.record_rating.rating_ids
self.assertTrue(rating_post_submit_one.consumed)
self.assertEqual(rating_post_submit_one.rating, 5)
self.assertEqual(rating_post_submit_one.feedback, "good")
self.assertEqual(self.record_rating.rating_last_value, 5)
# Second round of clicking the URL and then submitting FORM data
response_click_two = self.url_open(f"/rate/{access_token}/1")
response_click_two.raise_for_status()
self.assertEqual(self.record_rating.rating_last_value, 5) # should not be updated to 1
# check returned form
forms = lxml.html.fromstring(response_click_two.content).xpath('//form')
self.assertEqual(forms[0].get('method'), 'post')
self.assertEqual(forms[0].get('action', ''), f'/rate/{access_token}/submit_feedback')
response_submit_two = self.url_open(f"/rate/{access_token}/submit_feedback",
data={"rate": 1,
"csrf_token": http.Request.csrf_token(self),
"feedback": "bad job"})
response_submit_two.raise_for_status()
rating_post_submit_second = self.record_rating.rating_ids
self.assertTrue(rating_post_submit_second.consumed)
self.assertEqual(rating_post_submit_second.rating, 1)
self.assertEqual(rating_post_submit_second.feedback, "bad job")
self.assertEqual(self.record_rating.rating_last_value, 1)
# from model
message = record_rating.with_user(self.user_portal).message_post(
body="Not bad",
message_type="comment",
rating_value=3,
subtype_xmlid="mail.mt_comment",
)
rating = message.sudo().rating_id
self.assertEqual(rating.rating, 3, "rating was properly set")
# stealing attempt from another user
message2 = record_rating.message_post(
body="Attempt to steal rating with another user",
message_type="comment",
rating_id=rating.id,
subtype_xmlid="mail.mt_comment",
)
self.assertEqual(message.sudo().rating_id, rating, "rating was not removed from m1")
self.assertFalse(message2.rating_id, "rating was not added to m2")
# from controller
self.authenticate("portal_test", "portal_test")
res = self.make_jsonrpc_request(
"/mail/message/post",
{
"post_data": {
"body": "Good service",
"message_type": "comment",
"rating_value": 5,
"subtype_xmlid": "mail.mt_comment",
},
"thread_id": record_rating.id,
"thread_model": "mail.test.rating.thread.read",
},
)
message = next(
m for m in res["store_data"]["mail.message"] if m["id"] == res["message_id"]
)
rating = next(
r for r in res["store_data"]["rating.rating"] if r["id"] == message["rating_id"]
)
self.assertEqual(rating["rating"], 5)

View file

@ -13,7 +13,6 @@ class TestResUsers(TestMailFullCommon):
cls.portal_user = mail_new_test_user(
cls.env,
login='portal_user',
mobile='+32 494 12 34 56',
phone='+32 494 12 34 89',
password='password',
name='Portal User',
@ -24,7 +23,6 @@ class TestResUsers(TestMailFullCommon):
cls.portal_user_2 = mail_new_test_user(
cls.env,
login='portal_user_2',
mobile='+32 494 12 34 22',
phone='invalid phone',
password='password',
name='Portal User 2',
@ -32,6 +30,16 @@ class TestResUsers(TestMailFullCommon):
groups='base.group_portal',
)
cls.portal_user_3 = mail_new_test_user(
cls.env,
login='portal_user_3',
phone='+32 494 12 34 22',
password='password',
name='Portal User 3',
email='portal_3@test.example.com',
groups='base.group_portal',
)
# Remove existing blacklisted email / phone (they will be sanitized, so we avoid to sanitize them here)
cls.env['mail.blacklist'].search([]).unlink()
cls.env['phone.blacklist'].search([]).unlink()
@ -40,22 +48,24 @@ class TestResUsers(TestMailFullCommon):
"""Test that the email and the phone are blacklisted
when a portal user deactivate his own account.
"""
(self.portal_user | self.portal_user_2)._deactivate_portal_user(request_blacklist=True)
(self.portal_user | self.portal_user_2 | self.portal_user_3)._deactivate_portal_user(request_blacklist=True)
self.assertFalse(self.portal_user.active, 'Should have archived the user')
self.assertFalse(self.portal_user.partner_id.active, 'Should have archived the partner')
self.assertFalse(self.portal_user_2.active, 'Should have archived the user')
self.assertFalse(self.portal_user_2.partner_id.active, 'Should have archived the partner')
self.assertFalse(self.portal_user_3.active, 'Should have archived the user')
self.assertFalse(self.portal_user_3.partner_id.active, 'Should have archived the partner')
blacklist = self.env['mail.blacklist'].search([
('email', 'in', ('portal@test.example.com', 'portal_2@test.example.com')),
('email', 'in', ('portal@test.example.com', 'portal_2@test.example.com', 'portal_3@test.example.com')),
])
self.assertEqual(len(blacklist), 2, 'Should have blacklisted the users email')
self.assertEqual(len(blacklist), 3, 'Should have blacklisted the users email')
blacklists = self.env['phone.blacklist'].search([
('number', 'in', ('+32494123489', '+32494123456', '+32494123422')),
('number', 'in', ('+32494123489', '+32494123422')),
])
self.assertEqual(len(blacklists), 3, 'Should have blacklisted the user phone and mobile')
self.assertEqual(len(blacklists), 2, 'Should have blacklisted the user phone')
blacklist = self.env['phone.blacklist'].search([('number', '=', 'invalid phone')])
self.assertFalse(blacklist, 'Should have skipped invalid phone')

View file

@ -0,0 +1,111 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from urllib.parse import urlencode
from odoo import tests
from odoo.addons.test_mail_full.tests.test_portal import TestPortal
@tests.common.tagged("post_install", "-at_install")
class TestUIPortal(TestPortal):
def setUp(self):
super().setUp()
self.env["mail.message"].create(
{
"author_id": self.user_employee.partner_id.id,
"body": "Test Message",
"model": self.record_portal._name,
"res_id": self.record_portal.id,
"subtype_id": self.ref("mail.mt_comment"),
}
)
def test_star_message(self):
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}",
"star_message_tour",
login=self.user_employee.login,
)
def test_no_copy_link_for_non_readable_portal_record(self):
# mail.test.portal has read access only for base.group_user
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}?{urlencode({'token': self.record_portal.access_token})}",
"portal_no_copy_link_tour",
login=None,
)
def test_copy_link_for_readable_portal_record(self):
# mail.test.portal has read access only for base.group_user
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}?{urlencode({'token': self.record_portal.access_token})}",
"portal_copy_link_tour",
login=self.user_employee.login,
)
def test_load_more(self):
self.env["mail.message"].create(
[
{
"author_id": self.user_employee.partner_id.id,
"body": f"Test Message {i + 1}",
"model": self.record_portal._name,
"res_id": self.record_portal.id,
"subtype_id": self.ref("mail.mt_comment"),
}
for i in range(30)
]
)
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}",
"load_more_tour",
login=self.user_employee.login,
)
def test_message_actions_without_login(self):
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}?token={self.record_portal._portal_ensure_token()}",
"message_actions_tour",
)
def test_rating_record_portal(self):
record_rating = self.env["mail.test.rating"].create({"name": "Test rating record"})
# To check if there is no message with rating, there is no rating cards feature.
record_rating.message_post(
body="Message without rating",
message_type="comment",
subtype_xmlid="mail.mt_comment",
)
self.start_tour(
f"/my/test_portal_rating_records/{record_rating.id}?display_rating=True&token={record_rating._portal_ensure_token()}",
"portal_rating_tour"
)
def test_display_rating_portal(self):
record_rating = self.env["mail.test.rating"].create({"name": "Test rating record"})
record_rating.message_post(
body="Message with rating",
message_type="comment",
rating_value="5",
subtype_xmlid="mail.mt_comment",
)
self.start_tour(
f"/my/test_portal_rating_records/{record_rating.id}?display_rating=True&token={record_rating._portal_ensure_token()}",
"portal_display_rating_tour",
)
self.start_tour(
f"/my/test_portal_rating_records/{record_rating.id}?display_rating=False&token={record_rating._portal_ensure_token()}",
"portal_not_display_rating_tour",
)
def test_composer_actions_portal(self):
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}",
"portal_composer_actions_tour_internal_user",
login=self.user_employee.login,
)
self.start_tour(
f"/my/test_portal_records/{self.record_portal.id}?token={self.record_portal._portal_ensure_token()}",
"portal_composer_actions_tour_portal_user",
)