mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 08:32:03 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -1,22 +1,27 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_discuss_controller
|
||||
from . import test_get_model_definitions
|
||||
from . import test_discuss_tools
|
||||
from . import test_fetchmail
|
||||
from . import test_font_to_img
|
||||
from . import test_ir_mail_server
|
||||
from . import test_ir_ui_menu
|
||||
from . import test_ir_websocket
|
||||
from . import test_link_preview
|
||||
from . import test_mail_channel
|
||||
from . import test_mail_channel_as_guest
|
||||
from . import test_mail_channel_member
|
||||
from . import test_mail_activity
|
||||
from . import test_mail_blacklist
|
||||
from . import test_mail_composer
|
||||
from . import test_mail_full_composer
|
||||
from . import test_mail_mail
|
||||
from . import test_mail_mail_stable_selection
|
||||
from . import test_mail_message
|
||||
from . import test_mail_message_translate
|
||||
from . import test_mail_presence
|
||||
from . import test_mail_render
|
||||
from . import test_mail_template
|
||||
from . import test_mail_tools
|
||||
from . import test_res_partner
|
||||
from . import test_res_role
|
||||
from . import test_res_users
|
||||
from . import test_res_users_settings
|
||||
from . import test_rtc
|
||||
from . import test_uninstall
|
||||
from . import test_update_notification
|
||||
from . import test_user_modify_own_profile
|
||||
from . import test_websocket_controller
|
||||
|
||||
from .discuss import *
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
146
odoo-bringout-oca-ocb-mail/mail/tests/common_activity.py
Normal file
146
odoo-bringout-oca-ocb-mail/mail/tests/common_activity.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.mail.models.mail_activity import MailActivity
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import Form
|
||||
from odoo.tools.misc import format_date
|
||||
|
||||
|
||||
class ActivityScheduleCase(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# prepare activities
|
||||
cls.activity_type_todo = cls.env.ref('mail.mail_activity_data_todo')
|
||||
cls.activity_type_todo.delay_count = 4
|
||||
cls.activity_type_todo.sequence = 1
|
||||
cls.activity_type_todo.summary = "TodoSummary"
|
||||
cls.activity_type_call = cls.env.ref('mail.mail_activity_data_call')
|
||||
cls.activity_type_call.delay_count = 1
|
||||
cls.activity_type_call.sequence = 2
|
||||
cls.activity_type_call.summary = "TodoSumCallSummary"
|
||||
|
||||
def reverse_record_set(self, records):
|
||||
""" Get an equivalent recordset but with elements in reversed order. """
|
||||
return self.env[records._name].browse([record.id for record in reversed(records)])
|
||||
|
||||
def get_last_activities(self, on_record, limit=None):
|
||||
""" Get the last activities on the record in id asc order. """
|
||||
return self.reverse_record_set(self.env['mail.activity'].search(
|
||||
[('res_model', '=', on_record._name), ('res_id', '=', on_record.id)], order='id desc', limit=limit))
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ACTIVITIES MOCK
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@contextmanager
|
||||
def _mock_activities(self):
|
||||
activity_create_origin = MailActivity.create
|
||||
self._new_activities = self.env['mail.activity'].sudo()
|
||||
|
||||
def _activity_create(model, *args, **kwargs):
|
||||
res = activity_create_origin(model, *args, **kwargs)
|
||||
self._new_activities += res.sudo()
|
||||
return res
|
||||
|
||||
with patch.object(
|
||||
MailActivity, 'create', autospec=True, wraps=MailActivity,
|
||||
side_effect=_activity_create
|
||||
) as activity_create_mocked:
|
||||
self.activity_create_mocked = activity_create_mocked
|
||||
yield
|
||||
|
||||
def assertActivityValues(self, activity, activity_values):
|
||||
self.assertEqual(len(activity), 1)
|
||||
for fname, fvalue in activity_values.items():
|
||||
with self.subTest(fname=fname):
|
||||
self.assertEqual(activity[fname], fvalue)
|
||||
|
||||
def assertActivityCreatedOnRecord(self, record, activity_values):
|
||||
activity = self._new_activities.filtered(
|
||||
lambda act: act.res_model == record._name and act.res_id == record.id
|
||||
)
|
||||
self.assertTrue(activity)
|
||||
self.assertActivityValues(activity, activity_values)
|
||||
|
||||
def assertActivityDoneOnRecord(self, record, activity_type):
|
||||
last_message = record.message_ids[0]
|
||||
self.assertEqual(last_message.mail_activity_type_id, activity_type)
|
||||
self.assertIn(activity_type.name, last_message.body)
|
||||
self.assertIn('done', last_message.body)
|
||||
|
||||
def assertActivitiesFromPlan(self, plan, record, expected_deadlines, expected_responsible=None):
|
||||
""" Check that the last activities on the record correspond to the one
|
||||
that the plan must create (number of activities and activities content).
|
||||
|
||||
We check the created activities values against the template values because
|
||||
most of them are just copied when creating activities from templates except
|
||||
for deadlines and responsible for which we pass the expected values as parameters.
|
||||
|
||||
:param <mail.activity.plan> plan: activity plan that has been applied on the record
|
||||
:param recordset record: record on which the plan has been applied
|
||||
:param list<date> expected_deadlines: expected deadlines of the record created activities
|
||||
:param <res.user> expected_responsible: expected responsible for the created activities
|
||||
if set, otherwise checked against the responsible set on the related templates.
|
||||
"""
|
||||
expected_number_of_activity = len(plan.template_ids)
|
||||
activities = self._new_activities.filtered(
|
||||
lambda act: act.res_model == record._name and act.res_id == record.id
|
||||
)
|
||||
self.assertEqual(len(activities), expected_number_of_activity)
|
||||
|
||||
for activity, template, expected_deadline in zip(activities, plan.template_ids, expected_deadlines):
|
||||
self.assertEqual(activity.activity_type_id, template.activity_type_id)
|
||||
self.assertEqual(activity.date_deadline, expected_deadline)
|
||||
self.assertEqual(activity.note, template.note)
|
||||
self.assertEqual(activity.summary, template.summary)
|
||||
self.assertFalse(activity.automated)
|
||||
if expected_responsible:
|
||||
self.assertEqual(activity.user_id, expected_responsible)
|
||||
else:
|
||||
self.assertEqual(activity.user_id, template.responsible_id or self.env.user)
|
||||
|
||||
def assertMessagesFromPlan(self, plan, record, expected_deadlines, expected_responsible=None):
|
||||
""" Check that the last posted message on the record correspond to the one
|
||||
that the plan must generate (number of activities and activities content).
|
||||
|
||||
:param <mail.activity.plan> plan: activity plan that has been applied on the record
|
||||
:param recordset record: record on which the plan has been applied
|
||||
:param list<date> expected_deadlines: expected deadlines of the record created activities
|
||||
:param <res.user> expected_responsible: expected responsible for the created activities
|
||||
if set, otherwise checked against the responsible set on the related templates.
|
||||
"""
|
||||
message = record.message_ids[0]
|
||||
self.assertIn(f'The plan "{plan.name}" has been started', message.body)
|
||||
|
||||
for template, expected_deadline in zip(plan.template_ids, expected_deadlines):
|
||||
if expected_responsible:
|
||||
responsible_id = expected_responsible
|
||||
else:
|
||||
responsible_id = template.responsible_id or self.env.user
|
||||
|
||||
self.assertIn(template.summary, message.body)
|
||||
self.assertIn(f'{template.summary or template.activity_type_id.name}, '
|
||||
f'assigned to {responsible_id.name}, due on the '
|
||||
f'{format_date(self.env, expected_deadline)}', message.body)
|
||||
|
||||
def assertPlanExecution(self, plan, records, expected_deadlines, expected_responsible=None):
|
||||
""" Check that the plan has created the right activities and send the
|
||||
right message on the records (see assertActivitiesFromPlan and
|
||||
assertMessagesFromPlan). """
|
||||
for record in records:
|
||||
self.assertActivitiesFromPlan(plan, record, expected_deadlines, expected_responsible)
|
||||
self.assertMessagesFromPlan(plan, record, expected_deadlines, expected_responsible)
|
||||
|
||||
def _instantiate_activity_schedule_wizard(self, records, additional_context_value=None):
|
||||
""" Get a new Form with context default values referring to the records. """
|
||||
ctx = {
|
||||
'active_id': records.ids[0],
|
||||
'active_ids': records.ids,
|
||||
'active_model': records._name,
|
||||
} if records else {}
|
||||
ctx.update(**(additional_context_value or {}))
|
||||
return Form(self.env['mail.activity.schedule'].with_context(ctx))
|
||||
327
odoo-bringout-oca-ocb-mail/mail/tests/common_controllers.py
Normal file
327
odoo-bringout-oca-ocb-mail/mail/tests/common_controllers.py
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
from markupsafe import Markup
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.base.tests.common import HttpCase
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user, MailCommon
|
||||
from odoo.http import Request
|
||||
from odoo.tests import JsonRpcException
|
||||
from odoo.tools import file_open, mute_logger
|
||||
|
||||
|
||||
class MessagePostSubTestData:
|
||||
def __init__(self, user, allowed, /, *, partners=None, partner_emails=None, add_mention_token=False, route_kw=None,
|
||||
exp_author=None, exp_partners=None, exp_emails=None):
|
||||
self.user = user if user._name == "res.users" else user.env.ref("base.public_user")
|
||||
self.guest = user if user._name == "mail.guest" else user.env["mail.guest"]
|
||||
self.allowed = allowed
|
||||
self.route_kw = {
|
||||
"context": {"mail_post_autofollow_author_skip": True, "mail_post_autofollow": False},
|
||||
**(route_kw or {}),
|
||||
}
|
||||
self.post_data = {
|
||||
"body": "<p>Hello</p>",
|
||||
"message_type": "comment",
|
||||
"subtype_xmlid": "mail.mt_comment",
|
||||
}
|
||||
if partner_emails is not None:
|
||||
self.post_data["partner_emails"] = partner_emails
|
||||
if partners is not None:
|
||||
self.post_data["partner_ids"] = partners.ids
|
||||
if add_mention_token:
|
||||
self.post_data["partner_ids_mention_token"] = {
|
||||
partner.id: partner._get_mention_token() for partner in partners
|
||||
}
|
||||
self.exp_author = exp_author
|
||||
self.exp_partners = exp_partners
|
||||
self.exp_emails = exp_emails
|
||||
|
||||
|
||||
class MailControllerCommon(HttpCase, MailCommon):
|
||||
# Note that '_get_with_access' is going to call '_get_thread_with_access'
|
||||
# which relies on classic portal parameter given as kwargs on most routes
|
||||
# (aka hash, token, pid)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.maxDiff = None
|
||||
cls._create_portal_user()
|
||||
cls.guest = cls.env["mail.guest"].create({"name": "Guest"})
|
||||
last_message = cls.env["mail.message"].search([], order="id desc", limit=1)
|
||||
cls.fake_message = cls.env["mail.message"].browse(last_message.id + 1000000)
|
||||
cls.user_employee_nopartner = mail_new_test_user(
|
||||
cls.env,
|
||||
company_id=cls.company_admin.id,
|
||||
country_id=cls.env.ref('base.be').id,
|
||||
groups='base.group_user,mail.group_mail_template_editor',
|
||||
login='employee_nopartner',
|
||||
name='Elodie EmployeeNoPartner',
|
||||
notification_type='inbox',
|
||||
)
|
||||
# whatever default creation values, we need a "no partner manager" user
|
||||
cls.user_employee_nopartner.write({'group_ids': [(3, cls.env.ref('base.group_partner_manager').id)]})
|
||||
|
||||
def _authenticate_pseudo_user(self, pseudo_user):
|
||||
user = pseudo_user if pseudo_user._name == "res.users" else self.user_public
|
||||
guest = pseudo_user if pseudo_user._name == "mail.guest" else self.env["mail.guest"]
|
||||
if user and user != self.user_public:
|
||||
self.authenticate(user.login, user.login)
|
||||
else:
|
||||
self.authenticate(None, None)
|
||||
if guest:
|
||||
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
|
||||
return user, guest
|
||||
|
||||
def _get_sign_token_params(self, record):
|
||||
if 'access_token' not in record:
|
||||
raise ValueError("Test should run with portal installed")
|
||||
access_token = record._portal_ensure_token()
|
||||
partner = record.env["res.partner"].create({"name": "Sign Partner"})
|
||||
_hash = record._sign_token(partner.id)
|
||||
token = {"token": access_token}
|
||||
bad_token = {"token": "incorrect token"}
|
||||
sign = {"hash": _hash, "pid": partner.id}
|
||||
bad_sign = {"hash": "incorrect hash", "pid": partner.id}
|
||||
return token, bad_token, sign, bad_sign, partner
|
||||
|
||||
|
||||
class MailControllerAttachmentCommon(MailControllerCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.all_users = (
|
||||
cls.guest,
|
||||
cls.user_admin,
|
||||
cls.user_employee,
|
||||
cls.user_portal,
|
||||
cls.user_public,
|
||||
)
|
||||
|
||||
def _execute_subtests_upload(self, document, subtests):
|
||||
for data_user, allowed, *args in subtests:
|
||||
route_kw = args[0] if args else {}
|
||||
user, guest = self._authenticate_pseudo_user(data_user)
|
||||
with self.subTest(document=document, user=user.name, guest=guest.name, route_kw=route_kw):
|
||||
if allowed:
|
||||
attachment_id = self._upload_attachment(document, route_kw)
|
||||
attachment = self.env["ir.attachment"].sudo().search([("id", "=", attachment_id)])
|
||||
self.assertTrue(attachment)
|
||||
else:
|
||||
with self.assertRaises(
|
||||
HTTPError, msg="upload attachment should raise NotFound"
|
||||
):
|
||||
self._upload_attachment(document, route_kw)
|
||||
|
||||
def _execute_subtests_delete(self, users, token, allowed, thread=None):
|
||||
for user_data in users:
|
||||
user, guest = self._authenticate_pseudo_user(user_data)
|
||||
with self.subTest(
|
||||
user=user.name, guest=guest.name, token=token, allowed=allowed, thread=thread,
|
||||
):
|
||||
attachment = self.env["ir.attachment"].create({"name": "sample attachment"})
|
||||
if thread:
|
||||
attachment.write({"res_model": thread._name, "res_id": thread.id})
|
||||
if allowed:
|
||||
self._delete_attachment(attachment, token)
|
||||
self.assertFalse(attachment.exists())
|
||||
else:
|
||||
with self.assertRaises(JsonRpcException, msg="Wrong access token"):
|
||||
self._delete_attachment(attachment, token)
|
||||
|
||||
def _upload_attachment(self, document, route_kw):
|
||||
with mute_logger("odoo.http"), file_open("addons/web/__init__.py") as file:
|
||||
res = self.url_open(
|
||||
url="/mail/attachment/upload",
|
||||
data={
|
||||
"csrf_token": Request.csrf_token(self),
|
||||
"is_pending": True,
|
||||
"thread_id": document.id,
|
||||
"thread_model": document._name,
|
||||
**route_kw,
|
||||
},
|
||||
files={"ufile": file},
|
||||
)
|
||||
res.raise_for_status()
|
||||
data = json.loads(res.content.decode("utf-8"))["data"]
|
||||
self.assertIn(
|
||||
data["attachment_id"],
|
||||
[a["id"] for a in data["store_data"]["ir.attachment"]]
|
||||
)
|
||||
return data["attachment_id"]
|
||||
|
||||
def _delete_attachment(self, attachment, token):
|
||||
with mute_logger("odoo.http"):
|
||||
self.make_jsonrpc_request(
|
||||
route="/mail/attachment/delete",
|
||||
params={
|
||||
"attachment_id": attachment.id,
|
||||
"access_token": attachment._get_ownership_token() if token else None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class MailControllerBinaryCommon(MailControllerCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.guest_2 = cls.env["mail.guest"].create({"name": "Guest 2"})
|
||||
|
||||
def _execute_subtests(self, record, subtests):
|
||||
for data_user, allowed in subtests:
|
||||
user, guest = self._authenticate_pseudo_user(data_user)
|
||||
with self.subTest(user=user.name, guest=guest.name, record=record):
|
||||
if allowed:
|
||||
self.assertEqual(
|
||||
self._get_avatar_url(record).headers["Content-Disposition"],
|
||||
f'inline; filename="{record.name}.svg"',
|
||||
)
|
||||
else:
|
||||
self.assertEqual(
|
||||
self._get_avatar_url(record).headers["Content-Disposition"],
|
||||
"inline; filename=placeholder.png",
|
||||
)
|
||||
|
||||
def _get_avatar_url(self, record):
|
||||
url = f"/web/image?field=avatar_128&id={record.id}&model={record._name}&unique={fields.Datetime.to_string(record.write_date)}"
|
||||
return self.url_open(url)
|
||||
|
||||
def _post_message(self, document, auth_pseudo_user):
|
||||
_user, _guest = self._authenticate_pseudo_user(auth_pseudo_user)
|
||||
self.make_jsonrpc_request(
|
||||
route="/mail/message/post",
|
||||
params={
|
||||
"thread_model": document._name,
|
||||
"thread_id": document.id,
|
||||
"post_data": {
|
||||
"body": "Test",
|
||||
"message_type": "comment",
|
||||
"subtype_xmlid": "mail.mt_comment",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class MailControllerReactionCommon(MailControllerCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.reaction = "😊"
|
||||
|
||||
def _execute_subtests(self, message, subtests):
|
||||
for data_user, allowed, *args in subtests:
|
||||
route_kw = args[0] if args else {}
|
||||
kwargs = args[1] if len(args) > 1 else {}
|
||||
user, guest = self._authenticate_pseudo_user(data_user)
|
||||
with self.subTest(user=user.name, guest=guest.name, route_kw=route_kw):
|
||||
if allowed:
|
||||
self._add_reaction(message, self.reaction, route_kw)
|
||||
reactions = message.reaction_ids
|
||||
self.assertEqual(len(reactions), 1)
|
||||
expected_partner = kwargs.get("partner")
|
||||
if guest and not expected_partner:
|
||||
self.assertEqual(reactions.guest_id, guest)
|
||||
else:
|
||||
self.assertEqual(reactions.partner_id, expected_partner or user.partner_id)
|
||||
self._remove_reaction(message, self.reaction, route_kw)
|
||||
self.assertFalse(message.reaction_ids)
|
||||
else:
|
||||
with self.assertRaises(
|
||||
JsonRpcException, msg="add reaction should raise NotFound"
|
||||
):
|
||||
self._add_reaction(message, self.reaction, route_kw)
|
||||
with self.assertRaises(
|
||||
JsonRpcException, msg="remove reaction should raise NotFound"
|
||||
):
|
||||
self._remove_reaction(message, self.reaction, route_kw)
|
||||
|
||||
def _add_reaction(self, message, content, route_kw):
|
||||
self.make_jsonrpc_request(
|
||||
route="/mail/message/reaction",
|
||||
params={"action": "add", "content": content, "message_id": message.id, **route_kw},
|
||||
)
|
||||
|
||||
def _remove_reaction(self, message, content, route_kw):
|
||||
self.make_jsonrpc_request(
|
||||
route="/mail/message/reaction",
|
||||
params={"action": "remove", "content": content, "message_id": message.id, **route_kw},
|
||||
)
|
||||
|
||||
|
||||
class MailControllerThreadCommon(MailControllerCommon):
|
||||
|
||||
def _execute_message_post_subtests(self, record, tests: list[MessagePostSubTestData]):
|
||||
for test in tests:
|
||||
self._authenticate_pseudo_user(test.user if (test.user and test.user != self.user_public) else test.guest)
|
||||
with self.subTest(record=record, user=test.user.name, guest=test.guest.name, route_kw=test.route_kw):
|
||||
if test.allowed:
|
||||
message = self._message_post(record, test.post_data, test.route_kw)
|
||||
if test.guest and not test.exp_author:
|
||||
self.assertEqual(message.author_guest_id, test.guest)
|
||||
else:
|
||||
self.assertEqual(message.author_id, test.exp_author or test.user.partner_id)
|
||||
if test.exp_partners is not None:
|
||||
self.assertEqual(message.partner_ids, test.exp_partners)
|
||||
if test.exp_emails is not None:
|
||||
self.assertEqual(message.partner_ids.mapped("email"), test.exp_emails)
|
||||
else:
|
||||
with self.assertRaises(JsonRpcException, msg="werkzeug.exceptions.NotFound"):
|
||||
self._message_post(record, test.post_data, test.route_kw)
|
||||
|
||||
def _message_post(self, record, post_data, route_kw):
|
||||
self.make_jsonrpc_request(
|
||||
route="/mail/message/post",
|
||||
params={
|
||||
"thread_model": record._name,
|
||||
"thread_id": record.id,
|
||||
"post_data": post_data,
|
||||
**route_kw,
|
||||
},
|
||||
)
|
||||
return self.env["mail.message"].search(
|
||||
[("res_id", "=", record.id), ("model", "=", record._name)], order="id desc", limit=1
|
||||
)
|
||||
|
||||
|
||||
class MailControllerUpdateCommon(MailControllerCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.message_body = "Message body"
|
||||
cls.alter_message_body = "Altered message body"
|
||||
|
||||
def _execute_subtests(self, message, subtests):
|
||||
for data_user, allowed, *args in subtests:
|
||||
route_kw = args[0] if args else {}
|
||||
user, guest = self._authenticate_pseudo_user(data_user)
|
||||
with self.subTest(user=user.name, guest=guest.name, route_kw=route_kw):
|
||||
if allowed:
|
||||
self._update_content(message.id, self.alter_message_body, route_kw)
|
||||
self.assertEqual(message.body,
|
||||
Markup('<p>Altered message body<span class="o-mail-Message-edited"></span></p>'))
|
||||
else:
|
||||
with self.assertRaises(
|
||||
JsonRpcException,
|
||||
msg="update message content should raise NotFound",
|
||||
):
|
||||
self._update_content(message.id, self.alter_message_body, route_kw)
|
||||
|
||||
def _update_content(self, message_id, body, route_kw):
|
||||
self.make_jsonrpc_request(
|
||||
route="/mail/message/update_content",
|
||||
params={
|
||||
"message_id": message_id,
|
||||
"update_data": {
|
||||
"body": body,
|
||||
"attachment_ids": [],
|
||||
},
|
||||
**route_kw,
|
||||
},
|
||||
)
|
||||
196
odoo-bringout-oca-ocb-mail/mail/tests/common_tracking.py
Normal file
196
odoo-bringout-oca-ocb-mail/mail/tests/common_tracking.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
|
||||
|
||||
class MailTrackingDurationMixinCase(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, tested_model, model_fields=None):
|
||||
|
||||
super().setUpClass()
|
||||
if model_fields:
|
||||
for field in model_fields:
|
||||
if model_fields[field] == 'create':
|
||||
model_fields[field] = cls.env[tested_model][field].create({'name': 'test'}).id
|
||||
|
||||
stage_1_values = {'name': 'Stage 1'}
|
||||
stage_2_values = {'name': 'Stage 2'}
|
||||
stage_3_values = {'name': 'Stage 3'}
|
||||
stage_4_values = {'name': 'Stage 4'}
|
||||
cls.track_duration_field = cls.env[tested_model]._track_duration_field
|
||||
|
||||
stage_model = cls.env[tested_model][cls.track_duration_field]
|
||||
cls.stage_1 = stage_model.create(stage_1_values)
|
||||
cls.stage_2 = stage_model.create(stage_2_values)
|
||||
cls.stage_3 = stage_model.create(stage_3_values)
|
||||
cls.stage_4 = stage_model.create(stage_4_values)
|
||||
|
||||
record_values = {'name': 'test record', cls.track_duration_field: cls.stage_1.id}
|
||||
if model_fields:
|
||||
record_values.update(model_fields)
|
||||
|
||||
cls.mock_start_time = datetime(2023, 2, 15, 12, 0, 0)
|
||||
|
||||
with patch.object(cls.env.cr, 'now', return_value=cls.mock_start_time):
|
||||
cls.rec_1, cls.rec_2, cls.rec_3, cls.rec_4 = cls.env[tested_model].create(
|
||||
[record_values for i in range(4)])
|
||||
cls.flush_tracking(cls)
|
||||
|
||||
def _update_duration_tracking(self, record_to_tracking_dic, minutes, new_stage=False):
|
||||
"""
|
||||
Updates the mock duration_tracking field for multiple records based on the provided minutes.
|
||||
If new_stage is defined, the stage of the records is updated as well.
|
||||
|
||||
Args:
|
||||
record_to_tracking_dic (list): A list of tuples mapping records to their respective tracking dictionaries.
|
||||
minutes (int): The number of minutes to be added to the duration tracking, which will be converted to seconds.
|
||||
new_stage (int, optional): Indicated the new stage to be set for the records. Defaults to False.
|
||||
"""
|
||||
for record, tracking_dic in record_to_tracking_dic:
|
||||
tracking_dic[str(record[self.track_duration_field].id)] += minutes * 60
|
||||
if new_stage:
|
||||
tracking_dic[str(new_stage.id)] += 0
|
||||
record[self.track_duration_field] = new_stage
|
||||
self.flush_tracking()
|
||||
|
||||
def assertTrackingDuration(self, records, record_to_tracking_dic):
|
||||
"""
|
||||
Asserts whether for multiple records their duration_tracking is equal to a dictionary
|
||||
|
||||
Args:
|
||||
records (recordset): all the records that need to be asserted
|
||||
record_to_tracking_dic (list): A list of tuples mapping records to their respective tracking dictionaries.
|
||||
"""
|
||||
records._compute_duration_tracking()
|
||||
for record, tracking_dic in record_to_tracking_dic:
|
||||
self.assertDictEqual(dict(tracking_dic), record.duration_tracking)
|
||||
|
||||
def _test_record_duration_tracking(self):
|
||||
"""
|
||||
Moves a record's many2one field through several values and asserts the duration spent in that value each time
|
||||
"""
|
||||
|
||||
with patch.object(self.env.cr, 'now', return_value=self.mock_start_time) as now:
|
||||
|
||||
track_duration_tracking = defaultdict(lambda: 0)
|
||||
record = self.rec_1
|
||||
|
||||
minutes = 5
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking([(record, track_duration_tracking)], minutes, self.stage_2)
|
||||
self.assertTrackingDuration(record, [(record, track_duration_tracking)])
|
||||
|
||||
minutes = 100
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking([(record, track_duration_tracking)], minutes)
|
||||
self.assertTrackingDuration(record, [(record, track_duration_tracking)])
|
||||
|
||||
minutes = 5000
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking([(record, track_duration_tracking)], minutes, self.stage_3)
|
||||
self.assertTrackingDuration(record, [(record, track_duration_tracking)])
|
||||
|
||||
minutes = 5000
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking([(record, track_duration_tracking)], minutes, self.stage_4)
|
||||
self.assertTrackingDuration(record, [(record, track_duration_tracking)])
|
||||
|
||||
minutes = 20
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking([(record, track_duration_tracking)], minutes, self.stage_2)
|
||||
self.assertTrackingDuration(record, [(record, track_duration_tracking)])
|
||||
|
||||
minutes = 55
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking([(record, track_duration_tracking)], minutes, self.stage_4)
|
||||
self.assertTrackingDuration(record, [(record, track_duration_tracking)])
|
||||
|
||||
minutes = 200
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking([(record, track_duration_tracking)], minutes)
|
||||
self.assertTrackingDuration(record, [(record, track_duration_tracking)])
|
||||
|
||||
minutes = 300
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking([(record, track_duration_tracking)], minutes, self.stage_3)
|
||||
self.assertTrackingDuration(record, [(record, track_duration_tracking)])
|
||||
|
||||
def _test_record_duration_tracking_batch(self):
|
||||
"""
|
||||
Moves for a batch of records many2one field through several values and asserts the duration
|
||||
spent in that value each time.
|
||||
"""
|
||||
|
||||
with patch.object(self.env.cr, 'now', return_value=self.mock_start_time) as now:
|
||||
|
||||
track_duration_tracking1 = defaultdict(lambda: 0)
|
||||
track_duration_tracking2 = defaultdict(lambda: 0)
|
||||
track_duration_tracking3 = defaultdict(lambda: 0)
|
||||
batch = self.rec_1 | self.rec_2 | self.rec_3
|
||||
record_to_tracking_dic = [
|
||||
(self.rec_1, track_duration_tracking1),
|
||||
(self.rec_2, track_duration_tracking2),
|
||||
(self.rec_3, track_duration_tracking3)
|
||||
]
|
||||
|
||||
minutes = 5
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking(record_to_tracking_dic, minutes, self.stage_2)
|
||||
self.assertTrackingDuration(batch, record_to_tracking_dic)
|
||||
|
||||
minutes = 100
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking(record_to_tracking_dic, minutes)
|
||||
self.assertTrackingDuration(batch, record_to_tracking_dic)
|
||||
|
||||
minutes = 5000
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking(record_to_tracking_dic, minutes, self.stage_3)
|
||||
self.assertTrackingDuration(batch, record_to_tracking_dic)
|
||||
|
||||
minutes = 5000
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking(record_to_tracking_dic, minutes, self.stage_4)
|
||||
self.assertTrackingDuration(batch, record_to_tracking_dic)
|
||||
|
||||
minutes = 20
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking(record_to_tracking_dic, minutes, self.stage_2)
|
||||
self.assertTrackingDuration(batch, record_to_tracking_dic)
|
||||
|
||||
minutes = 55
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking(record_to_tracking_dic, minutes, self.stage_4)
|
||||
self.assertTrackingDuration(batch, record_to_tracking_dic)
|
||||
|
||||
minutes = 200
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking(record_to_tracking_dic, minutes)
|
||||
self.assertTrackingDuration(batch, record_to_tracking_dic)
|
||||
|
||||
minutes = 300
|
||||
now.return_value += timedelta(minutes=minutes)
|
||||
self._update_duration_tracking(record_to_tracking_dic, minutes, self.stage_3)
|
||||
self.assertTrackingDuration(batch, record_to_tracking_dic)
|
||||
|
||||
def _test_queries_batch_duration_tracking(self):
|
||||
"""
|
||||
The MailTrackingDuration mixin is only supposed to add 3 queries
|
||||
"""
|
||||
|
||||
batch = self.rec_1 | self.rec_2 | self.rec_3 | self.rec_4
|
||||
batch[self.track_duration_field] = self.stage_2.id
|
||||
self.flush_tracking()
|
||||
batch[self.track_duration_field] = self.stage_4.id
|
||||
self.flush_tracking()
|
||||
batch[self.track_duration_field] = self.stage_1.id
|
||||
self.flush_tracking()
|
||||
batch[self.track_duration_field] = self.stage_3.id
|
||||
self.flush_tracking()
|
||||
batch[self.track_duration_field] = self.stage_2.id
|
||||
|
||||
with self.assertQueryCount(2):
|
||||
batch._compute_duration_tracking()
|
||||
25
odoo-bringout-oca-ocb-mail/mail/tests/discuss/__init__.py
Normal file
25
odoo-bringout-oca-ocb-mail/mail/tests/discuss/__init__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_avatar_acl
|
||||
from . import test_discuss_attachment_controller
|
||||
from . import test_discuss_binary_controller
|
||||
from . import test_discuss_channel_invite
|
||||
from . import test_discuss_action
|
||||
from . import test_discuss_channel
|
||||
from . import test_discuss_channel_access
|
||||
from . import test_discuss_channel_as_guest
|
||||
from . import test_discuss_channel_member
|
||||
from . import test_discuss_mail_presence
|
||||
from . import test_discuss_mention_suggestions
|
||||
from . import test_discuss_message_update_controller
|
||||
from . import test_discuss_reaction_controller
|
||||
from . import test_discuss_res_role
|
||||
from . import test_discuss_sub_channels
|
||||
from . import test_discuss_thread_controller
|
||||
from . import test_guest
|
||||
from . import test_message_controller
|
||||
from . import test_guest_feature
|
||||
from . import test_toggle_upload
|
||||
from . import test_load_messages
|
||||
from . import test_rtc
|
||||
from . import test_ui
|
||||
BIN
odoo-bringout-oca-ocb-mail/mail/tests/discuss/files/test_AES.pdf
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/tests/discuss/files/test_AES.pdf
Normal file
Binary file not shown.
Binary file not shown.
204
odoo-bringout-oca-ocb-mail/mail/tests/discuss/test_avatar_acl.py
Normal file
204
odoo-bringout-oca-ocb-mail/mail/tests/discuss/test_avatar_acl.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, Command
|
||||
from odoo.tests import HttpCase
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestAvatarAcl(HttpCase):
|
||||
def get_avatar_url(self, record, add_token=False):
|
||||
access_token = ""
|
||||
if add_token:
|
||||
access_token = f"&access_token={record._get_avatar_128_access_token()}"
|
||||
return f"/web/image?field=avatar_128&id={record.id}&model={record._name}&unique={fields.Datetime.to_string(record.write_date)}{access_token}"
|
||||
|
||||
def test_partner_open_guest_avatar(self):
|
||||
self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_user")])],
|
||||
"name": "Test User",
|
||||
"login": "testuser",
|
||||
"password": "testuser",
|
||||
}
|
||||
)
|
||||
self.authenticate("testuser", "testuser")
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
res = self.url_open(url=self.get_avatar_url(guest))
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=Guest.svg")
|
||||
|
||||
def test_partner_open_guest_avatar_with_channel(self):
|
||||
testuser = self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_user")])],
|
||||
"name": "Test User",
|
||||
"login": "testuser",
|
||||
"password": "testuser",
|
||||
}
|
||||
)
|
||||
self.authenticate("testuser", "testuser")
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{
|
||||
"group_public_id": None,
|
||||
"name": "Test channel",
|
||||
}
|
||||
)
|
||||
channel._add_members(guests=guest, users=testuser)
|
||||
res = self.url_open(url=self.get_avatar_url(guest))
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=Guest.svg")
|
||||
|
||||
def test_guest_open_partner_avatar(self):
|
||||
self.authenticate(None, None)
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
|
||||
testuser = self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_user")])],
|
||||
"name": "Test User",
|
||||
"login": "testuser",
|
||||
"password": "testuser",
|
||||
}
|
||||
)
|
||||
partner = self.env["res.users"].browse(testuser.id).partner_id
|
||||
res = self.url_open(url=self.get_avatar_url(partner))
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
|
||||
def test_guest_open_partner_avatar_with_channel(self):
|
||||
self.authenticate(None, None)
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
|
||||
testuser = self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_user")])],
|
||||
"name": "Test User",
|
||||
"login": "testuser",
|
||||
"password": "testuser",
|
||||
}
|
||||
)
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{
|
||||
"group_public_id": None,
|
||||
"name": "Test channel",
|
||||
}
|
||||
)
|
||||
channel._add_members(guests=guest, users=testuser)
|
||||
res = self.url_open(url=self.get_avatar_url(testuser.partner_id))
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
res = self.url_open(url=self.get_avatar_url(testuser.partner_id, add_token=True))
|
||||
self.assertEqual(res.headers["Content-Disposition"], f'inline; filename="{testuser.partner_id.name}.svg"')
|
||||
|
||||
def test_partner_open_partner_avatar(self):
|
||||
testuser = self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_user")])],
|
||||
"name": "Test User",
|
||||
"login": "testuser",
|
||||
"password": "testuser",
|
||||
}
|
||||
)
|
||||
self.authenticate("testuser", "testuser")
|
||||
testuser2 = self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser2@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_user")])],
|
||||
"name": "Test User 2",
|
||||
"login": "testuser 2",
|
||||
"password": "testuser 2",
|
||||
}
|
||||
)
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{
|
||||
"group_public_id": None,
|
||||
"name": "Test channel",
|
||||
}
|
||||
)
|
||||
channel._add_members(users=testuser | testuser2)
|
||||
res = self.url_open(url=self.get_avatar_url(testuser2.partner_id))
|
||||
self.assertEqual(res.headers["Content-Disposition"], f'inline; filename="{testuser2.partner_id.name}.svg"')
|
||||
|
||||
def test_guest_open_guest_avatar(self):
|
||||
self.authenticate(None, None)
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
|
||||
guest2 = self.env["mail.guest"].create({"name": "Guest 2"})
|
||||
res = self.url_open(url=self.get_avatar_url(guest2))
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
|
||||
def test_guest_open_guest_avatar_with_channel(self):
|
||||
self.authenticate(None, None)
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
|
||||
guest2 = self.env["mail.guest"].create({"name": "Guest 2"})
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{
|
||||
"group_public_id": None,
|
||||
"name": "Test channel",
|
||||
}
|
||||
)
|
||||
channel._add_members(guests=guest | guest2)
|
||||
res = self.url_open(url=self.get_avatar_url(guest2))
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
res = self.url_open(url=self.get_avatar_url(guest2, add_token=True))
|
||||
self.assertEqual(res.headers["Content-Disposition"], 'inline; filename="Guest 2.svg"')
|
||||
|
||||
def test_portal_open_partner_avatar(self):
|
||||
self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_portal")])],
|
||||
"name": "Test User",
|
||||
"login": "testuser",
|
||||
"password": "testuser",
|
||||
}
|
||||
)
|
||||
self.authenticate("testuser", "testuser")
|
||||
testuser2 = self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser2@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_user")])],
|
||||
"name": "Test User 2",
|
||||
"login": "testuser 2",
|
||||
"password": "testuser 2",
|
||||
}
|
||||
)
|
||||
partner2 = self.env["res.users"].browse(testuser2.id).partner_id
|
||||
res = self.url_open(url=self.get_avatar_url(partner2))
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
|
||||
def test_portal_open_partner_avatar_with_channel(self):
|
||||
testuser = self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_portal")])],
|
||||
"name": "Test User",
|
||||
"login": "testuser",
|
||||
"password": "testuser",
|
||||
}
|
||||
)
|
||||
self.authenticate("testuser", "testuser")
|
||||
testuser2 = self.env["res.users"].create(
|
||||
{
|
||||
"email": "testuser2@testuser.com",
|
||||
"group_ids": [Command.set([self.ref("base.group_user")])],
|
||||
"name": "Test User 2",
|
||||
"login": "testuser 2",
|
||||
"password": "testuser 2",
|
||||
}
|
||||
)
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{
|
||||
"group_public_id": None,
|
||||
"name": "Test channel",
|
||||
}
|
||||
)
|
||||
channel._add_members(users=testuser | testuser2)
|
||||
res = self.url_open(url=self.get_avatar_url(testuser2.partner_id))
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
res = self.url_open(url=self.get_avatar_url(testuser2.partner_id, add_token=True))
|
||||
self.assertEqual(res.headers["Content-Disposition"], f'inline; filename="{testuser2.partner_id.name}.svg"')
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
try:
|
||||
import websocket as ws
|
||||
except ImportError:
|
||||
ws = None
|
||||
|
||||
from odoo.tests import tagged, new_test_user
|
||||
from odoo.addons.bus.tests.common import WebsocketCase
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.bus.models.bus import channel_with_db, json_dump
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestBusPresence(WebsocketCase, MailCommon):
|
||||
def _receive_presence(self, sender, recipient):
|
||||
self._reset_bus()
|
||||
sent_from_user = isinstance(sender, self.env.registry["res.users"])
|
||||
receive_to_user = isinstance(recipient, self.env.registry["res.users"])
|
||||
if receive_to_user:
|
||||
session = self.authenticate(recipient.login, recipient.login)
|
||||
auth_cookie = f"session_id={session.sid};"
|
||||
else:
|
||||
self.authenticate(None, None)
|
||||
auth_cookie = f"{recipient._cookie_name}={recipient._format_auth_cookie()};"
|
||||
websocket = self.websocket_connect(cookie=auth_cookie)
|
||||
sender_bus_target = sender.partner_id if sent_from_user else sender
|
||||
self.subscribe(
|
||||
websocket,
|
||||
[f"odoo-presence-{sender_bus_target._name}_{sender_bus_target.id}"],
|
||||
self.env["bus.bus"]._bus_last_id(),
|
||||
)
|
||||
self.env["bus.presence"].create(
|
||||
{"user_id" if sent_from_user else "guest_id": sender.id, "status": "online"}
|
||||
)
|
||||
self.trigger_notification_dispatching([(sender_bus_target, "presence")])
|
||||
notifications = json.loads(websocket.recv())
|
||||
self._close_websockets()
|
||||
bus_record = self.env["bus.bus"].search([("id", "=", int(notifications[0]["id"]))])
|
||||
self.assertEqual(
|
||||
bus_record.channel,
|
||||
json_dump(channel_with_db(self.env.cr.dbname, (sender_bus_target, "presence"))),
|
||||
)
|
||||
self.assertEqual(notifications[0]["message"]["type"], "bus.bus/im_status_updated")
|
||||
self.assertEqual(notifications[0]["message"]["payload"]["im_status"], "online")
|
||||
self.assertEqual(notifications[0]["message"]["payload"]["presence_status"], "online")
|
||||
self.assertEqual(
|
||||
notifications[0]["message"]["payload"]["partner_id" if sent_from_user else "guest_id"],
|
||||
sender_bus_target.id,
|
||||
)
|
||||
|
||||
def test_receive_presences_as_guest(self):
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
bob = new_test_user(self.env, login="bob_user", groups="base.group_user")
|
||||
# Guest should not receive users's presence: no common channel.
|
||||
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
|
||||
self._receive_presence(sender=bob, recipient=guest)
|
||||
channel = self.env["discuss.channel"].channel_create(group_id=None, name="General")
|
||||
channel.add_members(guest_ids=[guest.id], partner_ids=[bob.partner_id.id])
|
||||
# Now that they share a channel, guest should receive users's presence.
|
||||
self._receive_presence(sender=bob, recipient=guest)
|
||||
|
||||
other_guest = self.env["mail.guest"].create({"name": "OtherGuest"})
|
||||
# Guest should not receive guest's presence: no common channel.
|
||||
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
|
||||
self._receive_presence(sender=other_guest, recipient=guest)
|
||||
channel.add_members(guest_ids=[other_guest.id])
|
||||
# Now that they share a channel, guest should receive guest's presence.
|
||||
self._receive_presence(sender=other_guest, recipient=guest)
|
||||
|
||||
def test_receive_presences_as_portal(self):
|
||||
portal = new_test_user(self.env, login="portal_user", groups="base.group_portal")
|
||||
bob = new_test_user(self.env, login="bob_user", groups="base.group_user")
|
||||
# Portal should not receive users's presence: no common channel.
|
||||
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
|
||||
self._receive_presence(sender=bob, recipient=portal)
|
||||
channel = self.env["discuss.channel"].channel_create(group_id=None, name="General")
|
||||
channel.add_members(partner_ids=[portal.partner_id.id, bob.partner_id.id])
|
||||
# Now that they share a channel, portal should receive users's presence.
|
||||
self._receive_presence(sender=bob, recipient=portal)
|
||||
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
# Portal should not receive guest's presence: no common channel.
|
||||
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
|
||||
self._receive_presence(sender=guest, recipient=portal)
|
||||
channel.add_members(guest_ids=[guest.id])
|
||||
# Now that they share a channel, portal should receive guest's presence.
|
||||
self._receive_presence(sender=guest, recipient=portal)
|
||||
|
||||
def test_receive_presences_as_internal(self):
|
||||
internal = new_test_user(self.env, login="internal_user", groups="base.group_user")
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
# Internal can access guest's presence regardless of their channels.
|
||||
self._receive_presence(sender=guest, recipient=internal)
|
||||
# Internal can access users's presence regardless of their channels.
|
||||
bob = new_test_user(self.env, login="bob_user", groups="base.group_user")
|
||||
self._receive_presence(sender=bob, recipient=internal)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.tests import HttpCase, tagged
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install", "discuss_action")
|
||||
class TestDiscussAction(HttpCase, MailCommon):
|
||||
def test_go_back_to_thread_from_breadcrumbs(self):
|
||||
self.start_tour(
|
||||
"/odoo/discuss?active_id=mail.box_inbox",
|
||||
"discuss_go_back_to_thread_from_breadcrumbs.js",
|
||||
login="admin",
|
||||
)
|
||||
|
||||
def test_join_call_with_client_action(self):
|
||||
inviting_user = self.env['res.users'].sudo().create({'name': "Inviting User", 'login': 'inviting'})
|
||||
invited_user = self.env['res.users'].sudo().create({'name': "Invited User", 'login': 'invited'})
|
||||
channel = self.env['discuss.channel'].with_user(inviting_user)._get_or_create_chat(partners_to=invited_user.partner_id.ids)
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(
|
||||
lambda channel_member: channel_member.partner_id == inviting_user.partner_id)
|
||||
self._reset_bus()
|
||||
channel_member._rtc_join_call()
|
||||
self.start_tour(
|
||||
f"/odoo/{channel.id}/action-mail.action_discuss?call=accept",
|
||||
"discuss_channel_call_action.js",
|
||||
login=invited_user.login,
|
||||
)
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo
|
||||
from odoo.addons.mail.tests.common_controllers import MailControllerAttachmentCommon
|
||||
from odoo.tools.misc import file_open
|
||||
|
||||
|
||||
@odoo.tests.tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestDiscussAttachmentController(MailControllerAttachmentCommon):
|
||||
def test_attachment_allowed_upload_public_channel(self):
|
||||
"""Test access to upload an attachment on an allowed upload public channel"""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"group_public_id": None, "name": "public channel"}
|
||||
)
|
||||
channel._add_members(guests=self.guest)
|
||||
channel = channel.with_context(guest=self.guest)
|
||||
self._execute_subtests_upload(
|
||||
channel,
|
||||
(
|
||||
(self.guest, True),
|
||||
(self.user_admin, True),
|
||||
(self.user_employee, True),
|
||||
(self.user_portal, True),
|
||||
(self.user_public, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_attachment_delete_linked_to_public_channel(self):
|
||||
"""Test access to delete an attachment associated with a public channel
|
||||
whether or not limited `ownership_token` is sent"""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"group_public_id": None, "name": "public channel"}
|
||||
)
|
||||
self._execute_subtests_delete(self.all_users, token=True, allowed=True, thread=channel)
|
||||
self._execute_subtests_delete(
|
||||
(self.user_admin, self.user_employee),
|
||||
token=False,
|
||||
allowed=True,
|
||||
thread=channel,
|
||||
)
|
||||
self._execute_subtests_delete(
|
||||
(self.guest, self.user_portal, self.user_public),
|
||||
token=False,
|
||||
allowed=False,
|
||||
thread=channel,
|
||||
)
|
||||
|
||||
def test_attachment_delete_linked_to_private_channel(self):
|
||||
"""Test access to delete an attachment associated with a private channel
|
||||
whether or not limited `ownership_token` is sent"""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"name": "Private Channel", "channel_type": "group"}
|
||||
)
|
||||
self._execute_subtests_delete(self.all_users, token=True, allowed=True, thread=channel)
|
||||
self._execute_subtests_delete(self.user_admin, token=False, allowed=True, thread=channel)
|
||||
self._execute_subtests_delete(
|
||||
(self.guest, self.user_employee, self.user_portal, self.user_public),
|
||||
token=False,
|
||||
allowed=False,
|
||||
thread=channel,
|
||||
)
|
||||
|
||||
def test_first_page_access_of_mail_attachment_pdf(self):
|
||||
"""Test accessing the first page of a PDF that is encrypted(test_AES.pdf) or has invalid encoding(test_unicode.pdf)."""
|
||||
attachments = []
|
||||
for pdf in (
|
||||
'mail/tests/discuss/files/test_AES.pdf',
|
||||
'mail/tests/discuss/files/test_unicode.pdf',
|
||||
):
|
||||
with file_open(pdf, "rb") as file:
|
||||
attachments.append({
|
||||
'name': pdf,
|
||||
'raw': file.read(),
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
attachments = self.env['ir.attachment'].create(attachments)
|
||||
|
||||
self.authenticate("admin", "admin")
|
||||
|
||||
for attachment in attachments:
|
||||
ownership_token = attachment._get_ownership_token()
|
||||
url = f'/mail/attachment/pdf_first_page/{attachment.id}?access_token={ownership_token}'
|
||||
response = self.url_open(url)
|
||||
# Depending on the environment, the response status_code may vary:
|
||||
# - 200 if PyPDF2 and PyCryptodome are installed (PDF successfully parsed)
|
||||
# - 415 if those libs are missing (PDF cannot be processed)
|
||||
self.assertIn(response.status_code, [415, 200])
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
from odoo.addons.mail.tests.common_controllers import MailControllerBinaryCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestDiscussBinaryController(MailControllerBinaryCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.private_channel = cls.env["discuss.channel"].create(
|
||||
{"name": "Private Channel", "channel_type": "group"}
|
||||
)
|
||||
cls.public_channel = cls.env["discuss.channel"]._create_channel(
|
||||
name="Public Channel", group_id=None
|
||||
)
|
||||
cls.users = (
|
||||
cls.user_public + cls.user_portal + cls.user_employee + cls.user_admin
|
||||
)
|
||||
|
||||
def test_open_guest_avatar(self):
|
||||
"""Test access to open the avatar of a guest.
|
||||
There is no common channel or any interaction from the guest."""
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_01_guest_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: group
|
||||
- target joins the channel: True
|
||||
- other users join the channel: True
|
||||
- target sends a message: False"""
|
||||
self.private_channel._add_members(users=self.users, guests=self.guest | self.guest_2)
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_01_partner_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: partner
|
||||
- channel type: group
|
||||
- target joins the channel: True
|
||||
- other users join the channel: True
|
||||
- target sends a message: False"""
|
||||
self.private_channel._add_members(
|
||||
users=self.users | self.user_employee_nopartner, guests=self.guest
|
||||
)
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_02_guest_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: group
|
||||
- target joins the channel: True
|
||||
- other users join the channel: True
|
||||
- target sends a message: True"""
|
||||
self.private_channel._add_members(users=self.users, guests=self.guest | self.guest_2)
|
||||
self._post_message(self.private_channel, self.guest_2)
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_02_partner_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: partner
|
||||
- channel type: group
|
||||
- target joins the channel: True
|
||||
- other users join the channel: True
|
||||
- target sends a message: True"""
|
||||
self.private_channel._add_members(
|
||||
users=self.users | self.user_employee_nopartner, guests=self.guest
|
||||
)
|
||||
self._post_message(self.private_channel, self.user_employee_nopartner)
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_03_guest_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: group
|
||||
- target joins the channel: True
|
||||
- other users join the channel: True
|
||||
- target sends a message: False
|
||||
- target leaves the channel: True"""
|
||||
self.private_channel._add_members(users=self.users, guests=self.guest | self.guest_2)
|
||||
self.env["discuss.channel.member"].search(
|
||||
[("guest_id", "=", self.guest_2.id), ("channel_id", "=", self.private_channel.id)]
|
||||
).unlink()
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_03_partner_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: partner
|
||||
- channel type: group
|
||||
- target joins the channel: True
|
||||
- other users join the channel: True
|
||||
- target sends a message: False
|
||||
- target leaves the channel: True"""
|
||||
self.private_channel._add_members(
|
||||
users=self.users | self.user_employee_nopartner, guests=self.guest
|
||||
)
|
||||
self.env["discuss.channel.member"].search(
|
||||
[
|
||||
("partner_id", "=", self.user_employee_nopartner.partner_id.id),
|
||||
("channel_id", "=", self.private_channel.id),
|
||||
]
|
||||
).unlink()
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_04_guest_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: group
|
||||
- target joins the channel: True
|
||||
- other users join the channel: True
|
||||
- target sends a message: True
|
||||
- target leaves the channel: True"""
|
||||
self.private_channel._add_members(users=self.users, guests=self.guest | self.guest_2)
|
||||
self._post_message(self.private_channel, self.guest_2)
|
||||
self.env["discuss.channel.member"].search(
|
||||
[("guest_id", "=", self.guest_2.id), ("channel_id", "=", self.private_channel.id)]
|
||||
).unlink()
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_04_partner_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: partner
|
||||
- channel type: group
|
||||
- target joins the channel: True
|
||||
- other users join the channel: True
|
||||
- target sends a message: True
|
||||
- target leaves the channel: True"""
|
||||
self.private_channel._add_members(
|
||||
users=self.users | self.user_employee_nopartner, guests=self.guest
|
||||
)
|
||||
self._post_message(self.private_channel, self.user_employee_nopartner)
|
||||
self.env["discuss.channel.member"].search(
|
||||
[
|
||||
("partner_id", "=", self.user_employee_nopartner.partner_id.id),
|
||||
("channel_id", "=", self.private_channel.id),
|
||||
]
|
||||
).unlink()
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_05_guest_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: group
|
||||
- target joins the channel: False
|
||||
- other users join the channel: False
|
||||
- target sends a message: True"""
|
||||
self.private_channel.with_user(self.user_public).with_context(
|
||||
guest=self.guest_2
|
||||
).sudo().message_post(body="Test", subtype_xmlid="mail.mt_comment", message_type="comment")
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_05_partner_avatar_private_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: partner
|
||||
- channel type: group
|
||||
- target joins the channel: False
|
||||
- other users join the channel: False
|
||||
- target sends a message: True"""
|
||||
self.private_channel.message_post(
|
||||
body="Test",
|
||||
subtype_xmlid="mail.mt_comment",
|
||||
message_type="comment",
|
||||
author_id=self.user_employee_nopartner.partner_id.id,
|
||||
)
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_01_guest_avatar_public_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: public
|
||||
- target joins the channel: False
|
||||
- other users join the channel: False
|
||||
- target sends a message: True"""
|
||||
self.public_channel.with_user(self.user_public).with_context(
|
||||
guest=self.guest_2
|
||||
).sudo().message_post(body="Test", subtype_xmlid="mail.mt_comment", message_type="comment")
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_01_partner_avatar_public_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: public
|
||||
- target joins the channel: False
|
||||
- other users join the channel: False
|
||||
- target sends a message: True"""
|
||||
self._post_message(self.public_channel, self.user_employee_nopartner)
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_02_guest_avatar_public_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: public
|
||||
- target joins the channel: True
|
||||
- other users join the channel: False
|
||||
- target sends a message: False
|
||||
- target leaves the channel: True"""
|
||||
target_member = self.public_channel._add_members(guests=self.guest_2)
|
||||
target_member.unlink()
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_02_partner_avatar_public_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: partner
|
||||
- channel type: public
|
||||
- target joins the channel: True
|
||||
- other users join the channel: False
|
||||
- target sends a message: False
|
||||
- target leaves the channel: True"""
|
||||
target_member = self.public_channel._add_members(users=self.user_employee_nopartner)
|
||||
target_member.unlink()
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_03_guest_avatar_public_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: guest
|
||||
- channel type: public
|
||||
- target joins the channel: True
|
||||
- other users join the channel: False
|
||||
- target sends a message: True
|
||||
- target leaves the channel: True"""
|
||||
target_member = self.public_channel._add_members(guests=self.guest_2)
|
||||
self._post_message(self.public_channel, self.guest_2)
|
||||
target_member.unlink()
|
||||
self._execute_subtests(
|
||||
self.guest_2,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_03_partner_avatar_public_channel(self):
|
||||
"""Test access to open the avatar:
|
||||
- target type: partner
|
||||
- channel type: public
|
||||
- target joins the channel: True
|
||||
- other users join the channel: False
|
||||
- target sends a message: True
|
||||
- target leaves the channel: True"""
|
||||
target_member = self.public_channel._add_members(users=self.user_employee_nopartner)
|
||||
self._post_message(self.public_channel, self.user_employee_nopartner)
|
||||
target_member.unlink()
|
||||
self._execute_subtests(
|
||||
self.user_employee_nopartner.partner_id,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,572 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from psycopg2.errors import UniqueViolation
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestDiscussChannelAccess(MailCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._channel_type_channel_access_cases = [
|
||||
("public", "no_group", "member", "read", True),
|
||||
("public", "no_group", "member", "write", False),
|
||||
("public", "no_group", "member", "unlink", False),
|
||||
("public", "no_group", "outside", "create", False),
|
||||
("public", "no_group", "outside", "read", True),
|
||||
("public", "no_group", "outside", "write", False),
|
||||
("public", "no_group", "outside", "unlink", False),
|
||||
("public", "group_matching", "member", "read", True),
|
||||
("public", "group_matching", "member", "write", False),
|
||||
("public", "group_matching", "member", "unlink", False),
|
||||
("public", "group_matching", "outside", "create", False),
|
||||
("public", "group_matching", "outside", "read", True),
|
||||
("public", "group_matching", "outside", "write", False),
|
||||
("public", "group_matching", "outside", "unlink", False),
|
||||
("public", "group_failing", "member", "read", False),
|
||||
("public", "group_failing", "member", "write", False),
|
||||
("public", "group_failing", "member", "unlink", False),
|
||||
("public", "group_failing", "outside", "create", False),
|
||||
("public", "group_failing", "outside", "read", False),
|
||||
("public", "group_failing", "outside", "write", False),
|
||||
("public", "group_failing", "outside", "unlink", False),
|
||||
("portal", "no_group", "member", "read", True),
|
||||
("portal", "no_group", "member", "write", False),
|
||||
("portal", "no_group", "member", "unlink", False),
|
||||
("portal", "no_group", "outside", "create", False),
|
||||
("portal", "no_group", "outside", "read", True),
|
||||
("portal", "no_group", "outside", "write", False),
|
||||
("portal", "no_group", "outside", "unlink", False),
|
||||
("portal", "group_matching", "member", "read", True),
|
||||
("portal", "group_matching", "member", "write", False),
|
||||
("portal", "group_matching", "member", "unlink", False),
|
||||
("portal", "group_matching", "outside", "create", False),
|
||||
("portal", "group_matching", "outside", "read", True),
|
||||
("portal", "group_matching", "outside", "write", False),
|
||||
("portal", "group_matching", "outside", "unlink", False),
|
||||
("portal", "group_failing", "member", "read", False),
|
||||
("portal", "group_failing", "member", "write", False),
|
||||
("portal", "group_failing", "member", "unlink", False),
|
||||
("portal", "group_failing", "outside", "create", False),
|
||||
("portal", "group_failing", "outside", "read", False),
|
||||
("portal", "group_failing", "outside", "write", False),
|
||||
("portal", "group_failing", "outside", "unlink", False),
|
||||
("user", "no_group", "member", "read", True),
|
||||
("user", "no_group", "member", "write", True),
|
||||
("user", "no_group", "member", "unlink", False),
|
||||
("user", "no_group", "outside", "create", True),
|
||||
("user", "no_group", "outside", "read", True),
|
||||
("user", "no_group", "outside", "write", True),
|
||||
("user", "no_group", "outside", "unlink", False),
|
||||
("user", "group_matching", "member", "read", True),
|
||||
("user", "group_matching", "member", "write", True),
|
||||
("user", "group_matching", "member", "unlink", False),
|
||||
("user", "group_matching", "outside", "create", True),
|
||||
("user", "group_matching", "outside", "read", True),
|
||||
("user", "group_matching", "outside", "write", True),
|
||||
("user", "group_matching", "outside", "unlink", False),
|
||||
("user", "group_failing", "member", "read", False),
|
||||
("user", "group_failing", "member", "write", False),
|
||||
("user", "group_failing", "member", "unlink", False),
|
||||
("user", "group_failing", "outside", "create", False),
|
||||
("user", "group_failing", "outside", "read", False),
|
||||
("user", "group_failing", "outside", "write", False),
|
||||
("user", "group_failing", "outside", "unlink", False),
|
||||
]
|
||||
cls._channel_type_channel_member_access_cases = [
|
||||
("public", "no_group", "member", "self", "create", False),
|
||||
("public", "no_group", "member", "self", "read", True),
|
||||
("public", "no_group", "member", "self", "write", True),
|
||||
("public", "no_group", "member", "self", "unlink", True),
|
||||
("public", "no_group", "member", "other", "create", False),
|
||||
("public", "no_group", "member", "other", "read", True),
|
||||
("public", "no_group", "member", "other", "write", False),
|
||||
("public", "no_group", "member", "other", "unlink", False),
|
||||
("public", "no_group", "outside", "self", "create", True),
|
||||
("public", "no_group", "outside", "other", "create", False),
|
||||
("public", "no_group", "outside", "other", "read", True),
|
||||
("public", "no_group", "outside", "other", "write", False),
|
||||
("public", "no_group", "outside", "other", "unlink", False),
|
||||
("public", "group_matching", "member", "self", "create", False),
|
||||
("public", "group_matching", "member", "self", "read", True),
|
||||
("public", "group_matching", "member", "self", "write", True),
|
||||
("public", "group_matching", "member", "self", "unlink", True),
|
||||
("public", "group_matching", "member", "other", "create", False),
|
||||
("public", "group_matching", "member", "other", "read", True),
|
||||
("public", "group_matching", "member", "other", "write", False),
|
||||
("public", "group_matching", "member", "other", "unlink", False),
|
||||
("public", "group_matching", "outside", "self", "create", True),
|
||||
("public", "group_matching", "outside", "other", "create", False),
|
||||
("public", "group_matching", "outside", "other", "read", True),
|
||||
("public", "group_matching", "outside", "other", "write", False),
|
||||
("public", "group_matching", "outside", "other", "unlink", False),
|
||||
("public", "group_failing", "member", "self", "create", False),
|
||||
("public", "group_failing", "member", "self", "read", False),
|
||||
("public", "group_failing", "member", "self", "write", False),
|
||||
("public", "group_failing", "member", "self", "unlink", False),
|
||||
("public", "group_failing", "member", "other", "create", False),
|
||||
("public", "group_failing", "member", "other", "read", False),
|
||||
("public", "group_failing", "member", "other", "write", False),
|
||||
("public", "group_failing", "member", "other", "unlink", False),
|
||||
("public", "group_failing", "outside", "self", "create", False),
|
||||
("public", "group_failing", "outside", "other", "create", False),
|
||||
("public", "group_failing", "outside", "other", "read", False),
|
||||
("public", "group_failing", "outside", "other", "write", False),
|
||||
("public", "group_failing", "outside", "other", "unlink", False),
|
||||
("portal", "no_group", "member", "self", "create", False),
|
||||
("portal", "no_group", "member", "self", "read", True),
|
||||
("portal", "no_group", "member", "self", "write", True),
|
||||
("portal", "no_group", "member", "self", "unlink", True),
|
||||
("portal", "no_group", "member", "other", "create", False),
|
||||
("portal", "no_group", "member", "other", "read", True),
|
||||
("portal", "no_group", "member", "other", "write", False),
|
||||
("portal", "no_group", "member", "other", "unlink", False),
|
||||
("portal", "no_group", "outside", "self", "create", True),
|
||||
("portal", "no_group", "outside", "other", "create", False),
|
||||
("portal", "no_group", "outside", "other", "read", True),
|
||||
("portal", "no_group", "outside", "other", "write", False),
|
||||
("portal", "no_group", "outside", "other", "unlink", False),
|
||||
("portal", "group_matching", "member", "self", "create", False),
|
||||
("portal", "group_matching", "member", "self", "read", True),
|
||||
("portal", "group_matching", "member", "self", "write", True),
|
||||
("portal", "group_matching", "member", "self", "unlink", True),
|
||||
("portal", "group_matching", "member", "other", "create", False),
|
||||
("portal", "group_matching", "member", "other", "read", True),
|
||||
("portal", "group_matching", "member", "other", "write", False),
|
||||
("portal", "group_matching", "member", "other", "unlink", False),
|
||||
("portal", "group_matching", "outside", "self", "create", True),
|
||||
("portal", "group_matching", "outside", "other", "create", False),
|
||||
("portal", "group_matching", "outside", "other", "read", True),
|
||||
("portal", "group_matching", "outside", "other", "write", False),
|
||||
("portal", "group_matching", "outside", "other", "unlink", False),
|
||||
("portal", "group_failing", "member", "self", "create", False),
|
||||
("portal", "group_failing", "member", "self", "read", False),
|
||||
("portal", "group_failing", "member", "self", "write", False),
|
||||
("portal", "group_failing", "member", "self", "unlink", False),
|
||||
("portal", "group_failing", "member", "other", "create", False),
|
||||
("portal", "group_failing", "member", "other", "read", False),
|
||||
("portal", "group_failing", "member", "other", "write", False),
|
||||
("portal", "group_failing", "member", "other", "unlink", False),
|
||||
("portal", "group_failing", "outside", "self", "create", False),
|
||||
("portal", "group_failing", "outside", "other", "create", False),
|
||||
("portal", "group_failing", "outside", "other", "read", False),
|
||||
("portal", "group_failing", "outside", "other", "write", False),
|
||||
("portal", "group_failing", "outside", "other", "unlink", False),
|
||||
("user", "no_group", "member", "self", "create", False),
|
||||
("user", "no_group", "member", "self", "read", True),
|
||||
("user", "no_group", "member", "self", "write", True),
|
||||
("user", "no_group", "member", "self", "unlink", True),
|
||||
("user", "no_group", "member", "other", "create", True),
|
||||
("user", "no_group", "member", "other", "read", True),
|
||||
("user", "no_group", "member", "other", "write", False),
|
||||
("user", "no_group", "member", "other", "unlink", False),
|
||||
("user", "no_group", "outside", "self", "create", True),
|
||||
("user", "no_group", "outside", "other", "create", True),
|
||||
("user", "no_group", "outside", "other", "read", True),
|
||||
("user", "no_group", "outside", "other", "write", False),
|
||||
("user", "no_group", "outside", "other", "unlink", False),
|
||||
("user", "group_matching", "member", "self", "create", False),
|
||||
("user", "group_matching", "member", "self", "read", True),
|
||||
("user", "group_matching", "member", "self", "write", True),
|
||||
("user", "group_matching", "member", "self", "unlink", True),
|
||||
("user", "group_matching", "member", "other", "create", True),
|
||||
("user", "group_matching", "member", "other", "read", True),
|
||||
("user", "group_matching", "member", "other", "write", False),
|
||||
("user", "group_matching", "member", "other", "unlink", False),
|
||||
("user", "group_matching", "outside", "self", "create", True),
|
||||
("user", "group_matching", "outside", "other", "create", True),
|
||||
("user", "group_matching", "outside", "other", "read", True),
|
||||
("user", "group_matching", "outside", "other", "write", False),
|
||||
("user", "group_matching", "outside", "other", "unlink", False),
|
||||
("user", "group_failing", "member", "self", "create", False),
|
||||
("user", "group_failing", "member", "self", "read", False),
|
||||
("user", "group_failing", "member", "self", "write", False),
|
||||
("user", "group_failing", "member", "self", "unlink", False),
|
||||
("user", "group_failing", "member", "other", "create", False),
|
||||
("user", "group_failing", "member", "other", "read", False),
|
||||
("user", "group_failing", "member", "other", "write", False),
|
||||
("user", "group_failing", "member", "other", "unlink", False),
|
||||
("user", "group_failing", "outside", "self", "create", False),
|
||||
("user", "group_failing", "outside", "other", "create", False),
|
||||
("user", "group_failing", "outside", "other", "read", False),
|
||||
("user", "group_failing", "outside", "other", "write", False),
|
||||
("user", "group_failing", "outside", "other", "unlink", False),
|
||||
]
|
||||
cls._group_type_channel_access_cases = [
|
||||
("public", "group", "member", "read", True),
|
||||
("public", "group", "member", "write", False),
|
||||
("public", "group", "member", "unlink", False),
|
||||
("public", "group", "outside", "create", False),
|
||||
("public", "group", "outside", "read", False),
|
||||
("public", "group", "outside", "write", False),
|
||||
("public", "group", "outside", "unlink", False),
|
||||
("portal", "group", "member", "read", True),
|
||||
("portal", "group", "member", "write", False),
|
||||
("portal", "group", "member", "unlink", False),
|
||||
("portal", "group", "outside", "create", False),
|
||||
("portal", "group", "outside", "read", False),
|
||||
("portal", "group", "outside", "write", False),
|
||||
("portal", "group", "outside", "unlink", False),
|
||||
("user", "group", "member", "read", True),
|
||||
("user", "group", "member", "write", True),
|
||||
("user", "group", "member", "unlink", False),
|
||||
("user", "group", "outside", "create", True),
|
||||
("user", "group", "outside", "read", False),
|
||||
("user", "group", "outside", "write", False),
|
||||
("user", "group", "outside", "unlink", False),
|
||||
]
|
||||
cls._group_type_channel_member_access_cases = [
|
||||
("public", "group", "member", "self", "create", False),
|
||||
("public", "group", "member", "self", "read", True),
|
||||
("public", "group", "member", "self", "write", True),
|
||||
("public", "group", "member", "self", "unlink", True),
|
||||
("public", "group", "member", "other", "create", False),
|
||||
("public", "group", "member", "other", "read", True),
|
||||
("public", "group", "member", "other", "write", False),
|
||||
("public", "group", "member", "other", "unlink", False),
|
||||
("public", "group", "outside", "self", "create", False),
|
||||
("public", "group", "outside", "other", "create", False),
|
||||
("public", "group", "outside", "other", "read", False),
|
||||
("public", "group", "outside", "other", "write", False),
|
||||
("public", "group", "outside", "other", "unlink", False),
|
||||
("portal", "group", "member", "self", "create", False),
|
||||
("portal", "group", "member", "self", "read", True),
|
||||
("portal", "group", "member", "self", "write", True),
|
||||
("portal", "group", "member", "self", "unlink", True),
|
||||
("portal", "group", "member", "other", "create", False),
|
||||
("portal", "group", "member", "other", "read", True),
|
||||
("portal", "group", "member", "other", "write", False),
|
||||
("portal", "group", "member", "other", "unlink", False),
|
||||
("portal", "group", "outside", "self", "create", False),
|
||||
("portal", "group", "outside", "other", "create", False),
|
||||
("portal", "group", "outside", "other", "read", False),
|
||||
("portal", "group", "outside", "other", "write", False),
|
||||
("portal", "group", "outside", "other", "unlink", False),
|
||||
("user", "group", "member", "self", "create", False),
|
||||
("user", "group", "member", "self", "read", True),
|
||||
("user", "group", "member", "self", "write", True),
|
||||
("user", "group", "member", "self", "unlink", True),
|
||||
("user", "group", "member", "other", "create", True),
|
||||
("user", "group", "member", "other", "read", True),
|
||||
("user", "group", "member", "other", "write", False),
|
||||
("user", "group", "member", "other", "unlink", False),
|
||||
("user", "group", "outside", "self", "create", False),
|
||||
("user", "group", "outside", "other", "create", False),
|
||||
("user", "group", "outside", "other", "read", False),
|
||||
("user", "group", "outside", "other", "write", False),
|
||||
("user", "group", "outside", "other", "unlink", False),
|
||||
]
|
||||
cls.secret_group = cls.env["res.groups"].create({"name": "Secret User Group"})
|
||||
cls.env["ir.model.data"].create(
|
||||
{
|
||||
"name": "secret_group",
|
||||
"module": "mail",
|
||||
"model": cls.secret_group._name,
|
||||
"res_id": cls.secret_group.id,
|
||||
}
|
||||
)
|
||||
cls.guest = cls.env["mail.guest"].create({"name": "A Guest"}).sudo(False)
|
||||
cls.users = {
|
||||
"public": mail_new_test_user(
|
||||
cls.env,
|
||||
login="public1",
|
||||
name="A Public User",
|
||||
groups="base.group_public,mail.secret_group",
|
||||
),
|
||||
"portal": mail_new_test_user(
|
||||
cls.env,
|
||||
login="portal1",
|
||||
name="A Portal User",
|
||||
groups="base.group_portal,mail.secret_group",
|
||||
),
|
||||
"user": mail_new_test_user(
|
||||
cls.env,
|
||||
login="user1",
|
||||
name="An Internal User",
|
||||
groups="base.group_user,mail.secret_group",
|
||||
),
|
||||
}
|
||||
cls.other_user = mail_new_test_user(
|
||||
cls.env,
|
||||
login="other1",
|
||||
name="Another User 1",
|
||||
groups="base.group_user,mail.secret_group",
|
||||
)
|
||||
cls.other_user_2 = mail_new_test_user(
|
||||
cls.env,
|
||||
login="other2",
|
||||
name="Another User 2",
|
||||
groups="base.group_user,mail.secret_group",
|
||||
)
|
||||
|
||||
def _test_discuss_channel_access(self, cases, for_sub_channel):
|
||||
"""
|
||||
Executes a list of operations on channels in various setups and checks whether the outcomes
|
||||
match the expected results.
|
||||
|
||||
:param cases: A list of test cases, where each tuple contains:
|
||||
|
||||
- user_key (``"portal"`` | ``"public"`` | ``"user"``): The user performing the operation.
|
||||
- group_key (``"chat"`` | ``"group"`` | ``"no_group"`` | ``"group_matching"`` |
|
||||
``"group_failing"``): The group specification to use. ``chat`` and ``group`` define the
|
||||
channel type, while the others configure group setups for the channels.
|
||||
- membership (``"member"`` | ``"outside"``): Whether the user is a member of the channel.
|
||||
- operation (``"create"`` | ``"read"`` | ``"write"`` | ``"unlink"``): The action being tested.
|
||||
- expected_result (bool): Whether the action is expected to be allowed (``True``) or denied
|
||||
(``False``).
|
||||
:type cases: List[Tuple[str, str, str, str, bool]]
|
||||
:param for_sub_channel: Whether the operation is being tested on a sub-channel. In this case, the
|
||||
``cases`` parameter is used to configure the parent channel.
|
||||
"""
|
||||
for user_key, channel_key, membership, operation, result in cases:
|
||||
if result:
|
||||
try:
|
||||
self._execute_action_channel(
|
||||
user_key, channel_key, membership, operation, result, for_sub_channel
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 - re-raising, just with a more contextual message
|
||||
raise AssertionError(
|
||||
f"{user_key, channel_key, membership, operation} should not raise"
|
||||
) from e
|
||||
else:
|
||||
try:
|
||||
with self.assertRaises(AccessError), mute_logger("odoo.sql_db"), mute_logger(
|
||||
"odoo.addons.base.models.ir_model"
|
||||
), mute_logger("odoo.addons.base.models.ir_rule"), mute_logger(
|
||||
"odoo.models.unlink"
|
||||
):
|
||||
self._execute_action_channel(
|
||||
user_key, channel_key, membership, operation, result, for_sub_channel
|
||||
)
|
||||
except AssertionError as e:
|
||||
raise AssertionError(
|
||||
f"{user_key, channel_key, membership, operation} should raise"
|
||||
) from e
|
||||
|
||||
def test_01_discuss_channel_access(self):
|
||||
cases = [
|
||||
*self._channel_type_channel_access_cases,
|
||||
*self._group_type_channel_access_cases,
|
||||
("public", "chat", "outside", "create", False),
|
||||
("public", "chat", "outside", "read", False),
|
||||
("public", "chat", "outside", "write", False),
|
||||
("public", "chat", "outside", "unlink", False),
|
||||
("portal", "chat", "member", "read", True),
|
||||
("portal", "chat", "member", "write", False),
|
||||
("portal", "chat", "member", "unlink", False),
|
||||
("portal", "chat", "outside", "create", False),
|
||||
("portal", "chat", "outside", "read", False),
|
||||
("portal", "chat", "outside", "write", False),
|
||||
("portal", "chat", "outside", "unlink", False),
|
||||
("user", "chat", "member", "read", True),
|
||||
("user", "chat", "member", "write", True),
|
||||
("user", "chat", "member", "unlink", False),
|
||||
("user", "chat", "outside", "create", True),
|
||||
("user", "chat", "outside", "read", False),
|
||||
("user", "chat", "outside", "write", False),
|
||||
("user", "chat", "outside", "unlink", False),
|
||||
]
|
||||
self._test_discuss_channel_access(cases, for_sub_channel=False)
|
||||
|
||||
def test_02_discuss_sub_channel_access(self):
|
||||
cases = [
|
||||
*self._channel_type_channel_access_cases,
|
||||
*self._group_type_channel_access_cases,
|
||||
]
|
||||
self._test_discuss_channel_access(cases, for_sub_channel=True)
|
||||
|
||||
def _test_discuss_channel_member_access(self, cases, for_sub_channel):
|
||||
"""
|
||||
Executes a list of operations on channel members in various setups and checks whether the
|
||||
outcomes match the expected results.
|
||||
|
||||
:param cases: A list of test cases, where each tuple contains:
|
||||
- user_key (``"portal"`` | ``"public"`` | ``"user"``):
|
||||
The user performing the operation.
|
||||
- group_key (``"chat"`` | ``"group"`` | ``"no_group"`` | ``"group_matching"`` |
|
||||
``"group_failing"``):
|
||||
The group specification to use. ``chat`` and ``group`` define the channel type, while the
|
||||
others configure group setups for the channels.
|
||||
- membership (``"member"`` | ``"outside"``):
|
||||
Whether the user is a member of the channel.
|
||||
- target (``"self"`` | ``"other"``):
|
||||
Whether the operation is executed on the self-member or another member.
|
||||
- operation (``"create"`` | ``"read"`` | ``"write"`` | ``"unlink"``):
|
||||
The action being tested.
|
||||
- expected_result (bool):
|
||||
Whether the action is expected to be allowed (``True``) or denied (``False``).
|
||||
:type cases: List[Tuple[str, str, str, str, str, bool]]
|
||||
:param for_sub_channel: Whether the operation is being tested on a sub-channel. In this case, the
|
||||
``cases`` parameter is used to configure the parent channel's member.
|
||||
"""
|
||||
for user_key, channel_key, membership, target, operation, result in cases:
|
||||
channel_id = self._get_channel_id(user_key, channel_key, membership, for_sub_channel)
|
||||
if result:
|
||||
try:
|
||||
self._execute_action_member(channel_id, user_key, target, operation, result)
|
||||
except Exception as e: # noqa: BLE001 - re-raising, just with a more contextual message
|
||||
raise AssertionError(
|
||||
f"{user_key, channel_key, membership, target, operation} should not raise"
|
||||
) from e
|
||||
else:
|
||||
try:
|
||||
with self.assertRaises(AccessError), mute_logger("odoo.sql_db"), mute_logger(
|
||||
"odoo.addons.base.models.ir_model"
|
||||
), mute_logger("odoo.addons.base.models.ir_rule"), mute_logger(
|
||||
"odoo.models.unlink"
|
||||
):
|
||||
try:
|
||||
self._execute_action_member(
|
||||
channel_id, user_key, target, operation, result
|
||||
)
|
||||
except (UniqueViolation, UserError) as e:
|
||||
raise AccessError("expected errors as access error") from e
|
||||
except AssertionError as e:
|
||||
raise AssertionError(
|
||||
f"{user_key, channel_key, membership, target, operation} should raise access error"
|
||||
) from e
|
||||
|
||||
def test_10_discuss_channel_member_access(self):
|
||||
cases = [
|
||||
*self._channel_type_channel_member_access_cases,
|
||||
*self._group_type_channel_member_access_cases,
|
||||
("public", "chat", "outside", "self", "create", False),
|
||||
("public", "chat", "outside", "other", "create", False),
|
||||
("public", "chat", "outside", "other", "read", False),
|
||||
("public", "chat", "outside", "other", "write", False),
|
||||
("public", "chat", "outside", "other", "unlink", False),
|
||||
("portal", "chat", "member", "self", "create", False),
|
||||
("portal", "chat", "member", "self", "read", True),
|
||||
("portal", "chat", "member", "self", "write", True),
|
||||
("portal", "chat", "member", "self", "unlink", True),
|
||||
("portal", "chat", "member", "other", "create", False),
|
||||
("portal", "chat", "member", "other", "read", True),
|
||||
("portal", "chat", "member", "other", "write", False),
|
||||
("portal", "chat", "member", "other", "unlink", False),
|
||||
("portal", "chat", "outside", "self", "create", False),
|
||||
("portal", "chat", "outside", "other", "create", False),
|
||||
("portal", "chat", "outside", "other", "read", False),
|
||||
("portal", "chat", "outside", "other", "write", False),
|
||||
("portal", "chat", "outside", "other", "unlink", False),
|
||||
("user", "chat", "member", "self", "create", False),
|
||||
("user", "chat", "member", "self", "read", True),
|
||||
("user", "chat", "member", "self", "write", True),
|
||||
("user", "chat", "member", "self", "unlink", True),
|
||||
("user", "chat", "member", "other", "create", False),
|
||||
("user", "chat", "member", "other", "read", True),
|
||||
("user", "chat", "member", "other", "write", False),
|
||||
("user", "chat", "member", "other", "unlink", False),
|
||||
("user", "chat", "outside", "self", "create", False),
|
||||
("user", "chat", "outside", "other", "create", False),
|
||||
("user", "chat", "outside", "other", "read", False),
|
||||
("user", "chat", "outside", "other", "write", False),
|
||||
("user", "chat", "outside", "other", "unlink", False),
|
||||
]
|
||||
self._test_discuss_channel_member_access(cases, for_sub_channel=False)
|
||||
|
||||
def test_11_discuss_sub_channel_member_access(self):
|
||||
cases = [
|
||||
*self._channel_type_channel_member_access_cases,
|
||||
*self._group_type_channel_member_access_cases,
|
||||
]
|
||||
self._test_discuss_channel_member_access(cases, for_sub_channel=True)
|
||||
|
||||
def _get_channel_id(self, user_key, channel_key, membership, sub_channel):
|
||||
user = self.env["res.users"] if user_key == "public" else self.users[user_key]
|
||||
partner = user.partner_id
|
||||
guest = self.guest if user_key == "public" else self.env["mail.guest"]
|
||||
partners = self.other_user.partner_id
|
||||
if membership == "member":
|
||||
partners += partner
|
||||
DiscussChannel = self.env["discuss.channel"].with_user(self.other_user)
|
||||
if channel_key == "group":
|
||||
channel = DiscussChannel._create_group(partners.ids)
|
||||
if membership == "member":
|
||||
channel._add_members(users=user, guests=guest)
|
||||
elif channel_key == "chat":
|
||||
channel = DiscussChannel._get_or_create_chat(partners.ids)
|
||||
else:
|
||||
channel = DiscussChannel._create_channel("Channel", group_id=None)
|
||||
if membership == "member":
|
||||
channel._add_members(users=user, guests=guest)
|
||||
if channel_key == "no_group":
|
||||
channel.group_public_id = None
|
||||
elif channel_key == "group_matching":
|
||||
channel.group_public_id = self.secret_group
|
||||
elif channel_key == "group_failing":
|
||||
channel.group_public_id = self.env.ref("base.group_system")
|
||||
if sub_channel:
|
||||
channel.sudo()._create_sub_channel()
|
||||
channel = channel.sub_channel_ids[0]
|
||||
if membership == "member":
|
||||
channel.sudo()._add_members(users=user, guests=guest)
|
||||
return channel.id
|
||||
|
||||
def _execute_action_channel(self, user_key, channel_key, membership, operation, result, for_sub_channel):
|
||||
current_user = self.users[user_key]
|
||||
guest = self.guest if user_key == "public" else self.env["mail.guest"]
|
||||
ChannelAsUser = self.env["discuss.channel"].with_user(current_user).with_context(guest=guest)
|
||||
if operation == "create":
|
||||
group_public_id = None
|
||||
if channel_key == "group_matching":
|
||||
group_public_id = self.secret_group.id
|
||||
elif channel_key == "group_failing":
|
||||
group_public_id = self.env.ref("base.group_system").id
|
||||
data = {
|
||||
"name": "Test Channel",
|
||||
"channel_type": channel_key if channel_key in ("group", "chat") else "channel",
|
||||
"group_public_id": group_public_id,
|
||||
}
|
||||
ChannelAsUser.create(data)
|
||||
else:
|
||||
channel = ChannelAsUser.browse(
|
||||
self._get_channel_id(user_key, channel_key, membership, for_sub_channel)
|
||||
)
|
||||
self.assertEqual(len(channel), 1, "should find the channel")
|
||||
if operation == "read":
|
||||
self.assertEqual(len(ChannelAsUser.search([("id", "=", channel.id)])), 1 if result else 0)
|
||||
channel.read(["name"])
|
||||
elif operation == "write":
|
||||
channel.write({"name": "new name"})
|
||||
elif operation == "unlink":
|
||||
channel.unlink()
|
||||
|
||||
def _execute_action_member(self, channel_id, user_key, target, operation, result):
|
||||
current_user = self.users[user_key]
|
||||
partner = self.env["res.partner"] if user_key == "public" else current_user.partner_id
|
||||
guest = self.guest if user_key == "public" else self.env["mail.guest"]
|
||||
ChannelMemberAsUser = self.env["discuss.channel.member"].with_user(current_user).with_context(guest=guest)
|
||||
if operation == "create":
|
||||
create_data = {"channel_id": channel_id}
|
||||
if target == "self":
|
||||
if guest:
|
||||
create_data["guest_id"] = guest.id
|
||||
else:
|
||||
create_data["partner_id"] = partner.id
|
||||
else:
|
||||
create_data["partner_id"] = self.other_user_2.partner_id.id
|
||||
ChannelMemberAsUser.create(create_data)
|
||||
else:
|
||||
domain = [("channel_id", "=", channel_id)]
|
||||
if target == "self":
|
||||
if guest:
|
||||
domain.append(("guest_id", "=", guest.id))
|
||||
else:
|
||||
domain.append(("partner_id", "=", partner.id))
|
||||
else:
|
||||
domain.append(("partner_id", "=", self.other_user.partner_id.id))
|
||||
member = ChannelMemberAsUser.sudo().search(domain).sudo(False)
|
||||
self.assertEqual(len(member), 1, "should find the target member")
|
||||
if operation == "read":
|
||||
self.assertEqual(len(ChannelMemberAsUser.search(domain)), 1 if result else 0)
|
||||
member.read(["custom_channel_name"])
|
||||
elif operation == "write":
|
||||
member.write({"custom_channel_name": "new name"})
|
||||
elif operation == "unlink":
|
||||
member.unlink()
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserPortal, HttpCaseWithUserDemo
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install", "is_tour")
|
||||
class TestMailPublicPage(HttpCaseWithUserPortal, HttpCaseWithUserDemo):
|
||||
"""Checks that the invite page redirects to the channel and that all
|
||||
modules load correctly on the welcome and channel page when authenticated as various users"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
portal_user = mail_new_test_user(
|
||||
self.env,
|
||||
name='Portal Bowser',
|
||||
login='portal_bowser',
|
||||
email='portal_bowser@example.com',
|
||||
groups='base.group_portal',
|
||||
)
|
||||
internal_user = mail_new_test_user(
|
||||
self.env,
|
||||
name='Internal Luigi',
|
||||
login='internal_luigi',
|
||||
email='internal_luigi@example.com',
|
||||
groups='base.group_user',
|
||||
)
|
||||
guest = self.env['mail.guest'].create({'name': 'Guest Mario'})
|
||||
|
||||
self.channel = self.env['discuss.channel']._create_channel(group_id=None, name='Test channel')
|
||||
self.channel._add_members(users=portal_user)
|
||||
self.channel._add_members(users=internal_user)
|
||||
self.channel._add_members(guests=guest)
|
||||
internal_member = self.channel.channel_member_ids.filtered(lambda m: internal_user.partner_id == m.partner_id)
|
||||
internal_member._rtc_join_call()
|
||||
|
||||
self.group = self.env['discuss.channel']._create_group(partners_to=(internal_user + portal_user).partner_id.ids, name="Test group")
|
||||
self.group._add_members(guests=guest)
|
||||
self.tour = "discuss_channel_public_tour.js"
|
||||
|
||||
def _open_channel_page_as_user(self, login):
|
||||
self.start_tour(self.channel.invitation_url, self.tour, login=login)
|
||||
# Update the body to a unique value to ensure the second run does not confuse the 2 messages.
|
||||
self.channel._get_last_messages().body = "a-very-unique-body-in-channel"
|
||||
# Second run of the tour as the first call has side effects, like creating user settings or adding members to
|
||||
# the channel, so we need to run it again to test different parts of the code.
|
||||
self.start_tour(self.channel.invitation_url, self.tour, login=login)
|
||||
|
||||
def _open_group_page_as_user(self, login):
|
||||
self.start_tour(self.group.invitation_url, self.tour, login=login)
|
||||
# Update the body to a unique value to ensure the second run does not confuse the 2 messages.
|
||||
self.channel._get_last_messages().body = "a-very-unique-body-in-group"
|
||||
# Second run of the tour as the first call has side effects, like creating user settings or adding members to
|
||||
# the channel, so we need to run it again to test different parts of the code.
|
||||
self.start_tour(self.group.invitation_url, self.tour, login=login)
|
||||
|
||||
def test_discuss_channel_public_page_as_admin(self):
|
||||
self._open_channel_page_as_user('admin')
|
||||
|
||||
def test_mail_group_public_page_as_admin(self):
|
||||
self._open_group_page_as_user('admin')
|
||||
|
||||
def test_discuss_channel_public_page_as_guest(self):
|
||||
self.start_tour(self.channel.invitation_url, "discuss_channel_as_guest_tour.js")
|
||||
guest = self.env['mail.guest'].search([('channel_ids', 'in', self.channel.id)], limit=1, order='id desc')
|
||||
self.start_tour(self.channel.invitation_url, self.tour, cookies={guest._cookie_name: guest._format_auth_cookie()})
|
||||
|
||||
def test_discuss_channel_public_page_call_public(self):
|
||||
self.channel.default_display_mode = 'video_full_screen'
|
||||
self.start_tour(self.channel.invitation_url, "discuss_channel_call_public_tour.js")
|
||||
|
||||
def test_mail_group_public_page_as_guest(self):
|
||||
self.start_tour(self.group.invitation_url, "discuss_channel_as_guest_tour.js")
|
||||
guest = self.env['mail.guest'].search([('channel_ids', 'in', self.channel.id)], limit=1, order='id desc')
|
||||
self.start_tour(self.group.invitation_url, self.tour, cookies={guest._cookie_name: guest._format_auth_cookie()})
|
||||
|
||||
def test_discuss_channel_public_page_as_internal(self):
|
||||
self._open_channel_page_as_user('demo')
|
||||
|
||||
def test_mail_group_public_page_as_internal(self):
|
||||
self._open_group_page_as_user('demo')
|
||||
|
||||
def test_discuss_channel_public_page_as_portal(self):
|
||||
self._open_channel_page_as_user('portal')
|
||||
|
||||
def test_mail_group_public_page_as_portal(self):
|
||||
self._open_group_page_as_user('portal')
|
||||
|
||||
def test_chat_from_token_as_guest(self):
|
||||
self.env['ir.config_parameter'].set_param('mail.chat_from_token', True)
|
||||
self.url_open('/chat/xyz')
|
||||
channel = self.env['discuss.channel'].search([('uuid', '=', 'xyz')])
|
||||
self.assertEqual(len(channel), 1)
|
||||
|
||||
def test_channel_invitation_from_token(self):
|
||||
public_channel = self.env["discuss.channel"]._create_channel(name="Public Channel", group_id=None)
|
||||
internal_channel = self.env["discuss.channel"]._create_channel(name="Internal Channel", group_id=self.env.ref("base.group_user").id)
|
||||
|
||||
public_response = self.url_open(public_channel.invitation_url)
|
||||
self.assertEqual(public_response.status_code, 200)
|
||||
|
||||
internal_response = self.url_open(internal_channel.invitation_url)
|
||||
self.assertEqual(internal_response.status_code, 404)
|
||||
|
||||
def test_sidebar_in_public_page(self):
|
||||
guest = self.env['mail.guest'].create({'name': 'Guest'})
|
||||
channel_1 = self.env["discuss.channel"]._create_channel(name="Channel 1", group_id=None)
|
||||
channel_2 = self.env["discuss.channel"]._create_channel(name="Channel 2", group_id=None)
|
||||
channel_1._add_members(guests=guest)
|
||||
channel_2._add_members(guests=guest)
|
||||
self.start_tour(f"/discuss/channel/{channel_1.id}", "sidebar_in_public_page_tour", cookies={guest._cookie_name: guest._format_auth_cookie()})
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from lxml import html
|
||||
from itertools import product
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import HttpCase, new_test_user, tagged, users
|
||||
from odoo.tools.misc import hash_sign
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestDiscussChannelInvite(HttpCase, MailCommon):
|
||||
def test_01_invite_by_email_flow(self):
|
||||
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
|
||||
john = new_test_user(self.env, "john", groups="base.group_user", email="john@test.com")
|
||||
group_chat = (
|
||||
self.env["discuss.channel"].with_user(bob)._create_group(partners_to=bob.partner_id.ids)
|
||||
)
|
||||
with self.mock_mail_gateway():
|
||||
self.start_tour(
|
||||
f"/odoo/discuss?active_id={group_chat.id}", "discuss.invite_by_email", login="bob"
|
||||
)
|
||||
self.assertIn(john.partner_id, group_chat.channel_member_ids.partner_id)
|
||||
self.assertNoMail(self.env["res.partner"], "john@test.com")
|
||||
self.assertMailMail(
|
||||
self.env["res.partner"],
|
||||
status=None,
|
||||
email_to_all=["unknown_email@test.com"],
|
||||
author=bob.partner_id,
|
||||
email_values={
|
||||
"subject": f"{bob.name} has invited you to a channel",
|
||||
},
|
||||
)
|
||||
mail = self.env["mail.mail"].search(
|
||||
[("model", "=", "discuss.channel"), ("res_id", "=", group_chat.id)]
|
||||
)
|
||||
body_html = html.fromstring(mail.body_html)
|
||||
join_link = body_html.xpath('//a[normalize-space(text())="Join Channel"]')
|
||||
self.assertTrue(join_link)
|
||||
self.assertEqual(
|
||||
join_link[0].get("href"),
|
||||
f"{self.env['ir.config_parameter'].get_base_url()}{group_chat.invitation_url}?email_token={hash_sign(self.env, 'mail.invite_email', 'unknown_email@test.com')}",
|
||||
)
|
||||
|
||||
def test_02_invite_by_email_excludes_member_emails(self):
|
||||
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
|
||||
group_chat = (
|
||||
self.env["discuss.channel"].with_user(bob)._create_group(partners_to=bob.partner_id.ids)
|
||||
)
|
||||
alfred_guest = self.env["mail.guest"].create({"email": "alfred@test.com", "name": "Alfred"})
|
||||
group_chat._add_members(guests=alfred_guest)
|
||||
with self.mock_mail_gateway():
|
||||
group_chat.invite_by_email(["alfred@test.com", "bob@test.com", "other@test.com"])
|
||||
self.assertMailMail(
|
||||
self.env["res.partner"],
|
||||
status=None,
|
||||
email_to_all=["other@test.com"],
|
||||
author=bob.partner_id,
|
||||
)
|
||||
self.assertNoMail(self.env["res.partner"], "bob@test.com")
|
||||
self.assertNoMail(self.env["res.partner"], "alfred@test.com")
|
||||
|
||||
def test_03_only_invite_by_email_on_allowed_channel_types(self):
|
||||
bob = new_test_user(self.env, "bob", groups="base.group_user")
|
||||
john = new_test_user(self.env, "john", groups="base.group_user")
|
||||
chat = (
|
||||
self.env["discuss.channel"]
|
||||
.with_user(bob)
|
||||
._get_or_create_chat(partners_to=john.partner_id.ids)
|
||||
)
|
||||
group_chat = (
|
||||
self.env["discuss.channel"]
|
||||
.with_user(bob)
|
||||
._create_group(partners_to=john.partner_id.ids)
|
||||
)
|
||||
public_channel = self.env["discuss.channel"].create(
|
||||
{"name": "public community", "group_public_id": False}
|
||||
)
|
||||
private_channel = self.env["discuss.channel"].create(
|
||||
{
|
||||
"name": "user restricted channel",
|
||||
"channel_type": "channel",
|
||||
"group_public_id": self.env.ref("base.group_user").id,
|
||||
}
|
||||
)
|
||||
for channel in [chat, private_channel]:
|
||||
with self.assertRaises(UserError) as exc:
|
||||
channel.invite_by_email(["some@email.com"])
|
||||
self.assertEqual(
|
||||
exc.exception.args[0],
|
||||
f"Inviting by email is not allowed for this channel type ({channel.channel_type}).",
|
||||
)
|
||||
with self.mock_mail_gateway():
|
||||
for channel in [group_chat, public_channel]:
|
||||
channel.invite_by_email(["some@email.com"])
|
||||
self.assertMailMail(
|
||||
self.env["res.partner"],
|
||||
status=None,
|
||||
email_to_all=["some@email.com"],
|
||||
email_values={"model": "discuss.channel", "res_id": channel.id},
|
||||
)
|
||||
|
||||
def test_04_guest_email_updated_when_invited_from_email(self):
|
||||
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
|
||||
group_chat = (
|
||||
self.env["discuss.channel"].with_user(bob)._create_group(partners_to=bob.partner_id.ids)
|
||||
)
|
||||
# Guest email is filled at create
|
||||
self.url_open(
|
||||
f"{group_chat.invitation_url}?email_token={hash_sign(self.env, 'mail.invite_email', 'alfred@test.com')}"
|
||||
)
|
||||
self.assertEqual(group_chat.channel_member_ids.guest_id.email, "alfred@test.com")
|
||||
self.assertEqual(group_chat.channel_member_ids.guest_id.name, "alfred@test.com")
|
||||
# Guest email is updated if empty when invited from email
|
||||
guest = self.env["mail.guest"].create({"name": "Alice"})
|
||||
self.assertFalse(guest.email)
|
||||
self.url_open(
|
||||
f"{group_chat.invitation_url}?email_token={hash_sign(self.env, 'mail.invite_email', 'alice@test.com')}",
|
||||
cookies={
|
||||
guest._cookie_name: f"{guest.id}{guest._cookie_separator}{guest.access_token}",
|
||||
},
|
||||
)
|
||||
self.assertEqual(guest.email, "alice@test.com")
|
||||
self.assertEqual(guest.name, "Alice")
|
||||
# Guest email is not overwriten if already filled
|
||||
guest = self.env["mail.guest"].create({"name": "John", "email": "john@test.com"})
|
||||
self.url_open(
|
||||
f"{group_chat.invitation_url}?email_token={hash_sign(self.env, 'mail.invite_email', 'john_other_email@test.com')}",
|
||||
cookies={
|
||||
guest._cookie_name: f"{guest.id}{guest._cookie_separator}{guest.access_token}",
|
||||
},
|
||||
)
|
||||
self.assertEqual(guest.email, "john@test.com")
|
||||
self.assertEqual(guest.name, "John")
|
||||
|
||||
def test_05_search_for_channel_invite_selectable_email(self):
|
||||
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
|
||||
john = new_test_user(self.env, "john", groups="base.group_user", email="john@test.com")
|
||||
alfred_guest = self.env["mail.guest"].create({"email": "alfred@test.com", "name": "Alfred"})
|
||||
chat = (
|
||||
self.env["discuss.channel"]
|
||||
.with_user(bob)
|
||||
._get_or_create_chat(partners_to=john.partner_id.ids)
|
||||
)
|
||||
group_chat = (
|
||||
self.env["discuss.channel"]
|
||||
.with_user(bob)
|
||||
._create_group(partners_to=john.partner_id.ids)
|
||||
)
|
||||
group_chat._add_members(guests=alfred_guest)
|
||||
public_channel = self.env["discuss.channel"].create(
|
||||
{"name": "public community", "group_public_id": False},
|
||||
)
|
||||
public_channel._add_members(guests=alfred_guest)
|
||||
private_channel = self.env["discuss.channel"].create(
|
||||
{
|
||||
"name": "user restricted channel",
|
||||
"channel_type": "channel",
|
||||
"group_public_id": self.env.ref("base.group_user").id,
|
||||
},
|
||||
)
|
||||
cases = [
|
||||
*product(
|
||||
[chat, private_channel, group_chat, public_channel],
|
||||
["foo@bar"],
|
||||
[False],
|
||||
),
|
||||
# Channel types that do not allow inviting by email, not selectable.
|
||||
*product(
|
||||
[chat, private_channel],
|
||||
["bob@odoo.com", "alfred@odoo.com", "jane@odoo.com"],
|
||||
[False],
|
||||
),
|
||||
# Channel types that allow inviting by email, valid email, selectable.
|
||||
*product(
|
||||
[group_chat, public_channel],
|
||||
["bob@odoo.com", "alfred@odoo.com", "jane@odoo.com"],
|
||||
[True],
|
||||
),
|
||||
]
|
||||
for channel, search_term, is_selectable in cases:
|
||||
with self.subTest(
|
||||
f"channel={channel.channel_type}_{channel.display_name}, search_term={search_term}, is_selectable={is_selectable}"
|
||||
):
|
||||
result = self.env["res.partner"].search_for_channel_invite(
|
||||
search_term, channel_id=channel.id
|
||||
)
|
||||
if is_selectable:
|
||||
self.assertEqual(result["selectable_email"], search_term)
|
||||
continue
|
||||
self.assertFalse(result["selectable_email"])
|
||||
|
||||
@users("employee")
|
||||
def test_06_invite_by_email_posts_user_notification(self):
|
||||
group_chat = self.env["discuss.channel"]._create_group(partners_to=self.user_employee.partner_id.ids)
|
||||
with self.mock_mail_gateway():
|
||||
group_chat.invite_by_email(["alfred@test.com"])
|
||||
last_message = group_chat._get_last_messages()
|
||||
self.assertEqual(last_message.message_type, "user_notification")
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.tests.common import new_test_user, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestDiscussChannelMember(MailCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.secret_group = cls.env['res.groups'].create({
|
||||
'name': 'Secret User Group',
|
||||
})
|
||||
cls.env['ir.model.data'].create({
|
||||
'name': 'secret_group',
|
||||
'module': 'mail',
|
||||
'model': cls.secret_group._name,
|
||||
'res_id': cls.secret_group.id,
|
||||
})
|
||||
cls.user_1 = new_test_user(
|
||||
cls.env, login="user_1", name="User 1", groups="base.group_user,mail.secret_group"
|
||||
)
|
||||
cls.user_2 = new_test_user(
|
||||
cls.env, login="user_2", name="User 2", groups="base.group_user,mail.secret_group"
|
||||
)
|
||||
cls.user_3 = new_test_user(
|
||||
cls.env, login="user_3", name="User 3", groups="base.group_user,mail.secret_group"
|
||||
)
|
||||
cls.user_portal = new_test_user(
|
||||
cls.env, login="user_portal", name="User Portal", groups="base.group_portal"
|
||||
)
|
||||
cls.user_public = new_test_user(
|
||||
cls.env, login="user_public", name="User Public", groups="base.group_public"
|
||||
)
|
||||
|
||||
|
||||
cls.group = cls.env['discuss.channel'].create({
|
||||
'name': 'Group',
|
||||
'channel_type': 'group',
|
||||
})
|
||||
cls.group_restricted_channel = cls.env['discuss.channel'].create({
|
||||
'name': 'Group restricted channel',
|
||||
'channel_type': 'channel',
|
||||
'group_public_id': cls.secret_group.id,
|
||||
})
|
||||
cls.public_channel = cls.env['discuss.channel']._create_channel(group_id=None, name='Public channel of user 1')
|
||||
(cls.group | cls.group_restricted_channel | cls.public_channel).channel_member_ids.unlink()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# GROUP
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_group_01(self):
|
||||
"""Test access on group."""
|
||||
res = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertFalse(res)
|
||||
|
||||
# User 1 can join group with SUDO
|
||||
self.group.with_user(self.user_1).sudo()._add_members(users=self.user_1)
|
||||
res = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(res.partner_id, self.user_1.partner_id)
|
||||
|
||||
# User 2 can not join group
|
||||
with self.assertRaises(AccessError):
|
||||
self.group.with_user(self.user_2)._add_members(users=self.user_2)
|
||||
|
||||
# User 2 can not create a `discuss.channel.member` to join the group
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['discuss.channel.member'].with_user(self.user_2).create({
|
||||
'partner_id': self.user_2.partner_id.id,
|
||||
'channel_id': self.group.id,
|
||||
})
|
||||
|
||||
# User 2 can not write on `discuss.channel.member` to join the group
|
||||
channel_member = self.env['discuss.channel.member'].with_user(self.user_2).search([('is_self', '=', True)])[0]
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member.channel_id = self.group.id
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member.write({'channel_id': self.group.id})
|
||||
|
||||
# Even with SUDO, channel_id of channel.member should not be changed.
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member.sudo().channel_id = self.group.id
|
||||
|
||||
# User 2 can not write on the `partner_id` of `discuss.channel.member`
|
||||
# of an other partner to join a group
|
||||
channel_member_1 = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_1.partner_id.id)])
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member_1.with_user(self.user_2).partner_id = self.user_2.partner_id
|
||||
self.assertEqual(channel_member_1.partner_id, self.user_1.partner_id)
|
||||
|
||||
# Even with SUDO, partner_id of channel.member should not be changed.
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member_1.with_user(self.user_2).sudo().partner_id = self.user_2.partner_id
|
||||
|
||||
def test_group_members(self):
|
||||
"""Test invitation in group part 1 (invite using crud methods)."""
|
||||
self.group.with_user(self.user_1).sudo()._add_members(users=self.user_1)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(len(channel_members), 1)
|
||||
|
||||
# User 2 is not in the group, they can not invite user 3
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['discuss.channel.member'].with_user(self.user_2).create({
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
'channel_id': self.group.id,
|
||||
})
|
||||
|
||||
# User 1 is in the group, they can invite other users
|
||||
self.env['discuss.channel.member'].with_user(self.user_1).create({
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
'channel_id': self.group.id,
|
||||
})
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
|
||||
|
||||
# But User 3 can not write on the `discuss.channel.member` of other user
|
||||
channel_member_1 = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_1.partner_id.id)])
|
||||
channel_member_3 = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_portal.partner_id.id)])
|
||||
channel_member_3.with_user(self.user_portal).custom_channel_name = 'Test'
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member_1.with_user(self.user_2).custom_channel_name = 'Blabla'
|
||||
self.assertNotEqual(channel_member_1.custom_channel_name, 'Blabla')
|
||||
|
||||
def test_group_invite(self):
|
||||
"""Test invitation in group part 2 (use `invite` action)."""
|
||||
self.group.with_user(self.user_1).sudo()._add_members(users=self.user_1)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
# User 2 is not in the group, they can not invite user_portal
|
||||
with self.assertRaises(AccessError):
|
||||
self.group.with_user(self.user_2)._add_members(users=self.user_portal)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
# User 1 is in the group, they can invite user_portal
|
||||
self.group.with_user(self.user_1)._add_members(users=self.user_portal)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
|
||||
|
||||
def test_group_leave(self):
|
||||
"""Test kick/leave channel."""
|
||||
self.group.with_user(self.user_1).sudo()._add_members(users=self.user_1)
|
||||
self.group.with_user(self.user_portal).sudo()._add_members(users=self.user_portal)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(len(channel_members), 2)
|
||||
|
||||
# User 2 is not in the group, they can not kick user 1
|
||||
with self.assertRaises(AccessError):
|
||||
channel_members.with_user(self.user_2).unlink()
|
||||
|
||||
# User 3 is in the group, but not admin/owner, they can not kick user 1
|
||||
with self.assertRaises(AccessError):
|
||||
channel_members.with_user(self.user_portal).unlink()
|
||||
|
||||
def test_group_subchannel_join(self):
|
||||
"""Test join subchannel."""
|
||||
self.group.add_members((self.user_1 | self.user_2).partner_id.ids)
|
||||
group_subchannel = self.group.with_user(self.user_1)._create_sub_channel()
|
||||
group_subchannel.with_user(self.user_2).add_members(self.user_2.partner_id.id)
|
||||
self.assertEqual(group_subchannel.channel_member_ids.partner_id, (self.user_1 | self.user_2).partner_id)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# GROUP BASED CHANNELS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_group_restricted_channel(self):
|
||||
"""Test basics on group channel."""
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertFalse(channel_members)
|
||||
|
||||
# user 1 is in the channel, they can join the channel
|
||||
self.group_restricted_channel.with_user(self.user_1)._add_members(users=self.user_1)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
# user 3 is not in the channel, they can not join
|
||||
with self.assertRaises(AccessError):
|
||||
self.group_restricted_channel.with_user(self.user_portal)._add_members(users=self.user_portal)
|
||||
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
with self.assertRaises(AccessError):
|
||||
channel_members.with_user(self.user_portal).partner_id = self.user_portal.partner_id
|
||||
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
self.group_restricted_channel.with_user(self.user_1)._add_members(users=self.user_portal)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
|
||||
|
||||
# but user 2 is in the channel and can be invited by user 1
|
||||
self.group_restricted_channel.with_user(self.user_1)._add_members(users=self.user_2)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id | self.user_portal.partner_id)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PUBLIC CHANNELS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_public_channel(self):
|
||||
""" Test access on public channels """
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.public_channel.id)])
|
||||
self.assertFalse(channel_members)
|
||||
|
||||
self.public_channel.with_user(self.user_1)._add_members(users=self.user_1)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.public_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
self.public_channel.with_user(self.user_2)._add_members(users=self.user_2)
|
||||
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.public_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id)
|
||||
|
||||
self.public_channel.with_user(self.user_portal)._add_members(users=self.user_portal)
|
||||
with self.assertRaises(ValidationError): # public cannot join without having a guest
|
||||
self.public_channel.with_user(self.user_public)._add_members(users=self.user_public)
|
||||
|
||||
def test_channel_member_invite_with_guest(self):
|
||||
guest = self.env['mail.guest'].create({'name': 'Guest'})
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'ToInvite',
|
||||
'active': True,
|
||||
'type': 'contact',
|
||||
'user_ids': self.user_1,
|
||||
})
|
||||
self.public_channel._add_members(guests=guest)
|
||||
data = self.env["res.partner"].search_for_channel_invite(
|
||||
partner.name, channel_id=self.public_channel.id
|
||||
)["store_data"]
|
||||
self.assertEqual(len(data["res.partner"]), 1)
|
||||
self.assertEqual(data["res.partner"][0]["id"], partner.id)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# UNREAD COUNTER TESTS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_unread_counter_with_message_post(self):
|
||||
channel_as_user_1 = self.env['discuss.channel'].with_user(self.user_1)._create_channel(group_id=None, name='Public channel')
|
||||
channel_as_user_1.with_user(self.user_1)._add_members(users=self.user_1)
|
||||
channel_as_user_1.with_user(self.user_1)._add_members(users=self.user_2)
|
||||
channel_1_rel_user_2 = self.env['discuss.channel.member'].search([
|
||||
('channel_id', '=', channel_as_user_1.id),
|
||||
('partner_id', '=', self.user_2.partner_id.id)
|
||||
])
|
||||
self.assertEqual(channel_1_rel_user_2.message_unread_counter, 0, "should not have unread message initially as notification type is ignored")
|
||||
|
||||
channel_as_user_1.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
channel_1_rel_user_2 = self.env['discuss.channel.member'].search([
|
||||
('channel_id', '=', channel_as_user_1.id),
|
||||
('partner_id', '=', self.user_2.partner_id.id)
|
||||
])
|
||||
self.assertEqual(channel_1_rel_user_2.message_unread_counter, 1, "should have 1 unread message after someone else posted a message")
|
||||
|
||||
def test_unread_counter_with_message_post_multi_channel(self):
|
||||
channel_1_as_user_1 = self.env['discuss.channel'].with_user(self.user_1)._create_channel(group_id=None, name='wololo channel')
|
||||
channel_2_as_user_2 = self.env['discuss.channel'].with_user(self.user_2)._create_channel(group_id=None, name='walala channel')
|
||||
channel_1_as_user_1._add_members(users=self.user_2)
|
||||
channel_2_as_user_2._add_members(users=self.user_1)
|
||||
channel_2_as_user_2._add_members(users=self.user_3)
|
||||
channel_1_as_user_1.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
channel_1_as_user_1.message_post(body='Test 2', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
channel_2_as_user_2.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
members = self.env['discuss.channel.member'].search([('channel_id', 'in', (channel_1_as_user_1 + channel_2_as_user_2).ids)], order="id")
|
||||
self.assertEqual(members.mapped('message_unread_counter'), [
|
||||
0, # channel 1 user 1: posted last message
|
||||
0, # channel 2 user 2: posted last message
|
||||
2, # channel 1 user 2: received 2 messages (from message post)
|
||||
1, # channel 2 user 1: received 1 message (from message post)
|
||||
1, # channel 2 user 3: received 1 message (from message post)
|
||||
])
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
try:
|
||||
import websocket as ws
|
||||
except ImportError:
|
||||
ws = None
|
||||
|
||||
from itertools import product
|
||||
|
||||
from odoo.tests import tagged, new_test_user
|
||||
from odoo.addons.bus.tests.common import WebsocketCase
|
||||
from odoo.addons.mail.tests.common import MailCommon, freeze_all_time
|
||||
from odoo.addons.bus.models.bus import channel_with_db, json_dump
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestMailPresence(WebsocketCase, MailCommon):
|
||||
def _receive_presence(self, requested_by, target, has_token=False):
|
||||
self.env["mail.presence"].search([]).unlink()
|
||||
target_user = isinstance(target, self.env.registry["res.users"])
|
||||
if isinstance(requested_by, self.env.registry["res.users"]):
|
||||
session = self.authenticate(requested_by.login, requested_by.login)
|
||||
auth_cookie = f"session_id={session.sid};"
|
||||
else:
|
||||
self.authenticate(None, None)
|
||||
auth_cookie = f"{requested_by._cookie_name}={requested_by._format_auth_cookie()};"
|
||||
websocket = self.websocket_connect(cookie=auth_cookie)
|
||||
target_channel = target.partner_id if target_user else target
|
||||
channel_parts = ["odoo-presence", f"{target_channel._name}_{target_channel.id}"]
|
||||
if has_token:
|
||||
channel_parts.append(target_channel._get_im_status_access_token())
|
||||
self.subscribe(websocket, ["-".join(channel_parts)], self.env["bus.bus"]._bus_last_id())
|
||||
self.env["mail.presence"]._update_presence(target)
|
||||
self.trigger_notification_dispatching([(target_channel, "presence")])
|
||||
notifications = json.loads(websocket.recv())
|
||||
self._close_websockets()
|
||||
bus_record = self.env["bus.bus"].search([("id", "=", int(notifications[0]["id"]))])
|
||||
self.assertEqual(
|
||||
bus_record.channel,
|
||||
json_dump(channel_with_db(self.env.cr.dbname, (target_channel, "presence"))),
|
||||
)
|
||||
self.assertEqual(notifications[0]["message"]["type"], "bus.bus/im_status_updated")
|
||||
self.assertEqual(notifications[0]["message"]["payload"]["im_status"], "online")
|
||||
self.assertEqual(notifications[0]["message"]["payload"]["presence_status"], "online")
|
||||
self.assertEqual(
|
||||
notifications[0]["message"]["payload"]["partner_id" if target_user else "guest_id"],
|
||||
target_channel.id,
|
||||
)
|
||||
|
||||
@freeze_all_time()
|
||||
def test_presence_access(self):
|
||||
internal = new_test_user(self.env, login="internal_user", groups="base.group_user")
|
||||
other_internal = new_test_user(
|
||||
self.env, login="other_internal_user", groups="base.group_user"
|
||||
)
|
||||
portal = new_test_user(self.env, login="portal_user", groups="base.group_portal")
|
||||
other_portal = new_test_user(
|
||||
self.env, login="other_portal_user", groups="base.group_portal"
|
||||
)
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
other_guest = self.env["mail.guest"].create({"name": "Other Guest"})
|
||||
for requested_by, target, has_token, allowed in [
|
||||
*product([internal], [guest, other_internal, portal], [True, False], [True]),
|
||||
*product([guest, portal], [internal, other_guest, other_portal], [False], [False]),
|
||||
*product([guest, portal], [internal, other_guest, other_portal], [True], [True]),
|
||||
]:
|
||||
with self.subTest(
|
||||
f"test presence access, requested_by={requested_by.name}, target={target.name}, has_token={has_token}, allowed={allowed}"
|
||||
):
|
||||
if allowed:
|
||||
self._receive_presence(requested_by, target, has_token=has_token)
|
||||
else:
|
||||
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
|
||||
self._receive_presence(requested_by, target, has_token=has_token)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests.common import HttpCase, new_test_user, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestDiscussMentionSuggestions(HttpCase):
|
||||
def test_mention_suggestions_group_restricted_channel(self):
|
||||
user_admin = self.env.ref("base.user_admin")
|
||||
user_group = self.env.ref("base.group_user")
|
||||
rd_group = self.env["res.groups"].create({"name": "R&D Group"})
|
||||
new_test_user(self.env, login="dev", name="Dev User", group_ids=[user_group.id, rd_group.id])
|
||||
# have a user that is not channel member and not in group -> should not be suggested as mention
|
||||
new_test_user(self.env, login="sales", name="Sales User", groups="base.group_user")
|
||||
consultant_user = new_test_user(self.env, login="consultant", name="Consultant User", groups="base.group_user")
|
||||
rd_channel = self.env['discuss.channel'].with_user(user_admin).create({
|
||||
"name": "R&D Channel",
|
||||
"channel_type": "channel",
|
||||
"group_public_id": rd_group.id,
|
||||
"channel_member_ids": [
|
||||
Command.create({"partner_id": consultant_user.partner_id.id}),
|
||||
Command.create({"partner_id": user_admin.partner_id.id}),
|
||||
],
|
||||
})
|
||||
self.start_tour(
|
||||
f"/odoo/discuss?active_id=discuss.channel_{rd_channel.id}",
|
||||
"discuss_mention_suggestions_group_restricted_channel.js",
|
||||
login="admin",
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from odoo.addons.mail.tests.common_controllers import MailControllerUpdateCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestDiscussMessageUpdateController(MailControllerUpdateCommon):
|
||||
|
||||
def test_message_update_guest_as_owner(self):
|
||||
"""Test only admin user and message author can update the message content in a channel."""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"group_public_id": None, "name": "public channel"}
|
||||
)
|
||||
channel._add_members(guests=self.guest)
|
||||
# sudo: discuss.channel: posting a message as guest in a test is acceptable
|
||||
message = (
|
||||
channel.with_user(self.user_public)
|
||||
.with_context(guest=self.guest)
|
||||
.sudo()
|
||||
.message_post(body=self.message_body, message_type="comment")
|
||||
)
|
||||
self._execute_subtests(
|
||||
message,
|
||||
(
|
||||
(self.guest, True),
|
||||
(self.user_admin, True),
|
||||
(self.user_employee, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_public, False),
|
||||
),
|
||||
)
|
||||
|
||||
def test_message_update_public_channel(self):
|
||||
"""Test only admin user can update the message content of other authors in a channel."""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"group_public_id": None, "name": "public channel"}
|
||||
)
|
||||
message = channel.message_post(
|
||||
body=self.message_body,
|
||||
message_type="comment",
|
||||
)
|
||||
self._execute_subtests(
|
||||
message,
|
||||
(
|
||||
(self.guest, False),
|
||||
(self.user_admin, True),
|
||||
(self.user_employee, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_public, False),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
from odoo.addons.mail.tests.common_controllers import MailControllerReactionCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestMessageReactionController(MailControllerReactionCommon):
|
||||
|
||||
def test_message_reaction_public_channel(self):
|
||||
"""Test access of message reaction for a public channel."""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"group_public_id": None, "name": "public channel"}
|
||||
)
|
||||
message = channel.message_post(body="public message")
|
||||
self._execute_subtests(
|
||||
message,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, True),
|
||||
(self.user_portal, True),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_message_reaction_channel_as_member(self):
|
||||
"""Test access of message reaction for a channel as member."""
|
||||
channel = self.env["discuss.channel"]._create_group(
|
||||
partners_to=(self.user_portal + self.user_employee).partner_id.ids
|
||||
)
|
||||
channel._add_members(guests=self.guest)
|
||||
message = channel.message_post(body="invite message")
|
||||
self._execute_subtests(
|
||||
message,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, True),
|
||||
(self.user_portal, True),
|
||||
(self.user_employee, True),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_message_reaction_channel_as_non_member(self):
|
||||
"""Test access of message reaction for a channel as non-member."""
|
||||
channel = self.env["discuss.channel"]._create_group(partners_to=[])
|
||||
message = channel.message_post(body="private message")
|
||||
self._execute_subtests(
|
||||
message,
|
||||
(
|
||||
(self.user_public, False),
|
||||
(self.guest, False),
|
||||
(self.user_portal, False),
|
||||
(self.user_employee, False),
|
||||
(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail.tests.test_res_role import TestResRole
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestDiscussResRole(TestResRole):
|
||||
def test_only_mention_by_role_when_channel_is_accessible(self):
|
||||
self.authenticate("admin", "admin")
|
||||
role = self.env["res.role"].create({"name": "rd-Discuss"})
|
||||
for idx, test_case in enumerate([
|
||||
# channel_type, channel_grp, user_grp, is_member, mentionned
|
||||
("channel", None, "base.group_user", False, True),
|
||||
("channel", None, "base.group_user", True, True),
|
||||
("channel", "base.group_system", "base.group_user", False, False),
|
||||
("channel", "base.group_system", "base.group_system", True, True),
|
||||
("group", None, "base.group_user", False, False),
|
||||
("group", None, "base.group_user", True, True),
|
||||
]):
|
||||
channel_type, channel_grp, user_grp, is_member, mentionned = test_case
|
||||
with self.subTest(
|
||||
channel_type=channel_type,
|
||||
channel_grp=channel_grp,
|
||||
user_grp=user_grp,
|
||||
is_member=is_member,
|
||||
notified=mentionned,
|
||||
):
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{
|
||||
"name": f"channel_{channel_grp}_{user_grp}_{mentionned}",
|
||||
"channel_type": channel_type,
|
||||
"group_public_id": self.env.ref(channel_grp).id if channel_grp else None,
|
||||
}
|
||||
)
|
||||
user = mail_new_test_user(
|
||||
self.env, login=f"user_{user_grp}_{idx}", role_ids=role.ids, groups=user_grp
|
||||
)
|
||||
if is_member:
|
||||
channel.add_members(partner_ids=user.partner_id.ids)
|
||||
data = self.make_jsonrpc_request(
|
||||
"/mail/message/post",
|
||||
{
|
||||
"thread_model": "discuss.channel",
|
||||
"thread_id": channel.id,
|
||||
"post_data": {
|
||||
"body": "irrelevant",
|
||||
"message_type": "comment",
|
||||
"role_ids": role.ids,
|
||||
"subtype_xmlid": "mail.mt_note",
|
||||
},
|
||||
},
|
||||
)
|
||||
formatted_partner = user.partner_id.id
|
||||
message = next(filter(lambda m: m["id"] == data["message_id"], data["store_data"]["mail.message"]))
|
||||
if mentionned:
|
||||
self.assertIn(formatted_partner, message["partner_ids"])
|
||||
else:
|
||||
self.assertNotIn(formatted_partner, message["partner_ids"])
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.tests.common import HttpCase, new_test_user, tagged
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestDiscussSubChannels(HttpCase):
|
||||
def test_01_gc_unpin_outdated_sub_channels(self):
|
||||
bob = new_test_user(self.env, "bob_user", groups="base.group_user")
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
parent._create_sub_channel()
|
||||
sub_channel = parent.sub_channel_ids[0]
|
||||
sub_channel._add_members(users=self.env.user)
|
||||
sub_channel.channel_pin(pinned=True)
|
||||
self_member = sub_channel.channel_member_ids.filtered(lambda m: m.is_self)
|
||||
self.assertTrue(self_member.is_pinned)
|
||||
# Last interrest of the member is older than 2 days, no activity on the
|
||||
# channel: should be unpinned.
|
||||
two_days_later_dt = datetime.now() + timedelta(days=3)
|
||||
with freeze_time(two_days_later_dt) as frozen_time:
|
||||
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
|
||||
self.assertFalse(self_member.is_pinned)
|
||||
unpin_dt = self_member.unpin_dt
|
||||
# The member isn't unpinned again when GC re-runs.
|
||||
frozen_time.tick(delta=timedelta(days=1))
|
||||
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
|
||||
self.assertEqual(self_member.unpin_dt, unpin_dt)
|
||||
sub_channel.channel_pin(pinned=True)
|
||||
with freeze_time(two_days_later_dt) as frozen_time:
|
||||
# Last interrest older than 2 days, activity on the channel: should be kept.
|
||||
message = sub_channel.with_user(bob).message_post(body="Hey!", message_type="comment")
|
||||
self_member._mark_as_read(message.id)
|
||||
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
|
||||
self.assertTrue(self_member.is_pinned)
|
||||
# Unread messages: should be kept regardless of last interrest.
|
||||
message = sub_channel.with_user(bob).message_post(body="Another message!", message_type="comment")
|
||||
frozen_time.tick(delta=timedelta(days=3))
|
||||
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
|
||||
self.assertTrue(self_member.is_pinned)
|
||||
self_member._mark_as_read(message.id)
|
||||
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
|
||||
self.assertFalse(self_member.is_pinned)
|
||||
# Ensure regular channels are not impacted.
|
||||
channel = self.env["discuss.channel"].create({"name": "General"})
|
||||
channel.channel_pin(pinned=True)
|
||||
with freeze_time(two_days_later_dt):
|
||||
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
|
||||
self.assertTrue(channel.channel_member_ids.filtered("is_self").is_pinned)
|
||||
|
||||
def test_02_sub_channel_members_sync_with_parent(self):
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
parent.action_unfollow()
|
||||
self.assertFalse(any(m.is_self for m in parent.channel_member_ids))
|
||||
parent._create_sub_channel()
|
||||
sub_channel = parent.sub_channel_ids[0]
|
||||
# Member created for sub channel (_create_sub_channel): should also be
|
||||
# created for the parent channel.
|
||||
self.assertTrue(any(m.is_self for m in parent.channel_member_ids))
|
||||
self.assertTrue(any(m.is_self for m in sub_channel.channel_member_ids))
|
||||
# Member removed from parent channel: should also be removed from the sub
|
||||
# channel.
|
||||
parent.action_unfollow()
|
||||
self.assertFalse(any(m.is_self for m in parent.channel_member_ids))
|
||||
self.assertFalse(any(m.is_self for m in sub_channel.channel_member_ids))
|
||||
# Member created for sub channel (add_members): should also be created
|
||||
# for parent.
|
||||
sub_channel._add_members(users=self.env.user)
|
||||
self.assertTrue(any(m.is_self for m in parent.channel_member_ids))
|
||||
self.assertTrue(any(m.is_self for m in sub_channel.channel_member_ids))
|
||||
|
||||
def test_03_cannot_create_recursive_sub_channel(self):
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
parent._create_sub_channel()
|
||||
sub_channel = parent.sub_channel_ids[0]
|
||||
with self.assertRaises(ValidationError):
|
||||
sub_channel._create_sub_channel()
|
||||
|
||||
def test_04_sub_channel_panel_search(self):
|
||||
bob_user = new_test_user(self.env, "bob_user", groups="base.group_user")
|
||||
self.authenticate("bob_user", "bob_user")
|
||||
channel = self.env["discuss.channel"]._create_channel(name="General", group_id=None)
|
||||
channel._add_members(users=bob_user)
|
||||
for i in range(100):
|
||||
channel._create_sub_channel(name=f"Sub Channel {i}")
|
||||
self.start_tour(
|
||||
f"/odoo/discuss?active_id=discuss.channel_{channel.id}",
|
||||
"test_discuss_sub_channel_search",
|
||||
login="bob_user",
|
||||
)
|
||||
|
||||
def test_05_cannot_upate_first_message_nor_parent_channel(self):
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
parent.message_post(body="Hello there!")
|
||||
parent._create_sub_channel(from_message_id=parent.message_ids[0].id)
|
||||
sub_channel = parent.sub_channel_ids[0]
|
||||
random_channel = self.env["discuss.channel"].create({"name": "Random"})
|
||||
parent.message_post(body="Random message")
|
||||
with self.assertRaises(UserError, msg="Cannot change initial message nor parent channel of: Hello there!."):
|
||||
sub_channel.parent_channel_id = random_channel
|
||||
with self.assertRaises(UserError, msg="Cannot change initial message nor parent channel of: Hello there!."):
|
||||
sub_channel.from_message_id = parent.message_ids[0]
|
||||
|
||||
def test_06_initial_message_must_belong_to_parent_channel(self):
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
random_channel = self.env["discuss.channel"].create({"name": "Random"})
|
||||
random_channel.message_post(body="Hello world!")
|
||||
with self.assertRaises(
|
||||
ValidationError,
|
||||
msg="Cannot create Hello world!: initial message should belong to parent channel.",
|
||||
):
|
||||
parent._create_sub_channel(from_message_id=random_channel.message_ids[0].id)
|
||||
|
||||
def test_07_unlink_sub_channel(self):
|
||||
bob_user = new_test_user(self.env, "bob_user", groups="base.group_user")
|
||||
baz_user = new_test_user(self.env, "baz_user", groups="base.group_user")
|
||||
parent_1 = self.env["discuss.channel"].with_user(bob_user).create({"name": "Parent 1"})
|
||||
parent_1_baz_member = parent_1._add_members(users=baz_user)
|
||||
parent_1_sub_channel_1 = parent_1._create_sub_channel(name="Parent 1 Sub 1")
|
||||
parent_1_sub_channel_1._add_members(users=baz_user)
|
||||
parent_1_sub_channel_2 = parent_1._create_sub_channel(name="Parent 1 Sub 2")
|
||||
parent_1_sub_channel_2._add_members(users=baz_user)
|
||||
parent_2 = self.env["discuss.channel"].with_user(baz_user).create({"name": "Parent 2"})
|
||||
parent_2_bob_member = parent_2._add_members(users=bob_user)
|
||||
parent_2_sub_channel = parent_2._create_sub_channel(name="Parent 2 Sub")
|
||||
parent_2_sub_channel._add_members(users=bob_user)
|
||||
parent_3 = self.env["discuss.channel"].with_user(bob_user).create({"name": "Parent 3"})
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
parent_3_guest_member = parent_3._add_members(guests=guest)
|
||||
parent_3_sub_channel = parent_3._create_sub_channel(name="Parent 3 Sub")
|
||||
parent_3_sub_channel._add_members(guests=guest)
|
||||
members_to_unlink = parent_1_baz_member + parent_2_bob_member + parent_3_guest_member
|
||||
members_to_unlink.sudo().unlink()
|
||||
self.assertNotIn(
|
||||
baz_user.partner_id,
|
||||
parent_1.channel_member_ids.partner_id
|
||||
| parent_1.sub_channel_ids.channel_member_ids.partner_id,
|
||||
)
|
||||
self.assertNotIn(
|
||||
bob_user.partner_id,
|
||||
parent_2.channel_member_ids.partner_id
|
||||
| parent_2.sub_channel_ids.channel_member_ids.partner_id,
|
||||
)
|
||||
self.assertNotIn(
|
||||
guest,
|
||||
parent_3.channel_member_ids.guest_id
|
||||
| parent_3.sub_channel_ids.channel_member_ids.guest_id,
|
||||
)
|
||||
self.assertIn(bob_user.partner_id, parent_1_sub_channel_1.channel_member_ids.partner_id)
|
||||
self.assertIn(bob_user.partner_id, parent_1_sub_channel_2.channel_member_ids.partner_id)
|
||||
self.assertIn(baz_user.partner_id, parent_2_sub_channel.channel_member_ids.partner_id)
|
||||
self.assertIn(bob_user.partner_id, parent_3_sub_channel.channel_member_ids.partner_id)
|
||||
|
||||
def test_08_group_public_id_synced_with_parent(self):
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
parent._create_sub_channel()
|
||||
sub_channel = parent.sub_channel_ids[0]
|
||||
self.assertEqual(parent.group_public_id, self.env.ref("base.group_user"))
|
||||
self.assertEqual(sub_channel.group_public_id, parent.group_public_id)
|
||||
parent.group_public_id = self.env.ref("base.group_system")
|
||||
self.assertEqual(parent.group_public_id, self.env.ref("base.group_system"))
|
||||
self.assertEqual(sub_channel.group_public_id, parent.group_public_id)
|
||||
parent.group_public_id = None
|
||||
self.assertEqual(parent.group_public_id, self.env["res.groups"])
|
||||
self.assertEqual(sub_channel.group_public_id, parent.group_public_id)
|
||||
|
||||
def test_09_cannot_change_group_public_id_of_sub_channel(self):
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
parent._create_sub_channel()
|
||||
sub_channel = parent.sub_channel_ids[0]
|
||||
with self.assertRaises(UserError):
|
||||
sub_channel.group_public_id = self.env.ref("base.group_system")
|
||||
|
||||
def test_10_sub_channel_message_author_member(self):
|
||||
bob_user = new_test_user(self.env, "bob_user", groups="base.group_user")
|
||||
parent = self.env["discuss.channel"].create({
|
||||
"name": "General",
|
||||
"channel_member_ids": [Command.create({"partner_id": bob_user.partner_id.id})],
|
||||
})
|
||||
message = parent.with_user(bob_user).message_post(body="Hello there!")
|
||||
sub_channel = parent._create_sub_channel(from_message_id=message.id)
|
||||
self.assertIn(bob_user.partner_id, sub_channel.channel_member_ids.partner_id)
|
||||
self.assertEqual(len(sub_channel.channel_member_ids), 2)
|
||||
|
||||
def test_11_sub_channel_fallback_name_on_empty_message(self):
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
message = parent.message_post(body="Hello there!", message_type="comment")
|
||||
parent._message_update_content(message, body="")
|
||||
sub_channel = parent._create_sub_channel(from_message_id=message.id)
|
||||
self.assertEqual(sub_channel.name, "This message has been removed")
|
||||
|
||||
def test_12_unlink_children_members_only_once(self):
|
||||
parent = self.env["discuss.channel"].create({"name": "General"})
|
||||
child = parent._create_sub_channel()
|
||||
|
||||
og_unlink = self.env.registry["discuss.channel.member"].unlink
|
||||
unlinked_member_ids = []
|
||||
expected_unlinked_member_ids = sorted((parent.self_member_id | child.self_member_id).ids)
|
||||
|
||||
def _patched_unlink(records):
|
||||
unlinked_member_ids.extend(records.ids)
|
||||
og_unlink(records)
|
||||
|
||||
with patch.object(self.env.registry["discuss.channel.member"], "unlink", _patched_unlink):
|
||||
(parent | child).channel_member_ids.unlink()
|
||||
self.assertEqual(expected_unlinked_member_ids, sorted(unlinked_member_ids))
|
||||
|
||||
def test_13_mentioned_user_becomes_sub_channel_member(self):
|
||||
alice_user = new_test_user(self.env, "alice_user", groups="base.group_user")
|
||||
bob_user = new_test_user(self.env, "bob_user", groups="base.group_user")
|
||||
parent = self.env["discuss.channel"].create({
|
||||
"name": "General",
|
||||
"channel_member_ids": [
|
||||
Command.create({"partner_id": alice_user.partner_id.id}),
|
||||
Command.create({"partner_id": bob_user.partner_id.id}),
|
||||
],
|
||||
})
|
||||
message = parent.with_user(bob_user).message_post(body="Hello there!")
|
||||
sub_channel = parent._create_sub_channel(from_message_id=message.id)
|
||||
self.assertNotIn(alice_user.partner_id, sub_channel.channel_member_ids.partner_id)
|
||||
sub_channel.with_user(bob_user).message_post(
|
||||
body="Check this out @Alice",
|
||||
partner_ids=[alice_user.partner_id.id],
|
||||
)
|
||||
self.assertIn(alice_user.partner_id, sub_channel.channel_member_ids.partner_id)
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
from odoo.addons.mail.tests.common_controllers import MailControllerThreadCommon, MessagePostSubTestData
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestDiscussThreadController(MailControllerThreadCommon):
|
||||
|
||||
def test_internal_channel_message_post_access(self):
|
||||
"""Test access of message_post on internal channel."""
|
||||
channel = self.env["discuss.channel"].create({"name": "Internal Channel"})
|
||||
|
||||
def test_access(user, allowed):
|
||||
return MessagePostSubTestData(user, allowed)
|
||||
|
||||
self._execute_message_post_subtests(
|
||||
channel,
|
||||
(
|
||||
test_access(self.user_public, False),
|
||||
test_access(self.guest, False),
|
||||
test_access(self.user_portal, False),
|
||||
test_access(self.user_employee, True),
|
||||
test_access(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_public_channel_message_post_access(self):
|
||||
"""Test access of message_post on public channel."""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"name": "Public Channel", "group_public_id": None}
|
||||
)
|
||||
|
||||
def test_access(user, allowed, exp_author=None):
|
||||
return MessagePostSubTestData(user, allowed, exp_author=exp_author)
|
||||
|
||||
self._execute_message_post_subtests(
|
||||
channel,
|
||||
(
|
||||
test_access(self.user_public, True),
|
||||
test_access(self.guest, True),
|
||||
test_access(self.user_portal, True),
|
||||
test_access(self.user_employee, True),
|
||||
test_access(self.user_admin, True),
|
||||
),
|
||||
)
|
||||
|
||||
def test_public_channel_message_post_partner_ids(self):
|
||||
"""Test partner_ids of message_post on public channel.
|
||||
Non-internal users cannot use mentions without mention_token."""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"name": "Public Channel", "group_public_id": None}
|
||||
)
|
||||
channel._add_members(users=self.user_employee_nopartner)
|
||||
partners = (
|
||||
self.user_portal + self.user_employee + self.user_employee_nopartner + self.user_admin
|
||||
).partner_id
|
||||
|
||||
def test_partners(user, allowed, exp_partners):
|
||||
return MessagePostSubTestData(
|
||||
user, allowed, partners=partners, exp_partners=exp_partners
|
||||
)
|
||||
|
||||
self._execute_message_post_subtests(
|
||||
channel,
|
||||
(
|
||||
test_partners(self.user_public, True, self.env["res.partner"]),
|
||||
test_partners(self.guest, True, self.env["res.partner"]),
|
||||
test_partners(self.user_portal, True, self.env["res.partner"]),
|
||||
test_partners(self.user_employee, True, partners),
|
||||
test_partners(self.user_employee_nopartner, True, partners),
|
||||
test_partners(self.user_admin, True, partners),
|
||||
),
|
||||
)
|
||||
|
||||
def test_public_channel_message_post_partner_emails(self):
|
||||
"""Test partner_emails of message_post on public channel can only be
|
||||
used by users of base.group_partner_manager."""
|
||||
channel = self.env["discuss.channel"].create(
|
||||
{"name": "Public Channel", "group_public_id": None}
|
||||
)
|
||||
no_emails = []
|
||||
existing_emails = [self.user_employee.email]
|
||||
partner_emails = [self.user_employee.email, "test@example.com"]
|
||||
|
||||
def test_emails(user, allowed, exp_emails, exp_author=None):
|
||||
return MessagePostSubTestData(
|
||||
user,
|
||||
allowed,
|
||||
partner_emails=partner_emails,
|
||||
exp_author=exp_author,
|
||||
exp_emails=exp_emails,
|
||||
)
|
||||
|
||||
self._execute_message_post_subtests(
|
||||
channel,
|
||||
(
|
||||
test_emails(self.user_public, True, no_emails),
|
||||
test_emails(self.guest, True, no_emails),
|
||||
test_emails(self.user_portal, True, no_emails),
|
||||
# restricted because not base.group_partner_manager: find existing only
|
||||
test_emails(self.user_employee_nopartner, True, existing_emails),
|
||||
test_emails(self.user_employee, True, partner_emails),
|
||||
test_emails(self.user_admin, True, partner_emails),
|
||||
),
|
||||
)
|
||||
47
odoo-bringout-oca-ocb-mail/mail/tests/discuss/test_guest.py
Normal file
47
odoo-bringout-oca-ocb-mail/mail/tests/discuss/test_guest.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from odoo import fields
|
||||
from odoo.addons.mail.tests.common import MailCase
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestGuest(MailCase):
|
||||
|
||||
def test_updating_guest_name_linked_to_multiple_channels(self):
|
||||
"""This test ensures that when a guest is linked to multiple channels,
|
||||
the guest's name is updated correctly and the appropriate bus notifications are sent.
|
||||
"""
|
||||
guest = self.env['mail.guest'].create({'name': 'Guest'})
|
||||
channel_1 = self.env["discuss.channel"]._create_channel(name="Channel 1", group_id=None)
|
||||
channel_2 = self.env["discuss.channel"]._create_channel(name="Channel 2", group_id=None)
|
||||
channel_1._add_members(guests=guest)
|
||||
channel_2._add_members(guests=guest)
|
||||
|
||||
def get_guest_bus_params():
|
||||
guest_write_date = fields.Datetime.to_string(guest.write_date)
|
||||
message = {
|
||||
"type": "mail.record/insert",
|
||||
"payload": {
|
||||
"mail.guest": [
|
||||
{
|
||||
"avatar_128_access_token": guest._get_avatar_128_access_token(),
|
||||
"id": guest.id,
|
||||
"name": "Guest Name Updated",
|
||||
"write_date": guest_write_date,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
[
|
||||
(self.cr.dbname, "discuss.channel", channel_1.id),
|
||||
(self.cr.dbname, "discuss.channel", channel_2.id),
|
||||
(self.cr.dbname, "mail.guest", guest.id),
|
||||
],
|
||||
[message, message, message],
|
||||
)
|
||||
|
||||
self._reset_bus()
|
||||
with self.assertBus(get_params=get_guest_bus_params):
|
||||
guest._update_name("Guest Name Updated")
|
||||
self.assertEqual(guest.name, "Guest Name Updated")
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
from odoo.tests import tagged
|
||||
from odoo.addons.bus.tests.common import WebsocketCase
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestGuestFeature(WebsocketCase, MailCommon):
|
||||
def test_mark_as_read_as_guest(self):
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
partner = self.env["res.partner"].create({"name": "John"})
|
||||
channel = self.env["discuss.channel"]._create_channel(
|
||||
group_id=None, name="General"
|
||||
)
|
||||
channel._add_members(guests=guest, partners=partner)
|
||||
channel.message_post(
|
||||
body="Hello World!", message_type="comment", subtype_xmlid="mail.mt_comment"
|
||||
)
|
||||
guest_member = channel.channel_member_ids.filtered(
|
||||
lambda m: m.guest_id == guest
|
||||
)
|
||||
self.assertEqual(guest_member.seen_message_id, self.env["mail.message"])
|
||||
self.make_jsonrpc_request(
|
||||
"/discuss/channel/mark_as_read",
|
||||
{
|
||||
"channel_id": channel.id,
|
||||
"last_message_id": channel.message_ids[0].id,
|
||||
},
|
||||
cookies={guest._cookie_name: guest._format_auth_cookie()}
|
||||
)
|
||||
self.assertEqual(guest_member.seen_message_id, channel.message_ids[0])
|
||||
|
||||
def test_subscribe_to_guest_channel(self):
|
||||
self._reset_bus()
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
guest_websocket = self.websocket_connect()
|
||||
self.subscribe(guest_websocket, [f"mail.guest_{guest._format_auth_cookie()}"], guest.id)
|
||||
guest._bus_send("lambda", {"foo": "bar"})
|
||||
self.trigger_notification_dispatching([guest])
|
||||
notifications = json.loads(guest_websocket.recv())
|
||||
self.assertEqual(1, len(notifications))
|
||||
self.assertEqual(notifications[0]["message"]["type"], "lambda")
|
||||
self.assertEqual(notifications[0]["message"]["payload"], {"foo": "bar"})
|
||||
|
||||
def test_subscribe_to_discuss_channel(self):
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
channel = self.env["discuss.channel"]._create_channel(
|
||||
group_id=None, name="General"
|
||||
)
|
||||
channel._add_members(guests=guest)
|
||||
self._reset_bus()
|
||||
guest_websocket = self.websocket_connect()
|
||||
self.subscribe(guest_websocket, [f"mail.guest_{guest._format_auth_cookie()}"], guest.id)
|
||||
channel._bus_send("lambda", {"foo": "bar"})
|
||||
self.trigger_notification_dispatching([channel])
|
||||
notifications = json.loads(guest_websocket.recv())
|
||||
self.assertEqual(1, len(notifications))
|
||||
self.assertEqual(notifications[0]["message"]["type"], "lambda")
|
||||
self.assertEqual(notifications[0]["message"]["payload"], {"foo": "bar"})
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
import odoo.tests
|
||||
from odoo import Command
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestLoadMessages(HttpCaseWithUserDemo):
|
||||
def test_01_mail_message_load_order_tour(self):
|
||||
partner_admin = self.env.ref('base.partner_admin')
|
||||
channel_id = self.env["discuss.channel"].create({
|
||||
"name": "MyTestChannel",
|
||||
"channel_member_ids": [Command.create({"partner_id": partner_admin.id})],
|
||||
})
|
||||
self.env["mail.message"].create([{
|
||||
"body": str(n),
|
||||
"model": "discuss.channel",
|
||||
"pinned_at": odoo.fields.Datetime.now() if n == 1 else None,
|
||||
"res_id": channel_id.id,
|
||||
"author_id": partner_admin.id,
|
||||
"message_type": "comment",
|
||||
} for n in range(1, 61)])
|
||||
channel_id.channel_member_ids.filtered(
|
||||
lambda m: m.partner_id == partner_admin
|
||||
)._mark_as_read(channel_id.message_ids[0].id)
|
||||
self.start_tour("/odoo/action-mail.action_discuss", "mail_message_load_order_tour", login="admin")
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
import odoo
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.addons.base.tests.common import HttpCase, HttpCaseWithUserDemo
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.http import STATIC_CACHE_LONG
|
||||
from odoo import Command, fields
|
||||
|
||||
|
||||
@odoo.tests.tagged("-at_install", "post_install", "mail_controller")
|
||||
class TestMessageController(HttpCaseWithUserDemo):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.channel = cls.env["discuss.channel"].create(
|
||||
{
|
||||
"group_public_id": None,
|
||||
"name": "Test channel",
|
||||
}
|
||||
)
|
||||
cls.public_user = cls.env.ref("base.public_user")
|
||||
cls.attachments = (
|
||||
cls.env["ir.attachment"]
|
||||
.with_user(cls.public_user)
|
||||
.sudo()
|
||||
.create(
|
||||
[
|
||||
{
|
||||
"name": "File 1",
|
||||
"res_id": 0,
|
||||
"res_model": "mail.compose.message",
|
||||
},
|
||||
{
|
||||
"name": "File 2",
|
||||
"res_id": 0,
|
||||
"res_model": "mail.compose.message",
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
cls.guest = cls.env["mail.guest"].create({"name": "Guest"})
|
||||
cls.channel._add_members(guests=cls.guest)
|
||||
|
||||
@mute_logger("odoo.addons.http_routing.models.ir_http", "odoo.http")
|
||||
def test_channel_message_attachments(self):
|
||||
self.authenticate(None, None)
|
||||
self.opener.cookies[self.guest._cookie_name] = self.guest._format_auth_cookie()
|
||||
# test message post: token error
|
||||
res1 = self.url_open(
|
||||
url="/mail/message/post",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": "discuss.channel",
|
||||
"thread_id": self.channel.id,
|
||||
"post_data": {
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[0].id],
|
||||
"attachment_tokens": ["wrong token"],
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res1.status_code, 200)
|
||||
self.assertIn(
|
||||
"One or more attachments do not exist, or you do not have the rights to access them.",
|
||||
res1.text,
|
||||
"guest should not be allowed to add attachment without token when posting message",
|
||||
)
|
||||
# test message post: token ok
|
||||
res2 = self.url_open(
|
||||
url="/mail/message/post",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": "discuss.channel",
|
||||
"thread_id": self.channel.id,
|
||||
"post_data": {
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[0].id],
|
||||
"attachment_tokens": [self.attachments[0]._get_ownership_token()],
|
||||
"message_type": "comment",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res2.status_code, 200)
|
||||
data1 = res2.json()["result"]
|
||||
self.assertEqual(
|
||||
data1["store_data"]["ir.attachment"],
|
||||
[
|
||||
{
|
||||
"checksum": False,
|
||||
"create_date": fields.Datetime.to_string(self.attachments[0].create_date),
|
||||
"file_size": 0,
|
||||
"has_thumbnail": False,
|
||||
"id": self.attachments[0].id,
|
||||
"mimetype": "application/octet-stream",
|
||||
"name": "File 1",
|
||||
"ownership_token": self.attachments[0]._get_ownership_token(),
|
||||
"raw_access_token": self.attachments[0]._get_raw_access_token(),
|
||||
"res_name": "Test channel",
|
||||
"res_model": self.attachments[0].res_model,
|
||||
"thread": {"id": self.channel.id, "model": "discuss.channel"},
|
||||
"thumbnail_access_token": self.attachments[0]._get_thumbnail_token(),
|
||||
"voice_ids": [],
|
||||
'type': 'binary',
|
||||
'url': False,
|
||||
},
|
||||
],
|
||||
"guest should be allowed to add attachment with token when posting message",
|
||||
)
|
||||
# test message update: token error
|
||||
res3 = self.url_open(
|
||||
url="/mail/message/update_content",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"message_id": data1["message_id"],
|
||||
"update_data": {
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[1].id],
|
||||
"attachment_tokens": ["wrong token"],
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res3.status_code, 200)
|
||||
self.assertIn(
|
||||
"One or more attachments do not exist, or you do not have the rights to access them.",
|
||||
res3.text,
|
||||
"guest should not be allowed to add attachment without token when updating message",
|
||||
)
|
||||
# test message update: token ok
|
||||
res4 = self.url_open(
|
||||
url="/mail/message/update_content",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"message_id": data1["message_id"],
|
||||
"update_data": {
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[1].id],
|
||||
"attachment_tokens": [self.attachments[1]._get_ownership_token()],
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res4.status_code, 200)
|
||||
data2 = res4.json()["result"]
|
||||
self.assertEqual(
|
||||
data2["ir.attachment"],
|
||||
[
|
||||
{
|
||||
"checksum": False,
|
||||
"create_date": fields.Datetime.to_string(self.attachments[0].create_date),
|
||||
"file_size": 0,
|
||||
"has_thumbnail": False,
|
||||
"id": self.attachments[0].id,
|
||||
"mimetype": "application/octet-stream",
|
||||
"name": "File 1",
|
||||
"ownership_token": self.attachments[0]._get_ownership_token(),
|
||||
"raw_access_token": self.attachments[0]._get_raw_access_token(),
|
||||
"res_name": "Test channel",
|
||||
"res_model": self.attachments[0].res_model,
|
||||
"thread": {"id": self.channel.id, "model": "discuss.channel"},
|
||||
"thumbnail_access_token": self.attachments[0]._get_thumbnail_token(),
|
||||
"voice_ids": [],
|
||||
'type': 'binary',
|
||||
'url': False,
|
||||
},
|
||||
{
|
||||
"checksum": False,
|
||||
"create_date": fields.Datetime.to_string(self.attachments[1].create_date),
|
||||
"file_size": 0,
|
||||
"has_thumbnail": False,
|
||||
"id": self.attachments[1].id,
|
||||
"mimetype": "application/octet-stream",
|
||||
"name": "File 2",
|
||||
"ownership_token": self.attachments[1]._get_ownership_token(),
|
||||
"raw_access_token": self.attachments[1]._get_raw_access_token(),
|
||||
"res_name": "Test channel",
|
||||
"res_model": self.attachments[1].res_model,
|
||||
"thread": {"id": self.channel.id, "model": "discuss.channel"},
|
||||
"thumbnail_access_token": self.attachments[1]._get_thumbnail_token(),
|
||||
"voice_ids": [],
|
||||
'type': 'binary',
|
||||
'url': False,
|
||||
},
|
||||
],
|
||||
"guest should be allowed to add attachment with token when updating message",
|
||||
)
|
||||
|
||||
@mute_logger("odoo.addons.http_routing.models.ir_http", "odoo.http")
|
||||
def test_mail_partner_from_email_unauthenticated(self):
|
||||
self.authenticate(None, None)
|
||||
self.opener.cookies[self.guest._cookie_name] = self.guest._format_auth_cookie()
|
||||
res1 = self.url_open(
|
||||
url="/mail/partner/from_email",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": "discuss.channel",
|
||||
"thread_id": self.channel.id,
|
||||
"emails": ["john@test.be"],
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res1.status_code, 200)
|
||||
self.assertEqual(
|
||||
0,
|
||||
self.env["res.partner"].search_count([('email', '=', "john@test.be")]),
|
||||
"guest should not be allowed to create a partner from an email",
|
||||
)
|
||||
|
||||
self.authenticate(None, None)
|
||||
self.opener.cookies[self.guest._cookie_name] = self.guest._format_auth_cookie()
|
||||
res1 = self.url_open(
|
||||
url="/mail/partner/from_email",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": "discuss.channel",
|
||||
"thread_id": self.channel.id,
|
||||
"emails": ["john@test.be"],
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res1.status_code, 200)
|
||||
self.assertEqual(
|
||||
0,
|
||||
self.env["res.partner"].search_count([('email', '=', "john@test.be")]),
|
||||
"guest should not be allowed to create a partner from an email",
|
||||
)
|
||||
res2 = self.url_open(
|
||||
url="/mail/message/post",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": "discuss.channel",
|
||||
"thread_id": self.channel.id,
|
||||
"post_data": {
|
||||
"body": "test",
|
||||
"partner_emails": ["john@test.be"],
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res2.status_code, 200)
|
||||
self.assertEqual(
|
||||
0,
|
||||
self.env["res.partner"].search_count([('email', '=', "john@test.be")]),
|
||||
"guest should not be allowed to create a partner from an email from message_post",
|
||||
)
|
||||
|
||||
def test_mail_cache_control_header(self):
|
||||
testuser = self.env['res.users'].create({
|
||||
'email': 'testuser@testuser.com',
|
||||
'group_ids': [Command.set([self.ref('base.group_portal')])],
|
||||
'name': 'Test User',
|
||||
'login': 'testuser',
|
||||
'password': 'testuser',
|
||||
})
|
||||
test_user = self.authenticate("testuser", "testuser")
|
||||
partner = self.env["res.users"].browse(test_user.uid).partner_id
|
||||
self.channel._add_members(users=testuser)
|
||||
res = self.url_open(
|
||||
url=f"/web/image/?field=avatar_128&id={self.channel.id}&model=discuss.channel&unique={self.channel.avatar_cache_key}"
|
||||
)
|
||||
self.assertIn(f"max-age={STATIC_CACHE_LONG}", res.headers["Cache-Control"])
|
||||
|
||||
res = self.url_open(
|
||||
url=f"/web/image/?field=avatar_128&id={self.channel.id}&model=discuss.channel"
|
||||
)
|
||||
self.assertIn("no-cache", res.headers["Cache-Control"])
|
||||
|
||||
res = self.url_open(
|
||||
url=f"/web/image?field=avatar_128&id={partner.id}&model=res.partner&unique={fields.Datetime.to_string(partner.write_date)}"
|
||||
)
|
||||
self.assertIn(f"max-age={STATIC_CACHE_LONG}", res.headers["Cache-Control"])
|
||||
|
||||
res = self.url_open(
|
||||
url=f"/web/image?field=avatar_128&id={partner.id}&model=res.partner"
|
||||
)
|
||||
self.assertIn("no-cache", res.headers["Cache-Control"])
|
||||
|
||||
res = self.url_open(
|
||||
url=f"/web/image?field=avatar_128&id={self.guest.id}&model=mail.guest&unique={fields.Datetime.to_string(partner.write_date)}"
|
||||
)
|
||||
self.assertIn(f"max-age={STATIC_CACHE_LONG}", res.headers["Cache-Control"])
|
||||
|
||||
res = self.url_open(
|
||||
url=f"/web/image?field=avatar_128&id={self.guest.id}&model=mail.guest"
|
||||
)
|
||||
self.assertIn("no-cache", res.headers["Cache-Control"])
|
||||
|
||||
|
||||
@tagged("mail_message")
|
||||
class TestMessageLinks(MailCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.user_employee_1 = mail_new_test_user(cls.env, login='tao1', groups='base.group_user', name='Tao Lee')
|
||||
cls.public_channel = cls.env['discuss.channel']._create_channel(name='Public Channel1', group_id=None)
|
||||
cls.private_group = cls.env['discuss.channel']._create_group(partners_to=cls.user_employee_1.partner_id.ids, name="Group")
|
||||
|
||||
@users('employee')
|
||||
def test_message_link_by_employee(self):
|
||||
channel_message = self.public_channel.message_post(body='Public Channel Message', message_type='comment')
|
||||
private_message_id = self.private_group.with_user(self.user_employee_1).message_post(
|
||||
body='Private Message',
|
||||
message_type='comment',
|
||||
).id
|
||||
self.authenticate('employee', 'employee')
|
||||
with self.subTest(channel_message=channel_message):
|
||||
expected_url = self.base_url() + f'/odoo/action-mail.action_discuss?active_id={channel_message.res_id}&highlight_message_id={channel_message.id}'
|
||||
res = self.url_open(f'/mail/message/{channel_message.id}')
|
||||
self.assertEqual(res.url, expected_url)
|
||||
with self.subTest(private_message_id=private_message_id):
|
||||
res = self.url_open(f'/mail/message/{private_message_id}')
|
||||
self.assertEqual(res.status_code, 404)
|
||||
|
||||
@users('employee')
|
||||
def test_message_link_by_public(self):
|
||||
message = self.public_channel.message_post(
|
||||
body='Public Channel Message',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment'
|
||||
)
|
||||
res = self.url_open(f'/mail/message/{message.id}')
|
||||
self.assertEqual(res.status_code, 200)
|
||||
1295
odoo-bringout-oca-ocb-mail/mail/tests/discuss/test_rtc.py
Normal file
1295
odoo-bringout-oca-ocb-mail/mail/tests/discuss/test_rtc.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,28 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from odoo import Command, http
|
||||
from odoo.tests.common import tagged, HttpCase
|
||||
from odoo.tools import file_open, mute_logger
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestToggleUpload(HttpCase):
|
||||
def test_upload_allowed(self):
|
||||
self.authenticate(None, None)
|
||||
channel = self.env["discuss.channel"].create({"name": "General", "group_public_id": None})
|
||||
guest = self.env["mail.guest"].create({"name": "Guest"})
|
||||
channel.write({"channel_member_ids": [Command.create({"guest_id": guest.id})]})
|
||||
with file_open("addons/web/__init__.py") as file:
|
||||
response = self.url_open(
|
||||
"/mail/attachment/upload",
|
||||
{
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
"thread_id": channel.id,
|
||||
"thread_model": "discuss.channel",
|
||||
},
|
||||
files={"ufile": file},
|
||||
cookies={guest._cookie_name: guest._format_auth_cookie()}
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
48
odoo-bringout-oca-ocb-mail/mail/tests/discuss/test_ui.py
Normal file
48
odoo-bringout-oca-ocb-mail/mail/tests/discuss/test_ui.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
from odoo import Command
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo, new_test_user
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestUi(HttpCaseWithUserDemo):
|
||||
|
||||
def test_01_mail_tour(self):
|
||||
self.start_tour("/odoo", 'discuss_channel_tour', login="admin")
|
||||
|
||||
def test_02_mail_create_channel_no_mail_tour(self):
|
||||
self.env['res.users'].create({
|
||||
'email': '', # User should be able to create a channel even if no email is defined
|
||||
'group_ids': [Command.set([self.ref('base.group_user')])],
|
||||
'name': 'Test User',
|
||||
'login': 'testuser',
|
||||
'password': 'testuser',
|
||||
})
|
||||
self.start_tour("/odoo", 'discuss_channel_tour', login='testuser')
|
||||
|
||||
# basic rendering test of the configuration menu in Discuss
|
||||
def test_03_mail_discuss_configuration_tour(self):
|
||||
self.start_tour("/odoo", "discuss_configuration_tour", login="admin")
|
||||
|
||||
def test_04_meeting_view_tour(self):
|
||||
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
|
||||
john = new_test_user(self.env, "john", groups="base.group_user", email="john@test.com")
|
||||
group_chat = (
|
||||
self.env["discuss.channel"]
|
||||
.with_user(bob)
|
||||
._create_group(
|
||||
partners_to=john.partner_id.ids, default_display_mode="video_full_screen"
|
||||
)
|
||||
)
|
||||
self.authenticate("bob", "bob")
|
||||
self.make_jsonrpc_request("/mail/rtc/channel/join_call", {"channel_id": group_chat.id})
|
||||
self.start_tour(
|
||||
f"/odoo/discuss?active_id=discuss.channel_{group_chat.id}&fullscreen=1",
|
||||
"discuss.meeting_view_tour",
|
||||
login="john",
|
||||
)
|
||||
self.start_tour(group_chat.invitation_url, "discuss.meeting_view_public_tour", login="john")
|
||||
|
||||
def test_05_can_create_channel_tour(self):
|
||||
self.start_tour("odoo/discuss", "can_create_channel_from_form_view", login="demo")
|
||||
BIN
odoo-bringout-oca-ocb-mail/mail/tests/play.png
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/tests/play.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
|
|
@ -1,209 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
import odoo
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@odoo.tests.tagged("-at_install", "post_install")
|
||||
class TestDiscussController(HttpCaseWithUserDemo):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.channel = cls.env["mail.channel"].create(
|
||||
{
|
||||
"group_public_id": None,
|
||||
"name": "Test channel",
|
||||
}
|
||||
)
|
||||
cls.public_user = cls.env.ref("base.public_user")
|
||||
cls.attachments = (
|
||||
cls.env["ir.attachment"]
|
||||
.with_user(cls.public_user)
|
||||
.sudo()
|
||||
.create(
|
||||
[
|
||||
{
|
||||
"access_token": cls.env["ir.attachment"]._generate_access_token(),
|
||||
"name": "File 1",
|
||||
"res_id": 0,
|
||||
"res_model": "mail.compose.message",
|
||||
},
|
||||
{
|
||||
"access_token": cls.env["ir.attachment"]._generate_access_token(),
|
||||
"name": "File 2",
|
||||
"res_id": 0,
|
||||
"res_model": "mail.compose.message",
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
cls.guest = cls.env["mail.guest"].create({"name": "Guest"})
|
||||
cls.channel.add_members(guest_ids=cls.guest.ids)
|
||||
|
||||
@mute_logger("odoo.addons.http_routing.models.ir_http", "odoo.http")
|
||||
def test_channel_message_attachments(self):
|
||||
self.authenticate(None, None)
|
||||
self.opener.cookies[
|
||||
self.guest._cookie_name
|
||||
] = f"{self.guest.id}{self.guest._cookie_separator}{self.guest.access_token}"
|
||||
# test message post: token error
|
||||
res1 = self.url_open(
|
||||
url="/mail/message/post",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": self.channel._name,
|
||||
"thread_id": self.channel.id,
|
||||
"post_data": {
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[0].id],
|
||||
"attachment_tokens": ["wrong token"],
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res1.status_code, 200)
|
||||
self.assertIn(
|
||||
f"The attachment {self.attachments[0].id} does not exist or you do not have the rights to access it",
|
||||
res1.text,
|
||||
"guest should not be allowed to add attachment without token when posting message",
|
||||
)
|
||||
# test message post: token ok
|
||||
res2 = self.url_open(
|
||||
url="/mail/message/post",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"thread_model": self.channel._name,
|
||||
"thread_id": self.channel.id,
|
||||
"post_data": {
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[0].id],
|
||||
"attachment_tokens": [self.attachments[0].access_token],
|
||||
"message_type": "comment",
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res2.status_code, 200)
|
||||
message_format1 = res2.json()["result"]
|
||||
self.assertEqual(
|
||||
message_format1["attachment_ids"],
|
||||
json.loads(json.dumps(self.attachments[0]._attachment_format())),
|
||||
"guest should be allowed to add attachment with token when posting message",
|
||||
)
|
||||
# test message update: token error
|
||||
res3 = self.url_open(
|
||||
url="/mail/message/update_content",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"message_id": message_format1["id"],
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[1].id],
|
||||
"attachment_tokens": ["wrong token"],
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res3.status_code, 200)
|
||||
self.assertIn(
|
||||
f"The attachment {self.attachments[1].id} does not exist or you do not have the rights to access it",
|
||||
res3.text,
|
||||
"guest should not be allowed to add attachment without token when updating message",
|
||||
)
|
||||
# test message update: token ok
|
||||
res4 = self.url_open(
|
||||
url="/mail/message/update_content",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"message_id": message_format1["id"],
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[1].id],
|
||||
"attachment_tokens": [self.attachments[1].access_token],
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res4.status_code, 200)
|
||||
message_format2 = res4.json()["result"]
|
||||
self.assertEqual(
|
||||
message_format2["attachments"],
|
||||
json.loads(json.dumps(self.attachments.sorted()._attachment_format())),
|
||||
"guest should be allowed to add attachment with token when updating message",
|
||||
)
|
||||
# test message update: own attachment ok
|
||||
res5 = self.url_open(
|
||||
url="/mail/message/update_content",
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"message_id": message_format2["id"],
|
||||
"body": "test",
|
||||
"attachment_ids": [self.attachments[1].id],
|
||||
},
|
||||
}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
self.assertEqual(res5.status_code, 200)
|
||||
message_format3 = res5.json()["result"]
|
||||
self.assertEqual(
|
||||
message_format3["attachments"],
|
||||
json.loads(json.dumps(self.attachments.sorted()._attachment_format())),
|
||||
"guest should be allowed to add own attachment without token when updating message",
|
||||
)
|
||||
|
||||
@mute_logger("odoo.addons.http_routing.models.ir_http", "odoo.http")
|
||||
def test_attachment_hijack(self):
|
||||
att = self.env["ir.attachment"].create(
|
||||
[
|
||||
{
|
||||
"name": "arguments_for_firing_marc_demo",
|
||||
"res_id": 0,
|
||||
"res_model": "mail.compose.message",
|
||||
},
|
||||
]
|
||||
)
|
||||
demo = self.authenticate("demo", "demo")
|
||||
channel = self.env["mail.channel"].create({"group_public_id": None, "name": "public_channel"})
|
||||
channel.add_members(
|
||||
self.env["res.users"].browse(demo.uid).partner_id.ids
|
||||
) # don't care, we just need a channel where demo is follower
|
||||
no_access_request = self.url_open("/web/content/" + str(att.id))
|
||||
self.assertFalse(
|
||||
no_access_request.ok
|
||||
) # if this test breaks, it might be due to a change in /web/content, or the default rules for accessing an attachment. This is not an issue but it makes this test irrelevant.
|
||||
response = self.url_open(
|
||||
url="/mail/message/post",
|
||||
headers={"Content-Type": "application/json"}, # route called as demo
|
||||
data=json.dumps(
|
||||
{
|
||||
"params": {
|
||||
"post_data": {
|
||||
"attachment_ids": [att.id], # demo does not have access to this attachment id
|
||||
"body": "",
|
||||
"message_type": "comment",
|
||||
"partner_ids": [],
|
||||
"subtype_xmlid": "mail.mt_comment",
|
||||
},
|
||||
"thread_id": channel.id,
|
||||
"thread_model": "mail.channel",
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertNotIn(
|
||||
"arguments_for_firing_marc_demo", response.text
|
||||
) # demo should not be able to see the name of the document
|
||||
243
odoo-bringout-oca-ocb-mail/mail/tests/test_discuss_tools.py
Normal file
243
odoo-bringout-oca-ocb-mail/mail/tests/test_discuss_tools.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.tests import tagged, TransactionCase
|
||||
from odoo.tests.common import new_test_user
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestDiscussTools(TransactionCase):
|
||||
"""Test class for discuss tools."""
|
||||
|
||||
# 0xx generic tests (key not in ids_by_model)
|
||||
|
||||
def test_010_store_dict(self):
|
||||
"""Test dict is present in result."""
|
||||
store = Store()
|
||||
store.add_model_values("key1", {"id": 1, "test": True})
|
||||
self.assertEqual(store.get_result(), {"key1": [{"id": 1, "test": True}]})
|
||||
|
||||
def test_011_store_dict_update_same_id(self):
|
||||
"""Test dict update same id."""
|
||||
store = Store()
|
||||
store.add_model_values("key1", {"id": 1, "test": True})
|
||||
store.add_model_values("key1", {"id": 1, "test": False, "abc": 1})
|
||||
self.assertEqual(store.get_result(), {"key1": [{"id": 1, "test": False, "abc": 1}]})
|
||||
|
||||
def test_012_store_dict_update_multiple_ids(self):
|
||||
"""Test dict update multiple ids."""
|
||||
store = Store()
|
||||
store.add_model_values("key1", {"id": 1, "test": True})
|
||||
store.add_model_values("key1", {"id": 2, "test": True})
|
||||
store.add_model_values("key1", {"id": 2, "test": False, "abc": 1})
|
||||
self.assertEqual(
|
||||
store.get_result(),
|
||||
{"key1": [{"id": 1, "test": True}, {"id": 2, "test": False, "abc": 1}]},
|
||||
)
|
||||
|
||||
def test_040_store_invalid(self):
|
||||
"""Test adding invalid value."""
|
||||
store = Store()
|
||||
with self.assertRaises(AttributeError):
|
||||
store.add_model_values("key1", True)
|
||||
|
||||
def test_042_store_invalid_missing_id(self):
|
||||
"""Test Thread adding invalid list value."""
|
||||
store = Store()
|
||||
with self.assertRaises(AssertionError):
|
||||
store.add_model_values("key1", {"test": True})
|
||||
|
||||
def test_060_store_data_empty_val(self):
|
||||
"""Test empty values are not present in result."""
|
||||
store = Store()
|
||||
store.add_model_values("key1", {})
|
||||
self.assertEqual(store.get_result(), {})
|
||||
|
||||
def test_061_store_data_empty_not_empty(self):
|
||||
"""Test mixing empty and non-empty values."""
|
||||
store = Store()
|
||||
store.add_model_values("key1", {})
|
||||
store.add_model_values("key2", {"id": 1})
|
||||
self.assertEqual(store.get_result(), {"key2": [{"id": 1}]})
|
||||
|
||||
def test_075_store_same_related_field_twice(self):
|
||||
"""Test adding the same related field twice combines their data"""
|
||||
user = mail_new_test_user(self.env, login="test_user", name="Test User")
|
||||
self.assertEqual(
|
||||
Store()
|
||||
.add(
|
||||
user,
|
||||
[Store.One("partner_id", "name"), Store.One("partner_id", "country_id")],
|
||||
)
|
||||
.get_result(),
|
||||
{
|
||||
"res.partner": [
|
||||
{
|
||||
"id": user.partner_id.id,
|
||||
"name": "Test User",
|
||||
"country_id": False,
|
||||
},
|
||||
],
|
||||
"res.users": [
|
||||
{"id": user.id, "partner_id": user.partner_id.id},
|
||||
],
|
||||
},
|
||||
)
|
||||
# 1xx Store specific tests (singleton in ids_by_model)
|
||||
|
||||
def test_110_store_store_singleton(self):
|
||||
"""Test Store dict is present in result as a singleton."""
|
||||
store = Store()
|
||||
store.add_global_values(test=True)
|
||||
self.assertEqual(store.get_result(), {"Store": {"test": True}})
|
||||
|
||||
def test_111_store_store_dict_update(self):
|
||||
"""Test Store dict update."""
|
||||
store = Store()
|
||||
store.add_global_values(test=True)
|
||||
store.add_global_values(test=False, abc=1)
|
||||
self.assertEqual(store.get_result(), {"Store": {"test": False, "abc": 1}})
|
||||
|
||||
def test_140_store_store_invalid_bool(self):
|
||||
"""Test Store adding invalid value."""
|
||||
store = Store()
|
||||
with self.assertRaises(AttributeError):
|
||||
store.add_model_values("key1", True)
|
||||
|
||||
def test_141_store_store_invalid_list(self):
|
||||
"""Test adding invalid value."""
|
||||
store = Store()
|
||||
with self.assertRaises(AttributeError):
|
||||
store.add_model_values("key1", [{"test": True}])
|
||||
|
||||
def test_160_store_store_data_empty_val(self):
|
||||
"""Test Store empty values are not present in result."""
|
||||
store = Store()
|
||||
store.add_global_values()
|
||||
self.assertEqual(store.get_result(), {})
|
||||
|
||||
def test_161_store_store_data_empty_not_empty(self):
|
||||
"""Test Store mixing empty and non-empty values."""
|
||||
store = Store()
|
||||
store.add_global_values()
|
||||
store.add_model_values("key2", {"id": 1})
|
||||
self.assertEqual(store.get_result(), {"key2": [{"id": 1}]})
|
||||
|
||||
# 2xx Thread specific tests (dual id in ids_by_model)
|
||||
|
||||
def test_210_store_thread_dict(self):
|
||||
"""Test Thread dict is present in result."""
|
||||
store = Store()
|
||||
store.add_model_values("mail.thread", {"id": 1, "model": "res.partner", "test": True})
|
||||
self.assertEqual(
|
||||
store.get_result(), {"mail.thread": [{"id": 1, "model": "res.partner", "test": True}]}
|
||||
)
|
||||
|
||||
def test_211_store_thread_dict_update_same_id(self):
|
||||
"""Test Thread dict update same id."""
|
||||
store = Store()
|
||||
store.add_model_values("mail.thread", {"id": 1, "model": "res.partner", "test": True})
|
||||
store.add_model_values("mail.thread", {"id": 1, "model": "res.partner", "test": False, "abc": 1})
|
||||
self.assertEqual(
|
||||
store.get_result(),
|
||||
{"mail.thread": [{"id": 1, "model": "res.partner", "test": False, "abc": 1}]},
|
||||
)
|
||||
|
||||
def test_212_store_thread_dict_update_multiple_ids(self):
|
||||
"""Test Thread dict update multiple ids."""
|
||||
store = Store()
|
||||
store.add_model_values("mail.thread", {"id": 1, "model": "res.partner", "test": True})
|
||||
store.add_model_values("mail.thread", {"id": 2, "model": "res.partner", "test": True})
|
||||
store.add_model_values("mail.thread", {"id": 2, "model": "res.partner", "test": False, "abc": 1})
|
||||
self.assertEqual(
|
||||
store.get_result(),
|
||||
{
|
||||
"mail.thread": [
|
||||
{"id": 1, "model": "res.partner", "test": True},
|
||||
{"id": 2, "model": "res.partner", "test": False, "abc": 1},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
def test_213_store_thread_dict_update_multiple_models(self):
|
||||
"""Test Thread dict update multiple models."""
|
||||
store = Store()
|
||||
store.add_model_values("mail.thread", {"id": 1, "model": "res.partner", "test": True})
|
||||
store.add_model_values("mail.thread", {"id": 2, "model": "res.partner", "test": True})
|
||||
store.add_model_values("mail.thread", {"id": 2, "model": "discuss.channel", "test": True, "abc": 1})
|
||||
store.add_model_values("mail.thread", {"id": 2, "model": "discuss.channel", "test": False, "abc": 2})
|
||||
store.add_model_values("mail.thread", {"id": 1, "model": "res.partner", "test": False})
|
||||
self.assertEqual(
|
||||
store.get_result(),
|
||||
{
|
||||
"mail.thread": [
|
||||
{"id": 1, "model": "res.partner", "test": False},
|
||||
{"id": 2, "model": "res.partner", "test": True},
|
||||
{"id": 2, "model": "discuss.channel", "test": False, "abc": 2},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
def test_240_store_thread_invalid_bool(self):
|
||||
"""Test Thread adding invalid bool value."""
|
||||
store = Store()
|
||||
with self.assertRaises(AttributeError):
|
||||
store.add_model_values("mail.thread", True)
|
||||
|
||||
def test_241_store_thread_invalid_list(self):
|
||||
"""Test Thread adding invalid list value."""
|
||||
store = Store()
|
||||
with self.assertRaises(AttributeError):
|
||||
store.add_model_values("mail.thread", [True])
|
||||
|
||||
def test_242_store_thread_invalid_missing_id(self):
|
||||
"""Test Thread adding invalid missing id."""
|
||||
store = Store()
|
||||
with self.assertRaises(AssertionError):
|
||||
store.add_model_values("mail.thread", {"model": "res.partner"})
|
||||
|
||||
def test_243_store_thread_invalid_missing_model(self):
|
||||
"""Test Thread adding invalid list value."""
|
||||
store = Store()
|
||||
with self.assertRaises(AssertionError):
|
||||
store.add_model_values("mail.thread", {"id": 1})
|
||||
|
||||
def test_260_store_thread_data_empty_val(self):
|
||||
"""Test Thread empty values are not present in result."""
|
||||
store = Store()
|
||||
store.add_model_values("mail.thread", {})
|
||||
self.assertEqual(store.get_result(), {})
|
||||
|
||||
def test_261_store_thread_data_empty_not_empty(self):
|
||||
"""Test Thread mixing empty and non-empty values."""
|
||||
store = Store()
|
||||
store.add_model_values("key1", {})
|
||||
store.add_model_values("mail.thread", {"id": 1, "model": "res.partner"})
|
||||
self.assertEqual(store.get_result(), {"mail.thread": [{"id": 1, "model": "res.partner"}]})
|
||||
|
||||
# 3xx Tests with real models
|
||||
|
||||
def test_350_non_list_extra_fields_copy_when_following_relations(self):
|
||||
"""Test that non-list extra_fields are properly copied when following relations."""
|
||||
user = new_test_user(self.env, "test_user_350@example.com")
|
||||
store = Store()
|
||||
store.add(user, Store.One("partner_id", extra_fields="email"))
|
||||
self.assertEqual(store.get_result()["res.partner"][0]["email"], "test_user_350@example.com")
|
||||
|
||||
def test_355_single_extra_fields_copy_with_records(self):
|
||||
"""Test that dynamic_fields apply individually to each record even when list extra_fields are present."""
|
||||
user_a = new_test_user(self.env, "test_user_355_a@example.com")
|
||||
user_b = new_test_user(self.env, "test_user_355_b@example.com")
|
||||
store = Store()
|
||||
store.add(
|
||||
user_a + user_b,
|
||||
Store.One(
|
||||
"partner_id",
|
||||
[],
|
||||
dynamic_fields=lambda user: ["email"] if user == user_a else [],
|
||||
extra_fields=["name"],
|
||||
),
|
||||
)
|
||||
self.assertEqual(store.get_result()["res.partner"][0]["email"], "test_user_355_a@example.com")
|
||||
self.assertNotIn("email", store.get_result()["res.partner"][1])
|
||||
106
odoo-bringout-oca-ocb-mail/mail/tests/test_fetchmail.py
Normal file
106
odoo-bringout-oca-ocb-mail/mail/tests/test_fetchmail.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.tests import TransactionCase, mute_logger
|
||||
|
||||
|
||||
class MockedConnection:
|
||||
def __init__(self):
|
||||
self.mock_messages = {}
|
||||
|
||||
def check_unread_messages(self):
|
||||
return len(self.mock_messages)
|
||||
|
||||
def retrieve_unread_messages(self):
|
||||
yield from list(self.mock_messages.items())
|
||||
|
||||
def handled_message(self, num):
|
||||
self.mock_messages.pop(num)
|
||||
|
||||
def disconnect(self):
|
||||
pass
|
||||
|
||||
|
||||
class TestFetchmail(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# mock connection
|
||||
cls.connection = MockedConnection()
|
||||
|
||||
def _connect__(self, allow_archived=False):
|
||||
self.ensure_one()
|
||||
return cls.connection
|
||||
patcher = patch.object(cls.registry['fetchmail.server'], '_connect__', _connect__)
|
||||
patcher.start()
|
||||
cls.addClassCleanup(patcher.stop)
|
||||
|
||||
def setUp(self):
|
||||
self.connection.mock_messages.clear()
|
||||
return super().setUp()
|
||||
|
||||
def test_fetchmail(self):
|
||||
mail_server = self.env['fetchmail.server'].create({
|
||||
'name': 'test server',
|
||||
})
|
||||
mail_server.search([('id', '!=', mail_server.id)]).action_archive()
|
||||
|
||||
# confirm the server
|
||||
mail_server.button_confirm_login()
|
||||
self.assertEqual(mail_server.state, 'done')
|
||||
|
||||
# fetch mail
|
||||
partner = self.env['res.partner'].create({'name': 'fetch test'})
|
||||
|
||||
def message_process(obj, model, message, **kw):
|
||||
self.assertEqual(message, "test msg")
|
||||
partner.with_env(obj.env).name = 'processed'
|
||||
with (
|
||||
self.enter_registry_test_mode(),
|
||||
self.registry.cursor() as cr,
|
||||
patch.object(self.registry['mail.thread'], 'message_process', side_effect=message_process, autospec=True) as process,
|
||||
):
|
||||
self.connection.mock_messages[1] = "test msg"
|
||||
mail_server.with_env(mail_server.env(cr=cr)).fetch_mail()
|
||||
process.assert_called_once()
|
||||
self.assertFalse(self.connection.mock_messages, "message not handled")
|
||||
self.assertEqual(partner.name, 'processed', "message_process side effect should be saved")
|
||||
|
||||
def test_fetchmail_deactivation(self):
|
||||
mail_server = self.env['fetchmail.server'].create({
|
||||
'name': 'test deactivation server',
|
||||
})
|
||||
mail_server.search([('id', '!=', mail_server.id)]).action_archive()
|
||||
mail_server.button_confirm_login()
|
||||
self.assertEqual(mail_server.state, 'done')
|
||||
|
||||
# fetch mail
|
||||
connection_failed_exception = Exception("mocked connection that fails")
|
||||
|
||||
def _connect__(obj, **kw):
|
||||
raise connection_failed_exception
|
||||
with (
|
||||
self.enter_registry_test_mode(),
|
||||
self.registry.cursor() as cr,
|
||||
patch.object(self.registry['fetchmail.server'], '_connect__', side_effect=_connect__, autospec=True),
|
||||
mute_logger('odoo.addons.mail.models'),
|
||||
self.assertLogs('odoo.addons.base.models.ir_cron') as cron_log_catcher,
|
||||
):
|
||||
server = mail_server.with_env(mail_server.env(cr=cr))
|
||||
self.assertFalse(server.error_date)
|
||||
self.assertFalse(server.error_message)
|
||||
exc = server._fetch_mail()
|
||||
self.assertIs(exc, connection_failed_exception)
|
||||
self.assertTrue(server.error_date)
|
||||
self.assertIn("mocked connection", server.error_message)
|
||||
|
||||
# set the date in past for deactivation
|
||||
server.error_date = server.error_date - timedelta(days=7, minutes=1)
|
||||
exc = server._fetch_mail()
|
||||
self.assertIs(exc, connection_failed_exception)
|
||||
self.assertEqual(server.state, 'draft')
|
||||
self.assertIn('WARNING:odoo.addons.base.models.ir_cron:Deactivating fetchmail imap server test deactivation server (too many failures)', cron_log_catcher.output)
|
||||
31
odoo-bringout-oca-ocb-mail/mail/tests/test_font_to_img.py
Normal file
31
odoo-bringout-oca-ocb-mail/mail/tests/test_font_to_img.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
from odoo.tools.misc import file_open
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestFontToImg(HttpCase):
|
||||
|
||||
def test_font_to_img(self):
|
||||
# This test was introduced because the play button was cropped in noble following some adaptation.
|
||||
# This test is able to reproduce the issue and ensure that the expected result is the right one
|
||||
# comparing image is not ideal, but this should work in most case, maybe adapted if the font is changed.
|
||||
|
||||
response = self.url_open(
|
||||
"/mail/font_to_img/61802/rgb(0,143,140)/rgb(255,255,255)/190x200"
|
||||
)
|
||||
|
||||
img = Image.open(BytesIO(response.content))
|
||||
self.assertEqual(
|
||||
img.size,
|
||||
(201, 200),
|
||||
"Looks strange regarding request but this is the current result",
|
||||
)
|
||||
# Image is a play button
|
||||
img_reference = Image.open(file_open("mail/tests/play.png", "rb"))
|
||||
self.assertEqual(img, img_reference, "Result image should be the play button")
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo
|
||||
from odoo.tests import HttpCase
|
||||
|
||||
|
||||
@odoo.tests.tagged('-at_install', 'post_install')
|
||||
class TestGetModelDefinitions(HttpCase):
|
||||
def test_access_cr(self):
|
||||
""" Checks that get_model_definitions does not return anything else than models """
|
||||
with self.assertRaises(KeyError):
|
||||
self.env['ir.model']._get_model_definitions(['res.users', 'cr'])
|
||||
|
||||
def test_access_all_model_fields(self):
|
||||
"""
|
||||
Check that get_model_definitions return all the models
|
||||
and their fields
|
||||
"""
|
||||
model_definitions = self.env['ir.model']._get_model_definitions([
|
||||
'res.users', 'res.partner'
|
||||
])
|
||||
# models are retrieved
|
||||
self.assertIn('res.users', model_definitions)
|
||||
self.assertIn('res.partner', model_definitions)
|
||||
# check that model fields are retrieved
|
||||
self.assertTrue(
|
||||
all(fname in model_definitions['res.users'].keys() for fname in ['email', 'name', 'partner_id'])
|
||||
)
|
||||
self.assertTrue(
|
||||
all(fname in model_definitions['res.partner'].keys() for fname in ['active', 'date', 'name'])
|
||||
)
|
||||
|
||||
def test_relational_fields_with_missing_model(self):
|
||||
"""
|
||||
Check that get_model_definitions only returns relational fields
|
||||
if the model is requested
|
||||
"""
|
||||
model_definitions = self.env['ir.model']._get_model_definitions([
|
||||
'res.partner'
|
||||
])
|
||||
# since res.country is not requested, country_id shouldn't be in
|
||||
# the model definition fields
|
||||
self.assertNotIn('country_id', model_definitions['res.partner'])
|
||||
|
||||
model_definitions = self.env['ir.model']._get_model_definitions([
|
||||
'res.partner', 'res.country',
|
||||
])
|
||||
# res.country is requested, country_id should be present on res.partner
|
||||
self.assertIn('country_id', model_definitions['res.partner'])
|
||||
745
odoo-bringout-oca-ocb-mail/mail/tests/test_ir_mail_server.py
Normal file
745
odoo-bringout-oca-ocb-mail/mail/tests/test_ir_mail_server.py
Normal file
|
|
@ -0,0 +1,745 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tools import config, mute_logger, split_every
|
||||
|
||||
|
||||
@tagged('mail_server')
|
||||
class TestIrMailServer(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.default_bounce_address = f'{cls.alias_bounce}@{cls.alias_domain}'
|
||||
cls.default_from_address = f'{cls.default_from}@{cls.alias_domain}'
|
||||
|
||||
def test_alter_smtp_to_list(self):
|
||||
""" Check smtp_to_list alteration. Reminder: Message is the envelope,
|
||||
SMTP is the actual sending. """
|
||||
IrMailServer = self.env['ir.mail_server']
|
||||
mail_from = 'specific_user@test.mycompany.com'
|
||||
|
||||
for mail_server, mail_values, smtp_to_lst, msg_to_lst, msg_cc_lst in [
|
||||
(
|
||||
IrMailServer,
|
||||
{'email_to': '"Customer" <customer@test.example.com>'},
|
||||
['customer@test.example.com'],
|
||||
['"Customer" <customer@test.example.com>'],
|
||||
[],
|
||||
),
|
||||
# 'send_validated_to' context key: restrict SMTP To actual recipients
|
||||
# but do not rewrite Msg['To'], aka envelope (main usage is to cleanup
|
||||
# addresses found by extract_rfc2822_addresses anyway)
|
||||
(
|
||||
IrMailServer.with_context(send_validated_to=['another@test.example.com', 'customer@test.example.com']),
|
||||
{'email_to': ['"Customer" <customer@test.example.com>', 'user2@test.mycompany.com']},
|
||||
['customer@test.example.com'],
|
||||
['"Customer" <customer@test.example.com>', 'user2@test.mycompany.com'],
|
||||
[],
|
||||
),
|
||||
# 'send_smtp_skip_to' context key: block list of SMTP recipients
|
||||
(
|
||||
IrMailServer.with_context(send_smtp_skip_to=['skip@test.example.com', 'other@test.example.com', 'wrong', 'skip.2@test.example.com']),
|
||||
{
|
||||
'email_to': ['"Customer" <customer@test.example.com>', '"Skip Me" <skip@test.example.com>',
|
||||
'"User" <user@test.mycompany.com>', 'user2@test.mycompany.com', '"Skip Me 2" <skip.2@test.example.com>'],
|
||||
},
|
||||
['customer@test.example.com', 'user@test.mycompany.com', 'user2@test.mycompany.com'],
|
||||
['"Customer" <customer@test.example.com>', '"Skip Me" <skip@test.example.com>',
|
||||
'"User" <user@test.mycompany.com>', 'user2@test.mycompany.com', '"Skip Me 2" <skip.2@test.example.com>'],
|
||||
{},
|
||||
),
|
||||
# 'X-Forge-To' header: force envelope Msg['To'] (not SMTP recipients)
|
||||
# used notably for mailing lists
|
||||
(
|
||||
IrMailServer,
|
||||
{
|
||||
'email_to': ['"Customer" <customer@test.example.com>', 'user2@test.mycompany.com'],
|
||||
'headers': {'X-Forge-To': 'mailing@some.domain'}
|
||||
},
|
||||
['customer@test.example.com', 'user2@test.mycompany.com'],
|
||||
['mailing@some.domain'],
|
||||
[],
|
||||
),
|
||||
# 'X-Msg-To-Add' header: add in Msg['To'] without impacting SMTP To, e.g.
|
||||
# displaying more recipients than actually mailed
|
||||
(
|
||||
IrMailServer,
|
||||
{
|
||||
'email_to': ['"Customer" <customer@test.example.com>', 'user2@test.mycompany.com'],
|
||||
'headers': {'X-Msg-To-Add': '"Other" <other.customer@test.example.com>'}
|
||||
},
|
||||
['customer@test.example.com', 'user2@test.mycompany.com'],
|
||||
['"Customer" <customer@test.example.com>', 'user2@test.mycompany.com', '"Other" <other.customer@test.example.com>'],
|
||||
{},
|
||||
),
|
||||
]:
|
||||
with self.subTest(mail_values=mail_values, smtp_to_lst=smtp_to_lst):
|
||||
with self.mock_smtplib_connection():
|
||||
smtp_session = mail_server._connect__(smtp_from=mail_from)
|
||||
message = self._build_email(mail_from=mail_from, **mail_values)
|
||||
mail_server.send_email(message, smtp_session=smtp_session)
|
||||
|
||||
self.assertEqual(len(self.emails), 1)
|
||||
self.assertSMTPEmailsSent(
|
||||
message_from=mail_from,
|
||||
smtp_from=mail_from,
|
||||
smtp_to_list=smtp_to_lst,
|
||||
msg_cc_lst=msg_cc_lst,
|
||||
msg_to_lst=msg_to_lst,
|
||||
)
|
||||
|
||||
def test_assert_base_values(self):
|
||||
self.assertEqual(
|
||||
self.env['ir.mail_server']._get_default_bounce_address(),
|
||||
self.default_bounce_address)
|
||||
self.assertEqual(
|
||||
self.env['ir.mail_server']._get_default_from_address(),
|
||||
self.default_from_address)
|
||||
|
||||
@patch.dict(config.options, {"email_from": "settings@example.com"})
|
||||
def test_default_email_from(self, *args):
|
||||
""" Check that default_from parameter of alias domain respected. """
|
||||
for (default_from, domain_name), expected_from in zip(
|
||||
[
|
||||
('icp', 'test.mycompany.com'),
|
||||
(False, 'test.mycompany.com'),
|
||||
(False, False),
|
||||
], [
|
||||
"icp@test.mycompany.com",
|
||||
"settings@example.com",
|
||||
"settings@example.com",
|
||||
],
|
||||
):
|
||||
with self.subTest(default_from=default_from, domain_name=domain_name):
|
||||
if domain_name:
|
||||
self.mail_alias_domain.name = domain_name
|
||||
self.mail_alias_domain.default_from = default_from
|
||||
self.env.company.alias_domain_id = self.mail_alias_domain
|
||||
else:
|
||||
self.env.company.alias_domain_id = False
|
||||
message = self.env["ir.mail_server"]._build_email__(
|
||||
False, "recipient@example.com", "Subject",
|
||||
"The body of an email",
|
||||
)
|
||||
self.assertEqual(message["From"], expected_from)
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
@patch.dict(config.options, {
|
||||
"from_filter": "dummy@example.com, test.mycompany.com, dummy2@example.com",
|
||||
"smtp_server": "example.com",
|
||||
})
|
||||
def test_mail_server_config_bin(self):
|
||||
""" Test the configuration provided in the odoo-bin arguments. This config
|
||||
is used when no mail server exists. Test with and without giving a
|
||||
pre-configured SMTP session, should not impact results.
|
||||
|
||||
Also check "mail.default.from_filter" parameter usage that should overwrite
|
||||
odoo-bin argument "--from-filter".
|
||||
"""
|
||||
IrMailServer = self.env['ir.mail_server']
|
||||
|
||||
# Remove all mail server so we will use the odoo-bin arguments
|
||||
IrMailServer.search([]).unlink()
|
||||
self.assertFalse(IrMailServer.search([]))
|
||||
|
||||
for mail_from, (expected_smtp_from, expected_msg_from) in zip(
|
||||
[
|
||||
# inside "from_filter" domain
|
||||
'specific_user@test.mycompany.com',
|
||||
'"Formatted Name" <specific_user@test.mycompany.com>',
|
||||
'"Formatted Name" <specific_user@test.MYCOMPANY.com>',
|
||||
'"Formatted Name" <SPECIFIC_USER@test.mycompany.com>',
|
||||
# outside "from_filter" domain
|
||||
'test@unknown_domain.com',
|
||||
'"Formatted Name" <test@unknown_domain.com>',
|
||||
], [
|
||||
# inside "from_filter" domain: no rewriting
|
||||
(self.default_bounce_address, 'specific_user@test.mycompany.com'),
|
||||
(self.default_bounce_address, '"Formatted Name" <specific_user@test.mycompany.com>'),
|
||||
(self.default_bounce_address, '"Formatted Name" <specific_user@test.MYCOMPANY.com>'),
|
||||
(self.default_bounce_address, '"Formatted Name" <SPECIFIC_USER@test.mycompany.com>'),
|
||||
# outside "from_filter" domain: we will use notifications emails in the
|
||||
# headers, and bounce address in the envelope because the "from_filter"
|
||||
# allows to use the entire domain
|
||||
(self.default_bounce_address, f'"test" <{self.default_from}@{self.alias_domain}>'),
|
||||
(self.default_bounce_address, f'"Formatted Name" <{self.default_from}@{self.alias_domain}>'),
|
||||
]
|
||||
):
|
||||
for provide_smtp in [False, True]: # providing smtp session should ont impact test
|
||||
with self.subTest(mail_from=mail_from, provide_smtp=provide_smtp):
|
||||
with self.mock_smtplib_connection():
|
||||
if provide_smtp:
|
||||
smtp_session = IrMailServer._connect__(smtp_from=mail_from)
|
||||
message = self._build_email(mail_from=mail_from)
|
||||
IrMailServer.send_email(message, smtp_session=smtp_session)
|
||||
else:
|
||||
message = self._build_email(mail_from=mail_from)
|
||||
IrMailServer.send_email(message)
|
||||
|
||||
self.connect_mocked.assert_called_once()
|
||||
self.assertEqual(len(self.emails), 1)
|
||||
self.assertSMTPEmailsSent(
|
||||
smtp_from=expected_smtp_from,
|
||||
message_from=expected_msg_from,
|
||||
from_filter="dummy@example.com, test.mycompany.com, dummy2@example.com",
|
||||
)
|
||||
|
||||
# for from_filter in ICP, overwrite the one from odoo-bin
|
||||
self.env['ir.config_parameter'].sudo().set_param('mail.default.from_filter', 'icp.example.com')
|
||||
|
||||
# Use an email in the domain of the config parameter "mail.default.from_filter"
|
||||
with self.mock_smtplib_connection():
|
||||
message = self._build_email(mail_from='specific_user@icp.example.com')
|
||||
IrMailServer.send_email(message)
|
||||
|
||||
self.assertSMTPEmailsSent(
|
||||
smtp_from='specific_user@icp.example.com',
|
||||
message_from='specific_user@icp.example.com',
|
||||
from_filter='icp.example.com',
|
||||
)
|
||||
|
||||
@users('admin')
|
||||
def test_mail_server_get_test_email_from(self):
|
||||
""" Test the email used to test the mail server connection. Check
|
||||
from_filter parsing / alias_domain.default_from support. """
|
||||
test_server = self.env['ir.mail_server'].create({
|
||||
'from_filter': 'example_2.com, example_3.com',
|
||||
'name': 'Test Server',
|
||||
'smtp_host': 'smtp_host',
|
||||
'smtp_encryption': 'none',
|
||||
})
|
||||
|
||||
# check default.from / filter matching
|
||||
for (default_from, from_filter), expected_test_email in zip(
|
||||
[
|
||||
('notifications', 'dummy.com, full_email@example_2.com, dummy2.com'),
|
||||
('notifications', self.mail_alias_domain.name),
|
||||
('notifications', f'{self.mail_alias_domain.name}, example_2.com'),
|
||||
# default relies on "odoo"
|
||||
(False, self.mail_alias_domain.name),
|
||||
# fallback on user email if no from_filter
|
||||
('notifications', ' '),
|
||||
('notifications', ','),
|
||||
('notifications', False),
|
||||
(False, False),
|
||||
], [
|
||||
'full_email@example_2.com',
|
||||
f'notifications@{self.mail_alias_domain.name}',
|
||||
f'notifications@{self.mail_alias_domain.name}',
|
||||
f'odoo@{self.mail_alias_domain.name}',
|
||||
self.env.user.email,
|
||||
self.env.user.email,
|
||||
self.env.user.email,
|
||||
self.env.user.email,
|
||||
],
|
||||
):
|
||||
with self.subTest(default_from=default_from, from_filter=from_filter):
|
||||
self.mail_alias_domain.default_from = default_from
|
||||
test_server.from_filter = from_filter
|
||||
email_from = test_server._get_test_email_from()
|
||||
self.assertEqual(email_from, expected_test_email)
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_mail_server_priorities(self):
|
||||
""" Test if we choose the right mail server to send an email.
|
||||
Priorities are
|
||||
1. Forced mail server (e.g.: in mass mailing)
|
||||
- If the "from_filter" of the mail server match the notification email
|
||||
use the notifications email in the "From header"
|
||||
- Otherwise spoof the "From" (because we force the mail server but we don't
|
||||
know which email use to send it)
|
||||
2. A mail server for which the "from_filter" match the "From" header
|
||||
3. A mail server for which the "from_filter" match the domain of the "From" header
|
||||
4. The mail server used for notifications
|
||||
5. A mail server without "from_filter" (and so spoof the "From" header because we
|
||||
do not know for which email address it can be used)
|
||||
"""
|
||||
# this mail server can now be used for a specific email address and 2 domain names
|
||||
self.mail_server_user.from_filter = "domain1.com, specific_user@test.mycompany.com, domain2.com"
|
||||
|
||||
for email_from, (expected_mail_server, expected_email_from) in zip(
|
||||
[
|
||||
# matches user-specific server
|
||||
'specific_user@test.mycompany.com',
|
||||
# matches user-specific server (with formatting') -> should extract
|
||||
# email from full name, must keep the given email_from
|
||||
'"Name name@strange.name" <specific_user@test.mycompany.com>',
|
||||
# case check
|
||||
'SPECIFIC_USER@test.mycompany.com',
|
||||
'specific_user@test.MYCOMPANY.com',
|
||||
# matches domain-based server: domain is case insensitive
|
||||
'unknown_email@test.mycompany.com',
|
||||
'unknown_email@TEST.MYCOMPANY.COM',
|
||||
'"Unknown" <unknown_email@test.mycompany.com>',
|
||||
# fallback on notification email
|
||||
'"Test" <test@unknown_domain.com>',
|
||||
# fallback when email_from is False, should default to notification email
|
||||
False,
|
||||
# mail_server_user multiple from_filter check: can be used for a
|
||||
# specific email and 2 domain names -> check other domains in filter
|
||||
'"Example" <test@domain2.com>',
|
||||
'"Example" <test@domain1.com>',
|
||||
], [
|
||||
(self.mail_server_user, 'specific_user@test.mycompany.com'),
|
||||
(self.mail_server_user, '"Name name@strange.name" <specific_user@test.mycompany.com>'),
|
||||
(self.mail_server_user, 'SPECIFIC_USER@test.mycompany.com'),
|
||||
(self.mail_server_user, 'specific_user@test.MYCOMPANY.com'),
|
||||
(self.mail_server_domain, 'unknown_email@test.mycompany.com'),
|
||||
(self.mail_server_domain, 'unknown_email@TEST.MYCOMPANY.COM'),
|
||||
(self.mail_server_domain, '"Unknown" <unknown_email@test.mycompany.com>'),
|
||||
(self.mail_server_notification, f'{self.default_from}@test.mycompany.com'),
|
||||
(self.mail_server_notification, f'{self.default_from}@test.mycompany.com'),
|
||||
# mail_server_user multiple from_filter check
|
||||
(self.mail_server_user, '"Example" <test@domain2.com>'),
|
||||
(self.mail_server_user, '"Example" <test@domain1.com>'),
|
||||
],
|
||||
):
|
||||
with self.subTest(email_from=email_from):
|
||||
mail_server, mail_from = self.env['ir.mail_server']._find_mail_server(email_from=email_from)
|
||||
self.assertEqual(mail_server, expected_mail_server)
|
||||
self.assertEqual(mail_from, expected_email_from)
|
||||
|
||||
@mute_logger('odoo.models.unlink', 'odoo.addons.base.models.ir_mail_server')
|
||||
def test_mail_server_send_email(self):
|
||||
""" Test main 'send_email' usage: check mail_server choice based on from
|
||||
filters, encapsulation, spoofing. """
|
||||
IrMailServer = self.env['ir.mail_server']
|
||||
|
||||
for mail_from, (expected_smtp_from, expected_msg_from, expected_mail_server) in zip(
|
||||
[
|
||||
'specific_user@test.mycompany.com',
|
||||
'"Name" <test@unknown_domain.com>',
|
||||
'test@unknown_domain.com',
|
||||
'"Name" <unknown_name@test.mycompany.com>'
|
||||
], [
|
||||
# A mail server is configured for the email
|
||||
('specific_user@test.mycompany.com', 'specific_user@test.mycompany.com', self.mail_server_user),
|
||||
# No mail server is configured for the email address, so it will use the
|
||||
# notifications email instead and encapsulate the old email
|
||||
(f'{self.default_from}@{self.alias_domain}', f'"Name" <{self.default_from}@{self.alias_domain}>', self.mail_server_notification),
|
||||
# same situation, but the original email has no name part
|
||||
(f'{self.default_from}@{self.alias_domain}', f'"test" <{self.default_from}@{self.alias_domain}>', self.mail_server_notification),
|
||||
# A mail server is configured for the entire domain name, so we can use the bounce
|
||||
# email address because the mail server supports it
|
||||
(self.default_bounce_address, '"Name" <unknown_name@test.mycompany.com>', self.mail_server_domain),
|
||||
],
|
||||
):
|
||||
# test with and without providing an SMTP session, which should not impact test
|
||||
for provide_smtp in [True, False]:
|
||||
with self.subTest(mail_from=mail_from):
|
||||
with self.mock_smtplib_connection():
|
||||
if provide_smtp:
|
||||
smtp_session = IrMailServer._connect__(smtp_from=mail_from)
|
||||
message = self._build_email(mail_from=mail_from)
|
||||
IrMailServer.send_email(message, smtp_session=smtp_session)
|
||||
else:
|
||||
message = self._build_email(mail_from=mail_from)
|
||||
IrMailServer.send_email(message)
|
||||
self.connect_mocked.assert_called_once()
|
||||
self.assertEqual(len(self.emails), 1)
|
||||
self.assertSMTPEmailsSent(
|
||||
smtp_from=expected_smtp_from,
|
||||
message_from=expected_msg_from,
|
||||
mail_server=expected_mail_server,
|
||||
)
|
||||
|
||||
# remove the notification server
|
||||
# so <notifications.test@test.mycompany.com> will use the <test.mycompany.com> mail server
|
||||
# The mail server configured for the notifications email has been removed
|
||||
# but we can still use the mail server configured for test.mycompany.com
|
||||
# and so we will be able to use the bounce address
|
||||
# because we use the mail server for "test.mycompany.com"
|
||||
self.mail_server_notification.unlink()
|
||||
for provide_smtp in [False, True]:
|
||||
with self.mock_smtplib_connection():
|
||||
if provide_smtp:
|
||||
smtp_session = IrMailServer._connect__(smtp_from='"Name" <test@unknown_domain.com>')
|
||||
message = self._build_email(mail_from='"Name" <test@unknown_domain.com>')
|
||||
IrMailServer.send_email(message, smtp_session=smtp_session)
|
||||
else:
|
||||
message = self._build_email(mail_from='"Name" <test@unknown_domain.com>')
|
||||
IrMailServer.send_email(message)
|
||||
self.connect_mocked.assert_called_once()
|
||||
self.assertEqual(len(self.emails), 1)
|
||||
self.assertSMTPEmailsSent(
|
||||
smtp_from=self.default_bounce_address,
|
||||
message_from=f'"Name" <{self.default_from}@{self.alias_domain}>',
|
||||
mail_server=self.mail_server_domain,
|
||||
)
|
||||
|
||||
# miss-configured database, no mail servers from filter
|
||||
# match the user / notification email
|
||||
self.env['ir.mail_server'].search([]).from_filter = "random.domain"
|
||||
self.mail_alias_domain.default_from = 'test'
|
||||
self.mail_alias_domain.name = 'custom_domain.com'
|
||||
with self.mock_smtplib_connection():
|
||||
message = self._build_email(mail_from='specific_user@test.com')
|
||||
IrMailServer.send_email(message)
|
||||
|
||||
self.connect_mocked.assert_called_once()
|
||||
self.assertSMTPEmailsSent(
|
||||
smtp_from='test@custom_domain.com',
|
||||
message_from='"specific_user" <test@custom_domain.com>',
|
||||
from_filter='random.domain',
|
||||
)
|
||||
|
||||
|
||||
@tagged('mail_server')
|
||||
class TestPersonalServer(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user_1, cls.user_2 = cls.user_employee, cls.user_employee_c2
|
||||
cls.mail_server_1, cls.mail_server_2 = cls.env["ir.mail_server"].create([{
|
||||
'name': 'test',
|
||||
'owner_user_id': user.id,
|
||||
'from_filter': user.email,
|
||||
'smtp_user': user.email,
|
||||
'smtp_host': f'test_{i}@example.com',
|
||||
} for i, user in enumerate((cls.user_1, cls.user_2))])
|
||||
|
||||
@contextmanager
|
||||
def assert_mail_sent_then_scheduled(self, mails, to_process_count, sent_count, send_datetime):
|
||||
"""Assert that X emails has been sent, and the other have been scheduled."""
|
||||
TEST_LIMIT = 5
|
||||
outgoing = mails.filtered(
|
||||
lambda m: m.state == 'outgoing'
|
||||
and (not m.scheduled_date or m.scheduled_date <= send_datetime)
|
||||
).sorted(lambda m: (m.create_date, m.id))
|
||||
|
||||
self.assertEqual(len(outgoing), to_process_count)
|
||||
|
||||
yield
|
||||
|
||||
sent = outgoing.filtered(lambda m: m.state == 'sent')
|
||||
self.assertEqual(sent, outgoing[:len(sent)], "Should send in priority old mails")
|
||||
unsent = outgoing - sent
|
||||
self.assertEqual(len(sent), sent_count)
|
||||
|
||||
scheduled_dates = sorted(unsent.mapped('scheduled_date'))
|
||||
scheduled_dates = list(split_every(TEST_LIMIT, scheduled_dates))
|
||||
|
||||
for i, to_check in enumerate(scheduled_dates, start=1):
|
||||
self.assertEqual(set(to_check), {send_datetime.replace(second=0) + timedelta(minutes=i)})
|
||||
|
||||
@mute_logger('odoo.models.unlink', 'odoo.addons.base.models.ir_mail_server')
|
||||
@patch.dict(config.options, {
|
||||
"from_filter": "cli@example.com",
|
||||
"smtp_server": "example.com",
|
||||
})
|
||||
def test_personal_mail_server(self):
|
||||
"""Test that the personal mail servers can not be used as fallback."""
|
||||
IrMailServer = self.env['ir.mail_server']
|
||||
self.env['ir.mail_server'].search([]).from_filter = "random.domain"
|
||||
|
||||
# Sanity check, no owner so it can be used as fallback
|
||||
(self.env['ir.mail_server'].search([]) - self.mail_server_user).unlink()
|
||||
self.mail_server_user.write({
|
||||
'from_filter': 'user@test.mycompany.com',
|
||||
'owner_user_id': False,
|
||||
})
|
||||
with self.mock_smtplib_connection():
|
||||
message = self._build_email(mail_from='notifications.test@test.mycompany.com')
|
||||
IrMailServer.send_email(message)
|
||||
|
||||
self.connect_mocked.assert_called_once()
|
||||
self.assertSMTPEmailsSent(
|
||||
smtp_from='notifications.test@test.mycompany.com',
|
||||
message_from='notifications.test@test.mycompany.com',
|
||||
from_filter='user@test.mycompany.com',
|
||||
)
|
||||
|
||||
# Check that even if there is no other mail server,
|
||||
# we don't use the mail server having an owner as fallback
|
||||
# (to avoid leaking outgoing emails)
|
||||
self.mail_server_user.write({
|
||||
'from_filter': 'user@test.mycompany.com',
|
||||
'owner_user_id': self.env.user.id,
|
||||
})
|
||||
with self.mock_smtplib_connection():
|
||||
message = self._build_email(mail_from='notifications.test@test.mycompany.com')
|
||||
IrMailServer.send_email(message)
|
||||
|
||||
self.connect_mocked.assert_called_once()
|
||||
self.assertSMTPEmailsSent(
|
||||
smtp_from='notifications.test@test.mycompany.com',
|
||||
message_from='notifications.test@test.mycompany.com',
|
||||
from_filter='cli@example.com',
|
||||
)
|
||||
|
||||
with self.mock_smtplib_connection(), self.assertRaises(UserError):
|
||||
# We can't even force it
|
||||
message = self._build_email(mail_from='test@test.mycompany.com')
|
||||
IrMailServer.send_email(message, mail_server_id=self.mail_server_user.id)
|
||||
self.assertFalse(self.emails)
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_personal_mail_server_limit(self):
|
||||
# Test the limit per personal mail servers
|
||||
TEST_LIMIT = 5
|
||||
self.env['ir.config_parameter'].set_param('mail.server.personal.limit.minutes', str(TEST_LIMIT))
|
||||
user_1, user_2 = self.user_1, self.user_2
|
||||
mail_server_1, mail_server_2 = self.mail_server_1, self.mail_server_2
|
||||
|
||||
with self.mock_datetime_and_now("2025-01-01 20:02:23"):
|
||||
mails_user_1 = self.env["mail.mail"].with_user(user_1).sudo().create([
|
||||
{'state': 'outgoing', 'email_to': 'target@test.com', 'email_from': user_1.email}
|
||||
for i in range(22)
|
||||
])
|
||||
mails_user_2 = self.env["mail.mail"].with_user(user_2).sudo().create([
|
||||
{'state': 'outgoing', 'email_to': 'target@test.com', 'email_from': user_2.email}
|
||||
for i in range(17)
|
||||
])
|
||||
mails_other = self.env["mail.mail"].create([
|
||||
{'state': 'outgoing', 'email_to': 'target@test.com', 'email_from': user_1.email}
|
||||
for i in range(25)
|
||||
])
|
||||
|
||||
mails = mails_other + mails_user_1 + mails_user_2
|
||||
|
||||
self.assertEqual(mail_server_1.owner_limit_count, 0)
|
||||
self.assertFalse(mail_server_1.owner_limit_time)
|
||||
|
||||
DATE_SEND_1 = datetime(2025, 1, 1, 20, 5, 23)
|
||||
with (
|
||||
self.mock_smtplib_connection(),
|
||||
self.mock_datetime_and_now(DATE_SEND_1),
|
||||
self.assert_mail_sent_then_scheduled(mails_user_1, len(mails_user_1), 5, DATE_SEND_1),
|
||||
self.assert_mail_sent_then_scheduled(mails_user_2, len(mails_user_2), 5, DATE_SEND_1),
|
||||
):
|
||||
mails.send()
|
||||
|
||||
for personal_server in (mail_server_1, mail_server_2):
|
||||
self.assertEqual(personal_server.owner_limit_count, TEST_LIMIT)
|
||||
self.assertEqual(personal_server.owner_limit_time, DATE_SEND_1.replace(second=0))
|
||||
|
||||
self.assertEqual(self.connect_mocked.call_count, 3, "Called once for each mail server")
|
||||
|
||||
# Check that the email not related to personal mail server are all sent
|
||||
self.assertEqual(set(mails_other.mapped('state')), {'sent'})
|
||||
|
||||
# User 1 continues sending emails
|
||||
# Because emails are still in the queue, we delay all of them
|
||||
with self.mock_datetime_and_now("2025-01-01 20:04:23"):
|
||||
new_mails_user_1 = self.env["mail.mail"].with_user(user_1).sudo().create([
|
||||
{'state': 'outgoing', 'email_to': 'target@test.com', 'email_from': user_1.email}
|
||||
for i in range(12)
|
||||
])
|
||||
mails_user_1 |= new_mails_user_1
|
||||
|
||||
DATE_SEND_2 = datetime(2025, 1, 1, 20, 5, 23)
|
||||
with (
|
||||
self.mock_smtplib_connection(),
|
||||
self.mock_datetime_and_now(DATE_SEND_2),
|
||||
self.assert_mail_sent_then_scheduled(new_mails_user_1, 12, 0, DATE_SEND_2),
|
||||
):
|
||||
new_mails_user_1.send()
|
||||
|
||||
self.assertEqual(mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
self.assertEqual(mail_server_1.owner_limit_time, DATE_SEND_2.replace(second=0))
|
||||
|
||||
# One minute later, we can send again
|
||||
DATE_SEND_3 = datetime(2025, 1, 1, 20, 6, 23)
|
||||
processed = (mails_user_1 | new_mails_user_1).filtered(
|
||||
lambda m: not m.scheduled_date or m.scheduled_date <= DATE_SEND_3)
|
||||
with (
|
||||
self.mock_smtplib_connection(),
|
||||
self.mock_datetime_and_now(DATE_SEND_3),
|
||||
self.assert_mail_sent_then_scheduled(processed, 10, 5, DATE_SEND_3),
|
||||
):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
|
||||
self.assertEqual(mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
self.assertEqual(mail_server_1.owner_limit_time, DATE_SEND_3.replace(second=0))
|
||||
|
||||
# The CRON run in one minute later, we can 5 more emails
|
||||
DATE_SEND_5 = datetime(2025, 1, 1, 20, 7, 23)
|
||||
with (
|
||||
self.mock_smtplib_connection(),
|
||||
self.mock_datetime_and_now(DATE_SEND_5),
|
||||
self.assert_mail_sent_then_scheduled(mails_user_1, 15, 5, DATE_SEND_5),
|
||||
):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
|
||||
self.assertEqual(mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
self.assertEqual(mail_server_1.owner_limit_time, DATE_SEND_5.replace(second=0))
|
||||
|
||||
# The CRON is late compared to the scheduled mails,
|
||||
# it should re-schedule the mails, starting from the current time
|
||||
# Should send in priority the old mails
|
||||
DATE_SEND_6 = datetime(2025, 1, 1, 20, 25, 23)
|
||||
with (
|
||||
self.mock_smtplib_connection(),
|
||||
self.mock_datetime_and_now(DATE_SEND_6),
|
||||
self.assert_mail_sent_then_scheduled(mails_user_1, 19, 5, DATE_SEND_6),
|
||||
):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
|
||||
self.assertEqual(mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
self.assertEqual(mail_server_1.owner_limit_time, DATE_SEND_6.replace(second=0))
|
||||
|
||||
# Finish sending the email
|
||||
for i in range(2):
|
||||
DATE_SEND_7 = datetime(2025, 1, 1, 20, 26 + i, 23)
|
||||
with self.mock_smtplib_connection(), self.mock_datetime_and_now(DATE_SEND_7):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEqual(mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
self.assertEqual(mail_server_1.owner_limit_time, DATE_SEND_7.replace(second=0))
|
||||
|
||||
DATE_SEND_8 = datetime(2025, 1, 1, 20, 28, 23)
|
||||
with self.mock_smtplib_connection(), self.mock_datetime_and_now(DATE_SEND_8):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEqual(mail_server_1.owner_limit_count, 4)
|
||||
self.assertEqual(mail_server_1.owner_limit_time, DATE_SEND_8.replace(second=0))
|
||||
|
||||
# We send 4 emails this minute, check that will send 1 and schedule the remaining
|
||||
new_mails_user_1 = self.env["mail.mail"].with_user(user_1).sudo().create([
|
||||
{'state': 'outgoing', 'email_to': 'target@test.com', 'email_from': user_1.email}
|
||||
for i in range(TEST_LIMIT)
|
||||
])
|
||||
DATE_SEND_9 = datetime(2025, 1, 1, 20, 28, 23)
|
||||
with (
|
||||
self.mock_smtplib_connection(),
|
||||
self.mock_datetime_and_now(DATE_SEND_9),
|
||||
self.assert_mail_sent_then_scheduled(new_mails_user_1, len(new_mails_user_1), 1, DATE_SEND_9),
|
||||
):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEqual(mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
self.assertEqual(mail_server_1.owner_limit_time, DATE_SEND_9.replace(second=0))
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_personal_mail_server_limit_many_recipient(self):
|
||||
# Test with many recipients (should split the mail)
|
||||
TEST_LIMIT = 5
|
||||
self.env['ir.config_parameter'].set_param('mail.server.personal.limit.minutes', str(TEST_LIMIT))
|
||||
|
||||
partners = self.env['res.partner'].create([
|
||||
{'name': f'Partner {i}', 'email': f'partner_{i}@test.com'}
|
||||
for i in range(16)
|
||||
])
|
||||
with self.mock_datetime_and_now("2025-01-01 20:30:23"):
|
||||
email_to = '"Named To1" <to.1@test.com>, "Named To2" <to.1@test.com>'
|
||||
mail = self.env["mail.mail"].with_user(self.user_employee).sudo().create({
|
||||
'email_from': self.user_employee.email,
|
||||
'email_cc': '"Named Cc1" <cc.1@test.com>, "Named Cc2" <cc.2@test.com>',
|
||||
'email_to': email_to,
|
||||
'headers': '{"test": "test header"}',
|
||||
'recipient_ids': partners.ids,
|
||||
'state': 'outgoing',
|
||||
})
|
||||
|
||||
with self.mock_smtplib_connection(), self.mock_datetime_and_now("2025-01-01 20:31:23"):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
|
||||
self.assertEqual(self.mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
mails = self.env["mail.mail"].search(
|
||||
[('mail_message_id', '=', mail.mail_message_id.id)],
|
||||
order='create_date DESC, id DESC',
|
||||
)
|
||||
self.assertEqual(len(mails), 2)
|
||||
|
||||
# Only one mail preserved the email_to
|
||||
self.assertEqual(mails.mapped('email_to'), [email_to, False])
|
||||
|
||||
# Should preserve the header
|
||||
self.assertEqual(len(set(mails.mapped("headers"))), 1)
|
||||
self.assertEqual({"test": "test header"}, json.loads(mails[0].headers))
|
||||
|
||||
self.assertEqual(mails.mapped('state'), ['sent', 'outgoing'])
|
||||
outgoing = mails.filtered(lambda m: m.state == 'outgoing')
|
||||
self.assertEqual(len(outgoing), 1)
|
||||
self.assertFalse(outgoing.email_to)
|
||||
self.assertEqual(outgoing.state, 'outgoing')
|
||||
self.assertEqual(outgoing.create_uid, self.user_employee)
|
||||
self.assertEqual(len(outgoing.recipient_ids), 16 - TEST_LIMIT)
|
||||
|
||||
# Re-send the same minute, nothing change
|
||||
with self.mock_smtplib_connection(), self.mock_datetime_and_now("2025-01-01 20:31:33"):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEqual(self.mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
mails = self.env["mail.mail"].search([('mail_message_id', '=', mail.mail_message_id.id)])
|
||||
self.assertEqual(len(mails), 2)
|
||||
self.assertEqual(outgoing.state, 'outgoing')
|
||||
self.assertEqual(outgoing.create_uid, self.user_employee)
|
||||
self.assertEqual(len(outgoing.recipient_ids), 16 - TEST_LIMIT)
|
||||
|
||||
# Re-send one minute later
|
||||
with self.mock_smtplib_connection(), self.mock_datetime_and_now("2025-01-01 20:32:27"):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEqual(self.mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
mails = self.env["mail.mail"].search([('mail_message_id', '=', mail.mail_message_id.id)])
|
||||
self.assertEqual(sorted(mails.mapped('state')), ['outgoing', 'sent', 'sent'])
|
||||
outgoing = mails.filtered(lambda m: m.state == 'outgoing')
|
||||
self.assertEqual(len(outgoing), 1)
|
||||
self.assertFalse(outgoing.email_to)
|
||||
self.assertEqual(outgoing.state, 'outgoing')
|
||||
self.assertEqual(outgoing.create_uid, self.user_employee)
|
||||
self.assertEqual(len(outgoing.recipient_ids), 16 - 2 * TEST_LIMIT)
|
||||
|
||||
# The user re-send emails, while some emails are still in the queue
|
||||
# We have now 2 mails with many recipients to process in the queue
|
||||
with self.mock_datetime_and_now("2025-01-01 20:32:29"):
|
||||
other_mail = self.env["mail.mail"].with_user(self.user_employee).sudo().create({
|
||||
'email_from': self.user_employee.email,
|
||||
'email_to': 'target@test.com',
|
||||
'headers': '{"test": "test header"}',
|
||||
'recipient_ids': partners[:7].ids,
|
||||
'state': 'outgoing',
|
||||
})
|
||||
|
||||
with self.mock_smtplib_connection(), self.mock_datetime_and_now("2025-01-01 20:37:29"):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEqual(self.mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
self.assertEqual(other_mail.state, 'outgoing')
|
||||
mails = self.env["mail.mail"].search(
|
||||
[('mail_message_id', 'in', (mail.mail_message_id.id, other_mail.mail_message_id.id))],
|
||||
order='create_date DESC, id DESC',
|
||||
)
|
||||
self.assertEqual(sorted(mails.mapped('state')), ['outgoing', 'outgoing', 'sent', 'sent', 'sent'])
|
||||
outgoing_1 = mails[-1]
|
||||
self.assertFalse(outgoing_1.email_to)
|
||||
self.assertEqual(outgoing_1.state, 'outgoing')
|
||||
self.assertEqual(outgoing_1.create_uid, self.user_employee)
|
||||
self.assertEqual(len(outgoing_1.recipient_ids), 16 - 3 * TEST_LIMIT) # 1 recipient left
|
||||
|
||||
outgoing_2 = mails[1]
|
||||
self.assertEqual(outgoing_2.email_to, 'target@test.com')
|
||||
self.assertEqual(outgoing_2.state, 'outgoing')
|
||||
self.assertEqual(outgoing_2.create_uid, self.user_employee)
|
||||
self.assertEqual(len(outgoing_2.recipient_ids), 7)
|
||||
|
||||
# The next CRON will send all remaining emails of the first mail (1), and 4 mails for the second one
|
||||
# and schedule the 3 last (7 recipients - 3 sent)
|
||||
with self.mock_smtplib_connection(), self.mock_datetime_and_now("2025-01-01 20:39:29"):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEqual(self.mail_server_1.owner_limit_count, TEST_LIMIT)
|
||||
mails = self.env["mail.mail"].search([('mail_message_id', 'in', (mail.mail_message_id.id, other_mail.mail_message_id.id))])
|
||||
self.assertEqual(
|
||||
sorted(mails.mapped('state')),
|
||||
['outgoing', 'sent', 'sent', 'sent', 'sent', 'sent'],
|
||||
)
|
||||
|
||||
outgoing = mails.filtered(lambda m: m.state == 'outgoing')
|
||||
self.assertEqual(len(outgoing), 1)
|
||||
self.assertFalse(outgoing.email_to)
|
||||
self.assertEqual(outgoing.state, 'outgoing')
|
||||
self.assertEqual(outgoing.create_uid, self.user_employee)
|
||||
self.assertEqual(len(outgoing.recipient_ids), 3)
|
||||
|
||||
# Send the last email
|
||||
with self.mock_smtplib_connection(), self.mock_datetime_and_now("2025-01-01 20:42:29"):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
self.assertEqual(self.mail_server_1.owner_limit_count, 3)
|
||||
mails = self.env["mail.mail"].search([('mail_message_id', 'in', (mail.mail_message_id.id, other_mail.mail_message_id.id))])
|
||||
self.assertEqual(set(mails.mapped('state')), {'sent'})
|
||||
162
odoo-bringout-oca-ocb-mail/mail/tests/test_ir_ui_menu.py
Normal file
162
odoo-bringout-oca-ocb-mail/mail/tests/test_ir_ui_menu.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tests.common import warmup
|
||||
|
||||
|
||||
@tagged('mail_thread')
|
||||
class TestMenuRootLookupByModel(MailCommon):
|
||||
""" Test the determination of the best menu root for a given model.
|
||||
|
||||
When sharing a record through a link, it doesn't contain the menu context
|
||||
(menu root). To help the user, we try to restore a root menu related to
|
||||
the record when redirecting to the record from the link. That's what is tested
|
||||
here. For more details see IrUiMenu._get_best_backend_root_menu_id_for_model.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
""" Setup data for the tests, especially this menu hierarchy:
|
||||
- Contacts
|
||||
- Contacts (res.partner)
|
||||
- Invoicing
|
||||
- Customers
|
||||
- Customers (res.partner)
|
||||
- Companies (res.company)
|
||||
- Sales
|
||||
- Orders
|
||||
- Customers (res.partner)
|
||||
- Settings
|
||||
- Users & Companies
|
||||
- Companies (res.company)
|
||||
"""
|
||||
super().setUpClass()
|
||||
Menu = cls.env['ir.ui.menu']
|
||||
Action = cls.env['ir.actions.act_window']
|
||||
|
||||
def _new_menu(name, parent_id=None, action=None):
|
||||
menu = Menu.create({'name': name, 'parent_id': parent_id})
|
||||
if action:
|
||||
menu.action = action
|
||||
return menu
|
||||
|
||||
new_menu = Mock(side_effect=_new_menu)
|
||||
|
||||
def new_action(name, res_model, path=None, view_mode=None, domain=None, context=None):
|
||||
return Action.create({
|
||||
'name': name,
|
||||
'res_model': res_model,
|
||||
'path': path,
|
||||
'view_mode': view_mode,
|
||||
'domain': domain,
|
||||
'context': str(context) if context else {},
|
||||
'type': 'ir.actions.act_window',
|
||||
})
|
||||
|
||||
# Remove all menus and setup test menu with known results
|
||||
Menu.search([]).unlink()
|
||||
|
||||
menu_root_contact = new_menu('Contacts')
|
||||
new_menu('Contacts', parent_id=menu_root_contact.id,
|
||||
action=new_action(
|
||||
'Contacts', res_model='res.partner',
|
||||
path='all-contacts',
|
||||
view_mode='kanban,list,form,activity',
|
||||
context={'default_is_company': True},
|
||||
domain='[]'))
|
||||
menu_root_invoicing = new_menu('Invoicing')
|
||||
new_menu('Companies', parent_id=menu_root_invoicing.id,
|
||||
action=new_action(
|
||||
'Companies', res_model='res.company',
|
||||
view_mode='list,kanban,form'))
|
||||
menu_invoicing_customer = new_menu('Customers', parent_id=menu_root_invoicing.id)
|
||||
new_menu('Customers', parent_id=menu_invoicing_customer.id,
|
||||
action=new_action(
|
||||
'Customers', res_model='res.partner',
|
||||
path='all-customers',
|
||||
view_mode='kanban,list,form',
|
||||
context={
|
||||
'search_default_customer': 1,
|
||||
'res_partner_search_mode': 'customer',
|
||||
'default_is_company': True,
|
||||
'default_customer_rank': 1,
|
||||
}))
|
||||
menu_root_sales = new_menu('Sales')
|
||||
menu_sales_orders = new_menu('Orders', parent_id=menu_root_sales.id)
|
||||
new_menu('Customers', parent_id=menu_sales_orders.id,
|
||||
action=new_action(
|
||||
'Customers', res_model='res.partner',
|
||||
view_mode='kanban,list,form',
|
||||
context={
|
||||
'search_default_customer': 1,
|
||||
'res_partner_search_mode': 'customer',
|
||||
'default_is_company': True,
|
||||
'default_customer_rank': 1,
|
||||
}))
|
||||
menu_root_settings = new_menu('Settings')
|
||||
menu_settings_user_and_companies = new_menu(
|
||||
'Users & Companies', parent_id=menu_root_settings.id)
|
||||
new_menu('Companies', parent_id=menu_settings_user_and_companies.id,
|
||||
action=new_action(
|
||||
'Companies', res_model='res.company',
|
||||
path='all-companies',
|
||||
view_mode='list,kanban,form'))
|
||||
|
||||
cls.menu_count = new_menu.call_count
|
||||
cls.menu_root_contact = menu_root_contact
|
||||
cls.menu_root_sales = menu_root_sales
|
||||
cls.menu_root_settings = menu_root_settings
|
||||
|
||||
cls.user_public = mail_new_test_user(
|
||||
cls.env, login='user_public', groups='base.group_public', name='Bert Tartignole')
|
||||
cls.user_portal = mail_new_test_user(
|
||||
cls.env, login='user_portal', groups='base.group_portal', name='Chell Gladys')
|
||||
|
||||
def patch_get_backend_root_menu_ids(self, model, return_values):
|
||||
return patch.object(model.__class__, '_get_backend_root_menu_ids', return_value=return_values)
|
||||
|
||||
def test_initial_data(self):
|
||||
self.assertEqual(len(self.env['ir.ui.menu']._visible_menu_ids()), self.menu_count)
|
||||
|
||||
@warmup
|
||||
@users('employee')
|
||||
def test_look_for_existing_menu_root_user_with_access(self):
|
||||
Menu = self.env['ir.ui.menu']
|
||||
with (self.patch_get_backend_root_menu_ids(self.env['res.company'], []),
|
||||
self.assertQueryCount(employee=2)):
|
||||
# Auto-detection: the menu root with a sub-menu having an action with a path is selected
|
||||
self.assertEqual(Menu._get_best_backend_root_menu_id_for_model('res.company'),
|
||||
self.menu_root_settings.id)
|
||||
# Second time cache is used, so we got 2 queries less
|
||||
for idx, (return_values, expected_menu_root_id) in enumerate((
|
||||
# Auto-detection
|
||||
([], self.menu_root_contact.id),
|
||||
# Menu root defined by the model (and inheritance of), take the first one i.e. the least specific
|
||||
([self.menu_root_sales.id], self.menu_root_sales.id),
|
||||
([self.menu_root_sales.id, self.menu_root_contact.id], self.menu_root_sales.id),
|
||||
([self.menu_root_contact.id, self.menu_root_sales.id], self.menu_root_contact.id),
|
||||
)):
|
||||
with (self.patch_get_backend_root_menu_ids(self.env['res.partner'], return_values),
|
||||
self.assertQueryCount(employee=0)):
|
||||
self.assertEqual(Menu._get_best_backend_root_menu_id_for_model('res.partner'), expected_menu_root_id)
|
||||
|
||||
@warmup
|
||||
@users('user_portal', 'user_public')
|
||||
def test_look_for_existing_menu_root_user_no_access(self):
|
||||
Menu = self.env['ir.ui.menu']
|
||||
with self.assertQueryCount(user_portal=2, user_public=2):
|
||||
self.assertEqual(Menu._get_best_backend_root_menu_id_for_model('res.partner'), None)
|
||||
with self.assertQueryCount(user_portal=1, user_public=1):
|
||||
self.assertEqual(Menu._get_best_backend_root_menu_id_for_model('res.company'), None)
|
||||
with (self.patch_get_backend_root_menu_ids(
|
||||
self.env['res.partner'], [self.menu_root_sales.id, self.menu_root_contact.id]),
|
||||
self.assertQueryCount(user_portal=1, user_public=1)):
|
||||
self.assertEqual(Menu._get_best_backend_root_menu_id_for_model('res.partner'), None)
|
||||
|
||||
@warmup
|
||||
def test_look_for_non_existing_menu_root(self):
|
||||
with (self.patch_get_backend_root_menu_ids(self.env['res.bank'], []),
|
||||
self.assertQueryCount(__system__=2)):
|
||||
self.assertEqual(self.env['ir.ui.menu']._get_best_backend_root_menu_id_for_model('res.bank'), None)
|
||||
92
odoo-bringout-oca-ocb-mail/mail/tests/test_ir_websocket.py
Normal file
92
odoo-bringout-oca-ocb-mail/mail/tests/test_ir_websocket.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
try:
|
||||
import websocket as ws
|
||||
except ImportError:
|
||||
ws = None
|
||||
|
||||
from odoo.tests import new_test_user, tagged
|
||||
from odoo.addons.bus.tests.common import WebsocketCase
|
||||
from odoo.addons.bus.models.bus import channel_with_db, json_dump
|
||||
from odoo.addons.mail.models.mail_presence import AWAY_TIMER
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestIrWebsocket(WebsocketCase):
|
||||
def test_notify_on_status_change(self):
|
||||
bob = new_test_user(self.env, login="bob_user", groups="base.group_user")
|
||||
session = self.authenticate("bob_user", "bob_user")
|
||||
websocket = self.websocket_connect(cookie=f"session_id={session.sid};")
|
||||
self.subscribe(
|
||||
websocket,
|
||||
[f"odoo-presence-res.partner_{bob.partner_id.id}"],
|
||||
self.env["bus.bus"]._bus_last_id(),
|
||||
)
|
||||
# offline => online
|
||||
self.env["mail.presence"]._update_presence(bob)
|
||||
self.trigger_notification_dispatching([(bob.partner_id, "presence")])
|
||||
message = json.loads(websocket.recv())[0]["message"]
|
||||
self.assertEqual(message["type"], "bus.bus/im_status_updated")
|
||||
self.assertEqual(message["payload"]["im_status"], "online")
|
||||
self.assertEqual(message["payload"]["presence_status"], "online")
|
||||
self.assertEqual(message["payload"]["partner_id"], bob.partner_id.id)
|
||||
# online => away
|
||||
away_timer_later = datetime.now() + timedelta(seconds=AWAY_TIMER + 1)
|
||||
with freeze_time(away_timer_later):
|
||||
self.env["mail.presence"]._update_presence(bob, (AWAY_TIMER + 1) * 1000)
|
||||
self.trigger_notification_dispatching([(bob.partner_id, "presence")])
|
||||
message = json.loads(websocket.recv())[0]["message"]
|
||||
self.assertEqual(message["type"], "bus.bus/im_status_updated")
|
||||
self.assertEqual(message["payload"]["im_status"], "away")
|
||||
self.assertEqual(message["payload"]["presence_status"], "away")
|
||||
self.assertEqual(message["payload"]["partner_id"], bob.partner_id.id)
|
||||
# away => online
|
||||
ten_minutes_later = datetime.now() + timedelta(minutes=10)
|
||||
with freeze_time(ten_minutes_later):
|
||||
self.env["mail.presence"]._update_presence(bob)
|
||||
self.trigger_notification_dispatching([(bob.partner_id, "presence")])
|
||||
message = json.loads(websocket.recv())[0]["message"]
|
||||
self.assertEqual(message["type"], "bus.bus/im_status_updated")
|
||||
self.assertEqual(message["payload"]["im_status"], "online")
|
||||
self.assertEqual(message["payload"]["presence_status"], "online")
|
||||
self.assertEqual(message["payload"]["partner_id"], bob.partner_id.id)
|
||||
# online => online, nothing happens
|
||||
ten_minutes_later = datetime.now() + timedelta(minutes=10)
|
||||
with freeze_time(ten_minutes_later):
|
||||
self.env["mail.presence"]._update_presence(bob)
|
||||
self.trigger_notification_dispatching([(bob.partner_id, "presence")])
|
||||
timeout_occurred = False
|
||||
# Save point rollback of `assertRaises` can compete with `on_websocket_close`
|
||||
# leading to `InvalidSavepoint` errors. We need to avoid it.
|
||||
try:
|
||||
websocket.recv()
|
||||
except ws._exceptions.WebSocketTimeoutException:
|
||||
timeout_occurred = True
|
||||
self.assertTrue(timeout_occurred)
|
||||
|
||||
def test_receive_missed_presences_on_subscribe(self):
|
||||
bob = new_test_user(self.env, login="bob_user", groups="base.group_user")
|
||||
session = self.authenticate("bob_user", "bob_user")
|
||||
websocket = self.websocket_connect(cookie=f"session_id={session.sid};")
|
||||
self.env["mail.presence"]._update_presence(bob)
|
||||
self.env.cr.precommit.run() # trigger the creation of bus.bus records
|
||||
self.subscribe(
|
||||
websocket,
|
||||
[f"odoo-presence-res.partner_{bob.partner_id.id}"],
|
||||
self.env["bus.bus"]._bus_last_id(),
|
||||
)
|
||||
self.trigger_notification_dispatching([(bob.partner_id, "presence")])
|
||||
notification = json.loads(websocket.recv())[0]
|
||||
self._close_websockets()
|
||||
bus_record = self.env["bus.bus"].search([("id", "=", int(notification["id"]))])
|
||||
self.assertEqual(
|
||||
bus_record.channel,
|
||||
json_dump(channel_with_db(self.env.cr.dbname, bob.partner_id)),
|
||||
)
|
||||
self.assertEqual(notification["message"]["type"], "bus.bus/im_status_updated")
|
||||
self.assertEqual(notification["message"]["payload"]["im_status"], "online")
|
||||
self.assertEqual(notification["message"]["payload"]["presence_status"], "online")
|
||||
self.assertEqual(notification["message"]["payload"]["partner_id"], bob.partner_id.id)
|
||||
|
|
@ -1,107 +1,295 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from functools import partial
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from markupsafe import Markup
|
||||
from unittest.mock import patch
|
||||
|
||||
import io
|
||||
import requests
|
||||
|
||||
mail_channel_new_test_user = partial(mail_new_test_user, context={'mail_channel_nosubscribe': False})
|
||||
|
||||
|
||||
def _patched_get_html(*args, **kwargs):
|
||||
response = requests.Response()
|
||||
response.status_code = 200
|
||||
response._content = b"""
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:title" content="Test title">
|
||||
<meta property="og:description" content="Test description">
|
||||
</head>
|
||||
</html>
|
||||
"""
|
||||
response.headers["Content-Type"] = 'text/html'
|
||||
return response
|
||||
|
||||
def _patch_head_html(*args, **kwargs):
|
||||
response = requests.Response()
|
||||
response.status_code = 200
|
||||
response.headers["Content-Type"] = 'text/html'
|
||||
return response
|
||||
|
||||
|
||||
def _patch_no_content_type(*args, **kwargs):
|
||||
response = requests.Response()
|
||||
response.status_code = 200
|
||||
return response
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.addons.mail.tools import link_preview
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
@tagged("mail_link_preview", "mail_message", "post_install", "-at_install")
|
||||
class TestLinkPreview(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user_1 = mail_channel_new_test_user(
|
||||
cls.env, login='user_1',
|
||||
name='User 1',
|
||||
groups='base.group_user')
|
||||
cls.maxDiff = None
|
||||
cls.test_partner = cls.env['res.partner'].create({'name': 'a partner'})
|
||||
cls.existing_message = cls.test_partner.message_post(body='Test')
|
||||
cls.title = 'Test title'
|
||||
cls.og_title = 'Le carousel ne démarre pas.webm'
|
||||
cls.og_description = 'Test OG description'
|
||||
cls.og_image = 'https://dummy-image-url.nothing'
|
||||
cls.source_url = 'https://thisdomainedoentexist.nothing'
|
||||
|
||||
cls.public_channel = cls.env['mail.channel'].create({
|
||||
'name': 'Public channel of user 1',
|
||||
'channel_type': 'channel',
|
||||
})
|
||||
cls.public_channel.channel_member_ids.unlink()
|
||||
def _patch_head_html(self, *args, **kwargs):
|
||||
response = requests.Response()
|
||||
response.status_code = 200
|
||||
response.headers["Content-Type"] = 'text/html'
|
||||
return response
|
||||
|
||||
def test_01_link_preview_throttle(self):
|
||||
with patch.object(requests.Session, 'get', _patched_get_html), patch.object(requests.Session, 'head', _patch_head_html):
|
||||
throttle = int(self.env['ir.config_parameter'].sudo().get_param('mail.link_preview_throttle', 99))
|
||||
link_previews = []
|
||||
for _ in range(throttle):
|
||||
link_previews.append({'source_url': 'https://thisdomainedoentexist.nothing', 'message_id': 1})
|
||||
self.env['mail.link.preview'].create(link_previews)
|
||||
message = self.env['mail.message'].create({
|
||||
'model': 'mail.channel',
|
||||
'res_id': self.public_channel.id,
|
||||
'body': '<a href="https://thisdomainedoentexist.nothing">Nothing link</a>',
|
||||
})
|
||||
self.env['mail.link.preview']._create_link_previews(message)
|
||||
link_preview_count = self.env['mail.link.preview'].search_count([('source_url', '=', 'https://thisdomainedoentexist.nothing')])
|
||||
self.assertEqual(link_preview_count, throttle + 1)
|
||||
def _patched_get_html(self, content_type, content):
|
||||
response = requests.Response()
|
||||
response.status_code = 200
|
||||
response._content = content
|
||||
response.encoding = 'utf-8'
|
||||
# To handle chunks read on stream requests
|
||||
response.raw = io.BytesIO(response._content)
|
||||
response.headers["Content-Type"] = content_type
|
||||
return response
|
||||
|
||||
def test_02_link_preview_create(self):
|
||||
with patch.object(requests.Session, 'get', _patched_get_html), patch.object(requests.Session, 'head', _patch_head_html):
|
||||
message = self.env['mail.message'].create({
|
||||
'model': 'mail.channel',
|
||||
'res_id': self.public_channel.id,
|
||||
'body': '<a href="https://thisdomainedoentexist.nothing">Nothing link</a>',
|
||||
})
|
||||
self.env['mail.link.preview']._create_link_previews(message)
|
||||
self.assertBusNotifications(
|
||||
[(self.cr.dbname, 'mail.channel', self.public_channel.id)],
|
||||
message_items=[{
|
||||
'type': 'mail.link.preview/insert',
|
||||
'payload': [{
|
||||
'id': link_preview.id,
|
||||
'message': {'id': message.id},
|
||||
'image_mimetype': False,
|
||||
'og_description': 'Test description',
|
||||
'og_image': False,
|
||||
'og_mimetype': False,
|
||||
'og_title': 'Test title',
|
||||
'og_type': False,
|
||||
'source_url': 'https://thisdomainedoentexist.nothing',
|
||||
} for link_preview in message.link_preview_ids]
|
||||
}]
|
||||
def _patch_with_og_properties(self, *args, **kwargs):
|
||||
content = b"""
|
||||
<html>
|
||||
<head>
|
||||
<title>Test title</title>
|
||||
<meta property="og:title" content="Le carousel ne d\xc3\xa9marre pas.webm">
|
||||
<meta property="og:description" content="Test OG description">
|
||||
<meta property="og:image" content="https://dummy-image-url.nothing">
|
||||
</head>
|
||||
</html>
|
||||
"""
|
||||
return self._patched_get_html('text/html', content)
|
||||
|
||||
def _patch_without_og_properties(self, *args, **kwargs):
|
||||
content = b"""
|
||||
<html>
|
||||
<head>
|
||||
<title>Test title</title>
|
||||
</head>
|
||||
</html>
|
||||
"""
|
||||
return self._patched_get_html('text/html', content)
|
||||
|
||||
def _patch_with_image_mimetype(self, *args, **kwargs):
|
||||
content = b"""
|
||||
<html>
|
||||
<body>
|
||||
<img src='https://dummy-image-url.nothing'/>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return self._patched_get_html('image/png', content)
|
||||
|
||||
def _patch_with_no_content_type(self, *args, **kwargs):
|
||||
content = b""""""
|
||||
return self._patched_get_html(None, content)
|
||||
|
||||
def _patch_with_xml_declaration(self, *args, **kwargs):
|
||||
content = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test title</title>
|
||||
</head>
|
||||
</html>
|
||||
"""
|
||||
return self._patched_get_html("text/html", content)
|
||||
|
||||
def test_get_link_preview_from_url(self):
|
||||
test_cases = [
|
||||
(self._patch_with_og_properties, self.source_url),
|
||||
(self._patch_without_og_properties, self.source_url),
|
||||
(self._patch_with_image_mimetype, self.og_image),
|
||||
(self._patch_with_xml_declaration, self.source_url)
|
||||
]
|
||||
expected_values = [
|
||||
{
|
||||
'og_description': self.og_description,
|
||||
'og_image': self.og_image,
|
||||
'og_mimetype': None,
|
||||
'og_title': self.og_title,
|
||||
'og_type': None,
|
||||
'og_site_name': None,
|
||||
'source_url': self.source_url,
|
||||
},
|
||||
{
|
||||
'og_description': None,
|
||||
'og_image': None,
|
||||
'og_mimetype': None,
|
||||
'og_title': self.title,
|
||||
'og_type': None,
|
||||
'og_site_name': None,
|
||||
'source_url': self.source_url,
|
||||
},
|
||||
{
|
||||
'image_mimetype': 'image/png',
|
||||
'og_image': self.og_image,
|
||||
'source_url': self.og_image,
|
||||
},
|
||||
{
|
||||
'og_description': None,
|
||||
'og_image': None,
|
||||
'og_mimetype': None,
|
||||
'og_title': self.title,
|
||||
'og_type': None,
|
||||
'og_site_name': None,
|
||||
'source_url': self.source_url,
|
||||
}
|
||||
]
|
||||
session = requests.Session()
|
||||
for (get_patch, url), expected in zip(test_cases, expected_values):
|
||||
with self.subTest(get_patch=get_patch, url=url, expected=expected), patch.object(requests.Session, 'get', get_patch):
|
||||
preview = link_preview.get_link_preview_from_url(url, session)
|
||||
self.assertEqual(preview, expected)
|
||||
|
||||
def test_link_preview(self):
|
||||
with patch.object(requests.Session, 'get', self._patch_with_og_properties), patch.object(requests.Session, 'head', self._patch_head_html):
|
||||
message = self.test_partner.message_post(
|
||||
body=Markup(f'<a href={self.source_url}>Nothing link</a>'),
|
||||
)
|
||||
|
||||
def test_03_link_preview_create_no_content_type(self):
|
||||
with patch.object(requests.Session, 'request', _patch_no_content_type):
|
||||
message = self.env['mail.message'].create({
|
||||
'model': 'mail.channel',
|
||||
'res_id': self.public_channel.id,
|
||||
'body': '<a href="https://thisdomainedoentexist.nothing">Nothing link</a>',
|
||||
})
|
||||
self.env['mail.link.preview']._create_link_previews(message)
|
||||
link_preview_count = self.env['mail.link.preview'].search_count([('source_url', '=', 'https://thisdomainedoentexist.nothing')])
|
||||
self.assertEqual(link_preview_count, 0)
|
||||
def get_bus_params():
|
||||
return (
|
||||
[(self.cr.dbname, "res.partner", self.env.user.partner_id.id)],
|
||||
[
|
||||
{
|
||||
"type": "mail.record/insert",
|
||||
"payload": {
|
||||
"mail.link.preview": [
|
||||
{
|
||||
"id": message.message_link_preview_ids.link_preview_id.id,
|
||||
"image_mimetype": False,
|
||||
"og_description": self.og_description,
|
||||
"og_image": self.og_image,
|
||||
"og_mimetype": False,
|
||||
"og_site_name": False,
|
||||
"og_title": self.og_title,
|
||||
"og_type": False,
|
||||
"source_url": self.source_url,
|
||||
},
|
||||
],
|
||||
"mail.message": self._filter_messages_fields(
|
||||
{
|
||||
"id": message.id,
|
||||
"message_link_preview_ids": message.message_link_preview_ids.ids,
|
||||
},
|
||||
),
|
||||
"mail.message.link.preview": [
|
||||
{
|
||||
"id": message.message_link_preview_ids.id,
|
||||
"link_preview_id": message.message_link_preview_ids.link_preview_id.id,
|
||||
"message_id": message.id,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
with self.assertBus(get_params=get_bus_params):
|
||||
self.env["mail.link.preview"]._create_from_message_and_notify(message)
|
||||
|
||||
def test_link_preview_no_content_type(self):
|
||||
with patch.object(requests.Session, 'request', self._patch_with_no_content_type):
|
||||
url = self.source_url
|
||||
session = requests.Session()
|
||||
link_preview.get_link_preview_from_url(url, session)
|
||||
|
||||
def test_link_preview_ignore_internal_link(self):
|
||||
"""Test internal links are properly ignored from link preview."""
|
||||
with patch.object(requests.Session, "get", self._patch_with_og_properties), patch.object(
|
||||
requests.Session, "head", self._patch_head_html
|
||||
):
|
||||
urls = [
|
||||
("http://localhost:8069/", "http://localhost:8069/odoo", 0),
|
||||
("http://localhost:8069/", "http://localhost:8069/odoo/test", 0),
|
||||
("http://localhost:8069/", "http://localhost:8069/web/test", 0),
|
||||
("http://localhost:8069/", "http://localhost:8069/", 1),
|
||||
("http://localhost:8069/", "http://localhost:8069/odoo-experience", 1),
|
||||
("http://localhost:8069/", "http://localhost:8069/chat/5/bFtIfYHRco", 0),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/web", 0),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/odoo", 0),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/odoo/", 0),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/odoo?debug=assets", 0),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/odoo#anchor", 0),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/odoo-experience", 1),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/odoo/1519/tasks/4102866", 0),
|
||||
("http://www.odoo.com/", "https://www.odoo.com/odoo/1519/tasks/4102866", 1),
|
||||
("https://www.odoo.com/", "https://wwwaodoo.com/odoo/", 1),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/chat/", 0),
|
||||
("https://www.odoo.com/", "https://www.odoo.com/chat/5/bFtIfYHRco", 0),
|
||||
("http://www.odoo.com/", "https://www.odoo.com/chat/5/bFtIfYHRco", 1),
|
||||
("https://clients.odoo.com/", "https://www.odoo.com/odoo/1519/tasks/4102866", 1),
|
||||
("https://clients.odoo.com/", "https://www.odoo.com/chat/5/bFtIfYHRco", 1),
|
||||
]
|
||||
for request_url, url, counter in urls:
|
||||
with self.subTest(request_url=request_url, url=url, counter=counter):
|
||||
message = self.test_partner.message_post(
|
||||
body=Markup(f'<a href="{url}">Nothing link</a>'),
|
||||
)
|
||||
self.env["mail.link.preview"]._create_from_message_and_notify(
|
||||
message, request_url
|
||||
)
|
||||
link_preview_count = self.env["mail.message.link.preview"].search_count(
|
||||
[("message_id", "=", message.id)]
|
||||
)
|
||||
self.assertEqual(link_preview_count, counter)
|
||||
|
||||
def test_remove_unused_link_preview(self):
|
||||
with (
|
||||
patch.object(requests.Session, "get", self._patch_with_og_properties),
|
||||
patch.object(requests.Session, "head", self._patch_head_html),
|
||||
):
|
||||
message = self.test_partner.message_post(
|
||||
body=Markup(
|
||||
'<a href="https://www.odoo.com/odoo-experience">Nothing link</a> <a href="https://www.odoo.com/odoo-experience-2025">Other Nothing link</a>'
|
||||
),
|
||||
message_type="comment",
|
||||
)
|
||||
self.env["mail.link.preview"]._create_from_message_and_notify(message)
|
||||
link_preview_count = self.env["mail.message.link.preview"].search_count(
|
||||
[("message_id", "=", message.id)]
|
||||
)
|
||||
self.assertEqual(link_preview_count, 2)
|
||||
self.test_partner._message_update_content(
|
||||
message,
|
||||
body=Markup('<a href="https://www.odoo.com/odoo-experience">Nothing link</a>'),
|
||||
)
|
||||
self.env["mail.link.preview"]._create_from_message_and_notify(message)
|
||||
link_preview_count = self.env["mail.message.link.preview"].search_count(
|
||||
[("message_id", "=", message.id)]
|
||||
)
|
||||
self.assertEqual(link_preview_count, 1)
|
||||
|
||||
def test_link_preview_throttle(self):
|
||||
self.env["ir.config_parameter"].sudo().set_param("mail.link_preview_throttle", 1)
|
||||
with (
|
||||
patch.object(requests.Session, "get", self._patch_with_og_properties),
|
||||
patch.object(requests.Session, "head", self._patch_head_html),
|
||||
):
|
||||
message = self.test_partner.message_post(
|
||||
body=Markup('<a href="%s">Nothing link</a>') % self.source_url,
|
||||
)
|
||||
self.env["mail.link.preview"]._create_from_message_and_notify(message)
|
||||
link_preview = (
|
||||
self.env["mail.message.link.preview"]
|
||||
.search([("message_id", "=", message.id)])
|
||||
.link_preview_id
|
||||
)
|
||||
message = self.test_partner.message_post(
|
||||
body=Markup(f'<a href="%s/test">Nothing link</a>') % self.source_url
|
||||
)
|
||||
self.env["mail.link.preview"]._create_from_message_and_notify(message)
|
||||
link_preview_count = self.env["mail.message.link.preview"].search_count(
|
||||
[("link_preview_id", "=", link_preview.id)]
|
||||
)
|
||||
self.assertEqual(link_preview_count, 1)
|
||||
|
||||
def test_link_preview_delete_with_message(self):
|
||||
with patch.object(requests.Session, "get", self._patch_with_og_properties), patch.object(
|
||||
requests.Session, "head", self._patch_head_html
|
||||
):
|
||||
message = self.test_partner.message_post(
|
||||
body=Markup('<a href="%s">Test link</a>') % self.source_url,
|
||||
message_type="comment"
|
||||
)
|
||||
self.env["mail.link.preview"]._create_from_message_and_notify(message)
|
||||
preview = message.message_link_preview_ids
|
||||
self.assertTrue(preview)
|
||||
self.assertEqual(preview.link_preview_id.source_url, self.source_url)
|
||||
self.test_partner._message_update_content(message, body="")
|
||||
self.assertFalse(message.message_link_preview_ids)
|
||||
|
|
|
|||
71
odoo-bringout-oca-ocb-mail/mail/tests/test_mail_activity.py
Normal file
71
odoo-bringout-oca-ocb-mail/mail/tests/test_mail_activity.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
from freezegun import freeze_time
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.addons.mail.tests.common_activity import ActivityScheduleCase
|
||||
from odoo.tests import tagged, HttpCase
|
||||
|
||||
|
||||
@tagged("mail_activity", "-at_install", "post_install")
|
||||
class TestMailActivityChatter(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.test_partner = cls.env['res.partner'].create({
|
||||
'email': 'test.partner@example.com',
|
||||
'name': 'Test User',
|
||||
})
|
||||
|
||||
def test_mail_activity_date_format(self):
|
||||
with freeze_time("2024-01-01 09:00:00 AM"):
|
||||
LANG_CODE = "en_US"
|
||||
self.env = self.env(context={"lang": LANG_CODE})
|
||||
lang = self.env["res.lang"].search([('code', '=', LANG_CODE)])
|
||||
lang.date_format = "%d/%b/%y"
|
||||
lang.time_format = "%I:%M:%S %p"
|
||||
|
||||
self.start_tour(
|
||||
f"/web#id={self.test_partner.id}&model=res.partner",
|
||||
"mail_activity_date_format",
|
||||
login="admin",
|
||||
)
|
||||
|
||||
def test_mail_activity_schedule_from_chatter(self):
|
||||
self.start_tour(
|
||||
f"/odoo/res.partner/{self.test_partner.id}",
|
||||
"mail_activity_schedule_from_chatter",
|
||||
login="admin",
|
||||
)
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "mail_activity")
|
||||
class TestMailActivityIntegrity(ActivityScheduleCase):
|
||||
|
||||
def test_mail_activity_type_master_data(self):
|
||||
""" Test master data integrity
|
||||
|
||||
* 'call', 'meeting', 'todo', 'upload document' and 'warning' should always be cross model;
|
||||
* 'call', 'meeting' and 'todo' cannot be removed
|
||||
"""
|
||||
call = self.env.ref('mail.mail_activity_data_call')
|
||||
meeting = self.env.ref('mail.mail_activity_data_meeting')
|
||||
todo = self.env.ref('mail.mail_activity_data_todo')
|
||||
upload = self.env.ref('mail.mail_activity_data_upload_document')
|
||||
warning = self.env.ref('mail.mail_activity_data_warning')
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
call.write({'res_model': 'res.partner'})
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
meeting.write({'res_model': 'res.partner'})
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
todo.write({'res_model': 'res.partner'})
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
upload.write({'res_model': 'res.partner'})
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
warning.write({'res_model': 'res.partner'})
|
||||
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
call.unlink()
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
meeting.unlink()
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
todo.unlink()
|
||||
41
odoo-bringout-oca-ocb-mail/mail/tests/test_mail_blacklist.py
Normal file
41
odoo-bringout-oca-ocb-mail/mail/tests/test_mail_blacklist.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestMailBlacklist(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.email = "archived@test.example.com"
|
||||
cls.partner = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Archived Blacklist Test",
|
||||
"email": cls.email,
|
||||
}
|
||||
)
|
||||
cls.blacklist_entry = cls.env["mail.blacklist"].create(
|
||||
{
|
||||
"email": cls.email,
|
||||
"active": False,
|
||||
}
|
||||
)
|
||||
|
||||
def test_is_blacklisted_ignores_archived(self):
|
||||
self.assertFalse(
|
||||
self.partner.is_blacklisted,
|
||||
"Archived blacklist entries should not mark partner as blacklisted",
|
||||
)
|
||||
|
||||
def test_is_blacklisted_active_entry(self):
|
||||
self.blacklist_entry.action_unarchive()
|
||||
self.assertTrue(
|
||||
self.partner.is_blacklisted,
|
||||
"Active blacklist entries should mark partner as blacklisted",
|
||||
)
|
||||
|
||||
def test_active_test_false_context_works(self):
|
||||
self.assertFalse(
|
||||
self.partner.with_context(active_test=False).is_blacklisted,
|
||||
"Always blacklisted, even if context searches for archived records",
|
||||
)
|
||||
|
|
@ -1,578 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.addons.mail.models.mail_channel import channel_avatar, group_avatar
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tests import tagged, Form
|
||||
from odoo.tests.common import users
|
||||
from odoo.tools import html_escape, mute_logger
|
||||
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
|
||||
@tagged('mail_channel')
|
||||
class TestChannelAccessRights(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestChannelAccessRights, cls).setUpClass()
|
||||
|
||||
cls.user_employee_1 = mail_new_test_user(cls.env, login='user_employee_1', groups='base.group_user', name='Tao Lee')
|
||||
cls.user_public = mail_new_test_user(cls.env, login='user_public', groups='base.group_public', name='Bert Tartignole')
|
||||
cls.user_portal = mail_new_test_user(cls.env, login='user_portal', groups='base.group_portal', name='Chell Gladys')
|
||||
|
||||
# Channel for certain group
|
||||
cls.group_restricted_channel = cls.env['mail.channel'].browse(cls.env['mail.channel'].channel_create(name='Channel for Groups', group_id=cls.env.ref('base.group_user').id)['id'])
|
||||
# Public Channel
|
||||
cls.public_channel = cls.env['mail.channel'].browse(cls.env['mail.channel'].channel_create(name='Public Channel', group_id=None)['id'])
|
||||
# Group
|
||||
cls.private_group = cls.env['mail.channel'].browse(cls.env['mail.channel'].create_group(partners_to=cls.user_employee.partner_id.ids, name="Group")['id'])
|
||||
# Chat
|
||||
cls.chat_user_employee = cls.env['mail.channel'].browse(cls.env['mail.channel'].channel_get(cls.user_employee.partner_id.ids)['id'])
|
||||
cls.chat_user_employee_1 = cls.env['mail.channel'].browse(cls.env['mail.channel'].channel_get(cls.user_employee_1.partner_id.ids)['id'])
|
||||
cls.chat_user_portal = cls.env['mail.channel'].browse(cls.env['mail.channel'].channel_get(cls.user_portal.partner_id.ids)['id'])
|
||||
cls.chat_user_public = cls.env['mail.channel'].browse(cls.env['mail.channel'].channel_get(cls.user_public.partner_id.ids)['id'])
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model', 'odoo.models')
|
||||
@users('user_public')
|
||||
def test_access_public(self):
|
||||
# Read public channel -> ok
|
||||
self.env['mail.channel'].browse(self.public_channel.id).read()
|
||||
|
||||
# Read group restricted channel -> ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.group_restricted_channel.id).read()
|
||||
# Read group -> ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.private_group.id).read()
|
||||
|
||||
# Being a member of public channel: -> ok
|
||||
self.public_channel.add_members(self.user_public.partner_id.id)
|
||||
# Being a member of group restricted channel: -> ko, no access rights
|
||||
with self.assertRaises(UserError):
|
||||
self.group_restricted_channel.add_members(self.user_public.partner_id.id)
|
||||
# Being a group member: -> ok
|
||||
self.private_group.add_members(self.user_public.partner_id.id)
|
||||
|
||||
# Read a group when being a member: -> ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.private_group.id).read()
|
||||
# Read a chat when being a member: -> ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.chat_user_public.id).read()
|
||||
|
||||
# Create channel/group/chat: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].create({'name': 'Test', 'channel_type': 'channel'})
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].create({'name': 'Test', 'channel_type': 'group'})
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].create({'name': 'Test', 'channel_type': 'chat'})
|
||||
|
||||
# Update channel/group/chat: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.public_channel.id).write({'name': 'modified'})
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.group_restricted_channel.id).write({'name': 'modified'})
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.private_group.id).write({'name': 'modified'})
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.chat_user_public.id).write({'name': 'modified'})
|
||||
|
||||
# Unlink channel/group/chat: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.public_channel.id).unlink()
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.group_restricted_channel.id).unlink()
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.private_group.id).unlink()
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.chat_user_public.id).unlink()
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model', 'odoo.models')
|
||||
@users('employee')
|
||||
def test_access_employee(self):
|
||||
# Read public channel -> ok
|
||||
self.env['mail.channel'].browse(self.public_channel.id).read()
|
||||
# Read group restricted channel -> ok
|
||||
self.env['mail.channel'].browse(self.group_restricted_channel.id).read()
|
||||
# Read chat when not being a member: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.chat_user_employee_1.id).read()
|
||||
|
||||
# Update chat when not being a member: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.chat_user_employee_1.id).write({'name': 'modified'})
|
||||
|
||||
# Being a channel/group member: -> ok
|
||||
self.public_channel.add_members(self.user_public.partner_id.id)
|
||||
self.group_restricted_channel.add_members(self.env.user.partner_id.id)
|
||||
self.private_group.add_members(self.env.user.partner_id.id)
|
||||
|
||||
# Read a group when being a member: ok
|
||||
self.env['mail.channel'].browse(self.private_group.id).read()
|
||||
# Read a chat when being a member: ok
|
||||
self.env['mail.channel'].browse(self.chat_user_employee.id).read()
|
||||
|
||||
# Update channel/group/chat when being a member: ok
|
||||
self.env['mail.channel'].browse(self.public_channel.id).write({'name': 'modified again'})
|
||||
self.env['mail.channel'].browse(self.group_restricted_channel.id).write({'name': 'modified again'})
|
||||
self.env['mail.channel'].browse(self.private_group.id).write({'name': 'modified again'})
|
||||
self.env['mail.channel'].browse(self.chat_user_employee.id).write({'name': 'modified again'})
|
||||
|
||||
# Create channel/group/chat: ok
|
||||
new_channel = self.env['mail.channel'].create(
|
||||
{'name': 'Test', 'channel_type': 'channel'})
|
||||
new_group = self.env['mail.channel'].create(
|
||||
{'name': 'Test', 'channel_type': 'group'})
|
||||
new_chat = self.env['mail.channel'].create(
|
||||
{'name': 'Test', 'channel_type': 'chat'})
|
||||
|
||||
# Employee should be inside the created chat/group/chat
|
||||
self.assertIn(new_channel.channel_partner_ids, self.partner_employee)
|
||||
self.assertIn(new_group.channel_partner_ids, self.partner_employee)
|
||||
self.assertIn(new_chat.channel_partner_ids, self.partner_employee)
|
||||
|
||||
# Unlink channel/group/chat: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.public_channel.id).unlink()
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.group_restricted_channel.id).unlink()
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.private_group.id).unlink()
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.chat_user_employee.id).unlink()
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.base.models.ir_model', 'odoo.models')
|
||||
@users('user_portal')
|
||||
def test_access_portal(self):
|
||||
# Read public channel -> ok
|
||||
self.env['mail.channel'].browse(self.public_channel.id).read()
|
||||
# Read group restricted channel/group -> ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.group_restricted_channel.id).read()
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.private_group.id).read()
|
||||
|
||||
# Being a group member: -> ok
|
||||
self.private_group.add_members(self.user_portal.partner_id.id)
|
||||
|
||||
# Read a group/chat when being a member: ok
|
||||
self.env['mail.channel'].browse(self.private_group.id).read()
|
||||
self.env['mail.channel'].browse(self.chat_user_portal.id).read()
|
||||
|
||||
# Update group/chat when being a member: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.private_group.id).write({'name': 'modified'})
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.chat_user_portal.id).write({'name': 'modified'})
|
||||
|
||||
# Create group/chat: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].create({'name': 'Test', 'channel_type': 'group'})
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].create({'name': 'Test', 'channel_type': 'chat'})
|
||||
|
||||
# Unlink group/chat: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.private_group.id).unlink()
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel'].browse(self.chat_user_portal.id).unlink()
|
||||
|
||||
# Read message from group/chat: ok
|
||||
group_portal = self.env['mail.channel'].browse(self.private_group.id)
|
||||
for message in group_portal.message_ids:
|
||||
message.read(['subject'])
|
||||
chat_portal = self.env['mail.channel'].browse(self.chat_user_portal.id)
|
||||
for message in chat_portal.message_ids:
|
||||
message.read(['subject'])
|
||||
|
||||
# Read partner list from group: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
group_portal.message_partner_ids
|
||||
for partner in self.private_group.message_partner_ids:
|
||||
if partner.id == self.user_portal.partner_id.id:
|
||||
# Portal user can read their own partner record
|
||||
continue
|
||||
with self.assertRaises(AccessError):
|
||||
partner.with_user(self.user_portal).name
|
||||
|
||||
# Read partner list from chat: ko, no access rights
|
||||
with self.assertRaises(AccessError):
|
||||
chat_portal.message_partner_ids
|
||||
for partner in self.chat_user_portal.message_partner_ids:
|
||||
if partner.id == self.user_portal.partner_id.id:
|
||||
# Portal user can read their own partner record
|
||||
continue
|
||||
with self.assertRaises(AccessError):
|
||||
partner.with_user(self.user_portal).name
|
||||
|
||||
|
||||
@tagged('mail_channel')
|
||||
class TestChannelInternals(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestChannelInternals, cls).setUpClass()
|
||||
cls.test_channel = cls.env['mail.channel'].browse(cls.env['mail.channel'].with_context(cls._test_context).channel_create(name='Channel', group_id=None)['id'])
|
||||
cls.test_partner = cls.env['res.partner'].with_context(cls._test_context).create({
|
||||
'name': 'Test Partner',
|
||||
'email': 'test_customer@example.com',
|
||||
})
|
||||
cls.user_employee_nomail = mail_new_test_user(
|
||||
cls.env, login='employee_nomail',
|
||||
email=False,
|
||||
groups='base.group_user',
|
||||
company_id=cls.company_admin.id,
|
||||
name='Evita Employee NoEmail',
|
||||
notification_type='email',
|
||||
signature='--\nEvite'
|
||||
)
|
||||
cls.partner_employee_nomail = cls.user_employee_nomail.partner_id
|
||||
|
||||
@users('employee')
|
||||
def test_channel_members(self):
|
||||
channel = self.env['mail.channel'].browse(self.test_channel.ids)
|
||||
self.assertEqual(channel.message_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(channel.channel_partner_ids, self.env['res.partner'])
|
||||
|
||||
channel.add_members(self.test_partner.ids)
|
||||
self.assertEqual(channel.message_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(channel.channel_partner_ids, self.test_partner)
|
||||
|
||||
self.env['mail.channel.member'].sudo().search([
|
||||
('partner_id', 'in', self.test_partner.ids),
|
||||
('channel_id', 'in', channel.ids)
|
||||
]).unlink()
|
||||
self.assertEqual(channel.message_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(channel.channel_partner_ids, self.env['res.partner'])
|
||||
|
||||
channel.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
self.assertEqual(channel.message_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(channel.channel_partner_ids, self.env['res.partner'])
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
|
||||
def test_channel_chat_message_post_should_update_last_interest_dt(self):
|
||||
channel_info = self.env['mail.channel'].with_user(self.user_admin).channel_get((self.partner_employee | self.user_admin.partner_id).ids)
|
||||
chat = self.env['mail.channel'].with_user(self.user_admin).browse(channel_info['id'])
|
||||
post_time = fields.Datetime.now()
|
||||
# Mocks the return value of field.Datetime.now(),
|
||||
# so we can see if the `last_interest_dt` is updated correctly
|
||||
with patch.object(fields.Datetime, 'now', lambda: post_time):
|
||||
chat.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
channel_member_employee = self.env['mail.channel.member'].search([
|
||||
('partner_id', '=', self.partner_employee.id),
|
||||
('channel_id', '=', chat.id),
|
||||
])
|
||||
channel_member_admin = self.env['mail.channel.member'].search([
|
||||
('partner_id', '=', self.partner_admin.id),
|
||||
('channel_id', '=', chat.id),
|
||||
])
|
||||
self.assertEqual(channel_member_employee.last_interest_dt, post_time)
|
||||
self.assertEqual(channel_member_admin.last_interest_dt, post_time)
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
|
||||
def test_channel_recipients_channel(self):
|
||||
""" Posting a message on a channel should not send emails """
|
||||
channel = self.env['mail.channel'].browse(self.test_channel.ids)
|
||||
channel.add_members((self.partner_employee | self.partner_admin | self.test_partner).ids)
|
||||
with self.mock_mail_gateway():
|
||||
new_msg = channel.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
self.assertNotSentEmail()
|
||||
self.assertEqual(new_msg.model, self.test_channel._name)
|
||||
self.assertEqual(new_msg.res_id, self.test_channel.id)
|
||||
self.assertEqual(new_msg.partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(new_msg.notified_partner_ids, self.env['res.partner'])
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
|
||||
def test_channel_recipients_chat(self):
|
||||
""" Posting a message on a chat should not send emails """
|
||||
channel_info = self.env['mail.channel'].with_user(self.user_admin).channel_get((self.partner_employee | self.user_admin.partner_id).ids)
|
||||
chat = self.env['mail.channel'].with_user(self.user_admin).browse(channel_info['id'])
|
||||
with self.mock_mail_gateway():
|
||||
with self.with_user('employee'):
|
||||
new_msg = chat.message_post(body="Test", message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
self.assertNotSentEmail()
|
||||
self.assertEqual(new_msg.model, chat._name)
|
||||
self.assertEqual(new_msg.res_id, chat.id)
|
||||
self.assertEqual(new_msg.partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(new_msg.notified_partner_ids, self.env['res.partner'])
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
|
||||
def test_channel_recipients_mention(self):
|
||||
""" Posting a message on a classic channel should support mentioning somebody """
|
||||
with self.mock_mail_gateway():
|
||||
self.test_channel.message_post(
|
||||
body="Test", partner_ids=self.test_partner.ids,
|
||||
message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
self.assertSentEmail(self.test_channel.env.user.partner_id, [self.test_partner])
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_channel_user_synchronize(self):
|
||||
"""Archiving / deleting a user should automatically unsubscribe related partner from group restricted channels"""
|
||||
group_restricted_channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_create(name='Sic Mundus', group_id=self.env.ref('base.group_user').id)['id'])
|
||||
|
||||
self.test_channel.add_members((self.partner_employee | self.partner_employee_nomail).ids)
|
||||
group_restricted_channel.add_members((self.partner_employee | self.partner_employee_nomail).ids)
|
||||
|
||||
# Unsubscribe archived user from the private channels, but not from public channels
|
||||
self.user_employee.active = False
|
||||
self.assertEqual(group_restricted_channel.channel_partner_ids, self.partner_employee_nomail)
|
||||
self.assertEqual(self.test_channel.channel_partner_ids, self.user_employee.partner_id | self.partner_employee_nomail)
|
||||
|
||||
# Unsubscribe deleted user from the private channels, but not from public channels
|
||||
self.user_employee_nomail.unlink()
|
||||
self.assertEqual(group_restricted_channel.channel_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(self.test_channel.channel_partner_ids, self.user_employee.partner_id | self.partner_employee_nomail)
|
||||
|
||||
@users('employee_nomail')
|
||||
def test_channel_info_get(self):
|
||||
# `channel_get` should return a new channel the first time a partner is given
|
||||
initial_channel_info = self.env['mail.channel'].channel_get(partners_to=self.test_partner.ids)
|
||||
# shape of channelMembers is [('insert', data...)], [0][1] accesses the data
|
||||
self.assertEqual(set(m['persona']['partner']['id'] for m in initial_channel_info['channel']['channelMembers'][0][1]), {self.partner_employee_nomail.id, self.test_partner.id})
|
||||
|
||||
# `channel_get` should return the existing channel every time the same partner is given
|
||||
same_channel_info = self.env['mail.channel'].channel_get(partners_to=self.test_partner.ids)
|
||||
self.assertEqual(same_channel_info['id'], initial_channel_info['id'])
|
||||
|
||||
# `channel_get` should return the existing channel when the current partner is given together with the other partner
|
||||
together_channel_info = self.env['mail.channel'].channel_get(partners_to=(self.partner_employee_nomail + self.test_partner).ids)
|
||||
self.assertEqual(together_channel_info['id'], initial_channel_info['id'])
|
||||
|
||||
# `channel_get` should return a new channel the first time just the current partner is given,
|
||||
# even if a channel containing the current partner together with other partners already exists
|
||||
solo_channel_info = self.env['mail.channel'].channel_get(partners_to=self.partner_employee_nomail.ids)
|
||||
self.assertNotEqual(solo_channel_info['id'], initial_channel_info['id'])
|
||||
# shape of channelMembers is [('insert', data...)], [0][1] accesses the data
|
||||
self.assertEqual(set(m['persona']['partner']['id'] for m in solo_channel_info['channel']['channelMembers'][0][1]), {self.partner_employee_nomail.id})
|
||||
|
||||
# `channel_get` should return the existing channel every time the current partner is given
|
||||
same_solo_channel_info = self.env['mail.channel'].channel_get(partners_to=self.partner_employee_nomail.ids)
|
||||
self.assertEqual(same_solo_channel_info['id'], solo_channel_info['id'])
|
||||
|
||||
# `channel_get` will pin the channel by default and thus last interest will be updated.
|
||||
@users('employee')
|
||||
def test_channel_info_get_should_update_last_interest_dt(self):
|
||||
# create the channel via `channel_get`
|
||||
self.env['mail.channel'].channel_get(partners_to=self.partner_admin.ids)
|
||||
|
||||
retrieve_time = datetime(2021, 1, 1, 0, 0)
|
||||
with patch.object(fields.Datetime, 'now', lambda: retrieve_time):
|
||||
# `last_interest_dt` should be updated again when `channel_get` is called
|
||||
# because `channel_pin` is called.
|
||||
channel_info = self.env['mail.channel'].channel_get(partners_to=self.partner_admin.ids)
|
||||
self.assertEqual(channel_info['last_interest_dt'], retrieve_time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
|
||||
|
||||
@users('employee')
|
||||
def test_channel_info_seen(self):
|
||||
""" In case of concurrent channel_seen RPC, ensure the oldest call has no effect. """
|
||||
channel_info = self.env['mail.channel'].with_user(self.user_admin).channel_get((self.partner_employee | self.user_admin.partner_id).ids)
|
||||
chat = self.env['mail.channel'].with_user(self.user_admin).browse(channel_info['id'])
|
||||
msg_1 = self._add_messages(chat, 'Body1', author=self.user_employee.partner_id)
|
||||
msg_2 = self._add_messages(chat, 'Body2', author=self.user_employee.partner_id)
|
||||
|
||||
chat._channel_seen(msg_2.id)
|
||||
self.assertEqual(
|
||||
chat.channel_info()[0]['seen_partners_info'][0]['seen_message_id'],
|
||||
msg_2.id,
|
||||
"Last message id should have been updated"
|
||||
)
|
||||
|
||||
chat._channel_seen(msg_1.id)
|
||||
self.assertEqual(
|
||||
chat.channel_info()[0]['seen_partners_info'][0]['seen_message_id'],
|
||||
msg_2.id,
|
||||
"Last message id should stay the same after mark channel as seen with an older message"
|
||||
)
|
||||
|
||||
def test_channel_message_post_should_not_allow_adding_wrong_parent(self):
|
||||
channels = self.env['mail.channel'].create([{'name': '1'}, {'name': '2'}])
|
||||
message = self._add_messages(channels[0], 'Body1')
|
||||
message_format2 = channels[1].message_post(body='Body2', parent_id=message.id)
|
||||
self.assertFalse(message_format2['parent_id'], "should not allow parent from wrong thread")
|
||||
message_format3 = channels[1].message_post(body='Body3', parent_id=message.id + 100)
|
||||
self.assertFalse(message_format3['parent_id'], "should not allow non-existing parent")
|
||||
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_channel_unsubscribe_auto(self):
|
||||
""" Archiving / deleting a user should automatically unsubscribe related
|
||||
partner from private channels """
|
||||
test_user = self.env['res.users'].create({
|
||||
"login": "adam",
|
||||
"name": "Jonas",
|
||||
})
|
||||
test_partner = test_user.partner_id
|
||||
group_restricted_channel = self.env['mail.channel'].with_context(self._test_context).create({
|
||||
'name': 'Sic Mundus',
|
||||
'group_public_id': self.env.ref('base.group_user').id,
|
||||
'channel_partner_ids': [Command.link(self.user_employee.partner_id.id), Command.link(test_partner.id)],
|
||||
})
|
||||
self.test_channel.with_context(self._test_context).write({
|
||||
'channel_partner_ids': [Command.link(self.user_employee.partner_id.id), Command.link(test_partner.id)],
|
||||
})
|
||||
private_group = self.env['mail.channel'].with_user(self.user_employee).with_context(self._test_context).create({
|
||||
'name': 'test',
|
||||
'channel_type': 'group',
|
||||
'channel_partner_ids': [Command.link(self.user_employee.partner_id.id), Command.link(test_partner.id)],
|
||||
})
|
||||
|
||||
# Unsubscribe archived user from the private channels, but not from public channels and not from group
|
||||
self.user_employee.active = False
|
||||
(private_group | self.test_channel).invalidate_recordset(['channel_partner_ids'])
|
||||
self.assertEqual(group_restricted_channel.channel_partner_ids, test_partner)
|
||||
self.assertEqual(self.test_channel.channel_partner_ids, self.user_employee.partner_id | test_partner)
|
||||
self.assertEqual(private_group.channel_partner_ids, self.user_employee.partner_id | test_partner)
|
||||
|
||||
# Unsubscribe deleted user from the private channels, but not from public channels and not from group
|
||||
test_user.unlink()
|
||||
self.assertEqual(group_restricted_channel.channel_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(self.test_channel.channel_partner_ids, self.user_employee.partner_id | test_partner)
|
||||
self.assertEqual(private_group.channel_partner_ids, self.user_employee.partner_id | test_partner)
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_channel_private_unfollow(self):
|
||||
""" Test that a partner can leave (unfollow) a channel/group/chat. """
|
||||
group_restricted_channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_create(name='Channel for Groups', group_id=self.env.ref('base.group_user').id)['id'])
|
||||
public_channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_create(name='Channel for Everyone', group_id=None)['id'])
|
||||
private_group = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids, name="Group")['id'])
|
||||
chat_user_current = self.env['mail.channel'].browse(self.env['mail.channel'].channel_get(self.env.user.partner_id.ids)['id'])
|
||||
|
||||
group_restricted_channel.add_members(self.env.user.partner_id.id)
|
||||
public_channel.add_members(self.env.user.partner_id.id)
|
||||
|
||||
group_restricted_channel.action_unfollow()
|
||||
public_channel.action_unfollow()
|
||||
private_group.action_unfollow()
|
||||
chat_user_current.action_unfollow()
|
||||
|
||||
self.assertEqual(group_restricted_channel.channel_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(public_channel.channel_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(private_group.channel_partner_ids, self.env['res.partner'])
|
||||
self.assertEqual(chat_user_current.channel_partner_ids, self.env['res.partner'])
|
||||
|
||||
def test_channel_unfollow_should_not_post_message_if_the_partner_has_been_removed(self):
|
||||
'''
|
||||
When a partner leaves a channel, the system will help post a message under
|
||||
that partner's name in the channel to notify others if `email_sent` is set `False`.
|
||||
The message should only be posted when the partner is still a member of the channel
|
||||
before method `_action_unfollow()` is called.
|
||||
If the partner has been removed earlier, no more messages will be posted
|
||||
even if `_action_unfollow()` is called again.
|
||||
'''
|
||||
channel = self.env['mail.channel'].browse(self.test_channel.id)
|
||||
channel.add_members(self.test_partner.ids)
|
||||
|
||||
# no message should be posted under test_partner's name
|
||||
messages_0 = self.env['mail.message'].search([
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', '=', channel.id),
|
||||
('author_id', '=', self.test_partner.id)
|
||||
])
|
||||
self.assertEqual(len(messages_0), 0)
|
||||
|
||||
# a message should be posted to notify others when a partner is about to leave
|
||||
channel._action_unfollow(self.test_partner)
|
||||
messages_1 = self.env['mail.message'].search([
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', '=', channel.id),
|
||||
('author_id', '=', self.test_partner.id)
|
||||
])
|
||||
self.assertEqual(len(messages_1), 1)
|
||||
|
||||
# no more messages should be posted if the partner has been removed before.
|
||||
channel._action_unfollow(self.test_partner)
|
||||
messages_2 = self.env['mail.message'].search([
|
||||
('model', '=', 'mail.channel'),
|
||||
('res_id', '=', channel.id),
|
||||
('author_id', '=', self.test_partner.id)
|
||||
])
|
||||
self.assertEqual(len(messages_2), 1)
|
||||
self.assertEqual(messages_1, messages_2)
|
||||
|
||||
def test_channel_should_generate_correct_default_avatar(self):
|
||||
test_channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_create(name='Channel', group_id=self.env.ref('base.group_user').id)['id'])
|
||||
test_channel.uuid = 'channel-uuid'
|
||||
private_group = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id'])
|
||||
private_group.uuid = 'group-uuid'
|
||||
bgcolor_channel = html_escape('hsl(316, 61%, 45%)') # depends on uuid
|
||||
bgcolor_group = html_escape('hsl(17, 60%, 45%)') # depends on uuid
|
||||
expceted_avatar_channel = (channel_avatar.replace('fill="#875a7b"', f'fill="{bgcolor_channel}"')).encode()
|
||||
expected_avatar_group = (group_avatar.replace('fill="#875a7b"', f'fill="{bgcolor_group}"')).encode()
|
||||
|
||||
self.assertEqual(base64.b64decode(test_channel.avatar_128), expceted_avatar_channel)
|
||||
self.assertEqual(base64.b64decode(private_group.avatar_128), expected_avatar_group)
|
||||
|
||||
test_channel.image_128 = base64.b64encode(("<svg/>").encode())
|
||||
self.assertEqual(test_channel.avatar_128, test_channel.image_128)
|
||||
|
||||
def test_channel_write_should_send_notification_if_image_128_changed(self):
|
||||
channel = self.env['mail.channel'].create({'name': '', 'uuid': 'test-uuid'})
|
||||
# do the operation once before the assert to grab the value to expect
|
||||
channel.image_128 = base64.b64encode(("<svg/>").encode())
|
||||
avatar_cache_key = channel._get_avatar_cache_key()
|
||||
channel.image_128 = False
|
||||
self.env['bus.bus'].search([]).unlink()
|
||||
with self.assertBus(
|
||||
[(self.cr.dbname, 'mail.channel', channel.id)],
|
||||
[{
|
||||
"type": "mail.channel/insert",
|
||||
"payload": {
|
||||
"avatarCacheKey": avatar_cache_key,
|
||||
"id": channel.id,
|
||||
},
|
||||
}]
|
||||
):
|
||||
channel.image_128 = base64.b64encode(("<svg/>").encode())
|
||||
|
||||
def test_mail_message_starred_group(self):
|
||||
""" Test starred message computation for a group. A starred
|
||||
message in a group should be considered only if:
|
||||
- It's our message
|
||||
- OR we have access to the channel
|
||||
"""
|
||||
self.assertEqual(self.user_employee._init_messaging()['starred_counter'], 0)
|
||||
test_group = self.env['mail.channel'].create({
|
||||
'name': 'Private Channel',
|
||||
'channel_type': 'group',
|
||||
'channel_partner_ids': [(6, 0, self.partner_employee.id)]
|
||||
})
|
||||
|
||||
test_group_own_message = test_group.with_user(self.user_employee.id).message_post(body='TestingMessage')
|
||||
test_group_own_message.write({'starred_partner_ids': [(6, 0, self.partner_employee.ids)]})
|
||||
self.assertEqual(self.user_employee.with_user(self.user_employee)._init_messaging()['starred_counter'], 1)
|
||||
|
||||
test_group_message = test_group.message_post(body='TestingMessage')
|
||||
test_group_message.write({'starred_partner_ids': [(6, 0, self.partner_employee.ids)]})
|
||||
self.assertEqual(self.user_employee.with_user(self.user_employee)._init_messaging()['starred_counter'], 2)
|
||||
|
||||
test_group.write({'channel_partner_ids': False})
|
||||
self.assertEqual(self.user_employee.with_user(self.user_employee)._init_messaging()['starred_counter'], 1)
|
||||
|
||||
def test_multi_company_chat(self):
|
||||
self._activate_multi_company()
|
||||
self.assertEqual(self.env.user.company_id, self.company_admin)
|
||||
|
||||
with self.with_user('employee'):
|
||||
initial_channel_info = self.env['mail.channel'].with_context(
|
||||
allowed_company_ids=self.company_admin.ids
|
||||
).channel_get(self.partner_employee_c2.ids)
|
||||
self.assertTrue(initial_channel_info, 'should be able to chat with multi company user')
|
||||
|
||||
@users('employee')
|
||||
def test_create_chat_channel_should_only_pin_the_channel_for_the_current_user(self):
|
||||
chat = self.env['mail.channel'].channel_get(partners_to=self.test_partner.ids)
|
||||
member_of_current_user = self.env['mail.channel.member'].search([('channel_id', '=', chat['id']), ('partner_id', '=', self.env.user.partner_id.id)])
|
||||
member_of_correspondent = self.env['mail.channel.member'].search([('channel_id', '=', chat['id']), ('partner_id', '=', self.test_partner.id)])
|
||||
self.assertTrue(member_of_current_user.is_pinned)
|
||||
self.assertFalse(member_of_correspondent.is_pinned)
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserPortal, HttpCaseWithUserDemo
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
|
||||
|
||||
@odoo.tests.tagged('-at_install', 'post_install', 'is_tour')
|
||||
class TestMailPublicPage(HttpCaseWithUserDemo, HttpCaseWithUserPortal):
|
||||
"""Checks that the invite page redirects to the channel and that all
|
||||
modules load correctly on the welcome and channel page when authenticated as various users"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
portal_user = mail_new_test_user(
|
||||
self.env,
|
||||
name='Portal Bowser',
|
||||
login='portal_bowser',
|
||||
email='portal_bowser@example.com',
|
||||
groups='base.group_portal',
|
||||
)
|
||||
internal_user = mail_new_test_user(
|
||||
self.env,
|
||||
name='Internal Luigi',
|
||||
login='internal_luigi',
|
||||
email='internal_luigi@example.com',
|
||||
groups='base.group_user',
|
||||
)
|
||||
guest = self.env['mail.guest'].create({'name': 'Guest Mario'})
|
||||
|
||||
self.channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_create(group_id=None, name='Test channel')['id'])
|
||||
self.channel.add_members(portal_user.partner_id.ids)
|
||||
self.channel.add_members(internal_user.partner_id.ids)
|
||||
self.channel.add_members(guest_ids=[guest.id])
|
||||
|
||||
self.group = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(internal_user + portal_user).partner_id.ids, name="Test group")['id'])
|
||||
self.group.add_members(guest_ids=[guest.id])
|
||||
|
||||
self.tour = "mail/static/tests/tours/discuss_public_tour.js"
|
||||
|
||||
def _open_channel_page_as_user(self, login):
|
||||
self.start_tour(self.channel.invitation_url, self.tour, login=login)
|
||||
# Second run of the tour as the first call has side effects, like creating user settings or adding members to
|
||||
# the channel, so we need to run it again to test different parts of the code.
|
||||
self.start_tour(self.channel.invitation_url, self.tour, login=login)
|
||||
|
||||
def _open_group_page_as_user(self, login):
|
||||
self.start_tour(self.group.invitation_url, self.tour, login=login)
|
||||
# Second run of the tour as the first call has side effects, like creating user settings or adding members to
|
||||
# the channel, so we need to run it again to test different parts of the code.
|
||||
self.start_tour(self.group.invitation_url, self.tour, login=login)
|
||||
|
||||
def test_mail_channel_public_page_as_admin(self):
|
||||
self._open_channel_page_as_user('admin')
|
||||
|
||||
def test_mail_group_public_page_as_admin(self):
|
||||
self._open_group_page_as_user('admin')
|
||||
|
||||
def test_mail_channel_public_page_as_guest(self):
|
||||
self.start_tour(self.channel.invitation_url, "mail/static/tests/tours/mail_channel_as_guest_tour.js")
|
||||
guest = self.env['mail.guest'].search([('channel_ids', 'in', self.channel.id)], limit=1, order='id desc')
|
||||
self.start_tour(self.channel.invitation_url, self.tour, cookies={guest._cookie_name: f"{guest.id}{guest._cookie_separator}{guest.access_token}"})
|
||||
|
||||
def test_mail_group_public_page_as_guest(self):
|
||||
self.start_tour(self.group.invitation_url, "mail/static/tests/tours/mail_channel_as_guest_tour.js")
|
||||
guest = self.env['mail.guest'].search([('channel_ids', 'in', self.channel.id)], limit=1, order='id desc')
|
||||
self.start_tour(self.group.invitation_url, self.tour, cookies={guest._cookie_name: f"{guest.id}{guest._cookie_separator}{guest.access_token}"})
|
||||
|
||||
def test_mail_channel_public_page_as_internal(self):
|
||||
self._open_channel_page_as_user('demo')
|
||||
|
||||
def test_mail_group_public_page_as_internal(self):
|
||||
self._open_group_page_as_user('demo')
|
||||
|
||||
def test_mail_channel_public_page_as_portal(self):
|
||||
self._open_channel_page_as_user('portal')
|
||||
|
||||
def test_mail_group_public_page_as_portal(self):
|
||||
self._open_group_page_as_user('portal')
|
||||
|
||||
def test_chat_from_token_as_guest(self):
|
||||
self.env['ir.config_parameter'].set_param('mail.chat_from_token', True)
|
||||
self.url_open('/chat/xyz')
|
||||
channel = self.env['mail.channel'].search([('uuid', '=', 'xyz')])
|
||||
self.assertEqual(len(channel), 1)
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from functools import partial
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
|
||||
mail_channel_new_test_user = partial(mail_new_test_user, context={'mail_channel_nosubscribe': False})
|
||||
|
||||
|
||||
class TestMailChannelMembers(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailChannelMembers, cls).setUpClass()
|
||||
|
||||
cls.secret_group = cls.env['res.groups'].create({
|
||||
'name': 'Secret User Group',
|
||||
})
|
||||
cls.env['ir.model.data'].create({
|
||||
'name': 'secret_group',
|
||||
'module': 'mail',
|
||||
'model': cls.secret_group._name,
|
||||
'res_id': cls.secret_group.id,
|
||||
})
|
||||
|
||||
cls.user_1 = mail_channel_new_test_user(
|
||||
cls.env, login='user_1',
|
||||
name='User 1',
|
||||
groups='base.group_user,mail.secret_group')
|
||||
cls.user_2 = mail_channel_new_test_user(
|
||||
cls.env, login='user_2',
|
||||
name='User 2',
|
||||
groups='base.group_user,mail.secret_group')
|
||||
cls.user_3 = mail_channel_new_test_user(
|
||||
cls.env, login='user_3',
|
||||
name='User 3',
|
||||
groups='base.group_user,mail.secret_group')
|
||||
cls.user_portal = mail_channel_new_test_user(
|
||||
cls.env, login='user_portal',
|
||||
name='User Portal',
|
||||
groups='base.group_portal')
|
||||
cls.user_public = mail_channel_new_test_user(
|
||||
cls.env, login='user_ublic',
|
||||
name='User Public',
|
||||
groups='base.group_public')
|
||||
|
||||
cls.group = cls.env['mail.channel'].create({
|
||||
'name': 'Group',
|
||||
'channel_type': 'group',
|
||||
})
|
||||
cls.group_restricted_channel = cls.env['mail.channel'].create({
|
||||
'name': 'Group restricted channel',
|
||||
'channel_type': 'channel',
|
||||
'group_public_id': cls.secret_group.id,
|
||||
})
|
||||
cls.public_channel = cls.env['mail.channel'].browse(cls.env['mail.channel'].channel_create(group_id=None, name='Public channel of user 1')['id'])
|
||||
(cls.group | cls.group_restricted_channel | cls.public_channel).channel_member_ids.unlink()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# GROUP
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_group_01(self):
|
||||
"""Test access on group."""
|
||||
res = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertFalse(res)
|
||||
|
||||
# User 1 can join group with SUDO
|
||||
self.group.with_user(self.user_1).sudo().add_members(self.user_1.partner_id.ids)
|
||||
res = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(res.partner_id, self.user_1.partner_id)
|
||||
|
||||
# User 2 can not join group
|
||||
with self.assertRaises(AccessError):
|
||||
self.group.with_user(self.user_2).add_members(self.user_2.partner_id.ids)
|
||||
|
||||
# User 2 can not create a `mail.channel.member` to join the group
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel.member'].with_user(self.user_2).create({
|
||||
'partner_id': self.user_2.partner_id.id,
|
||||
'channel_id': self.group.id,
|
||||
})
|
||||
|
||||
# User 2 can not write on `mail.channel.member` to join the group
|
||||
channel_member = self.env['mail.channel.member'].with_user(self.user_2).search([('partner_id', '=', self.user_2.partner_id.id)])[0]
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member.channel_id = self.group.id
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member.write({'channel_id': self.group.id})
|
||||
|
||||
# Even with SUDO, channel_id of channel.member should not be changed.
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member.sudo().channel_id = self.group.id
|
||||
|
||||
# User 2 can not write on the `partner_id` of `mail.channel.member`
|
||||
# of an other partner to join a group
|
||||
channel_member_1 = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_1.partner_id.id)])
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member_1.with_user(self.user_2).partner_id = self.user_2.partner_id
|
||||
self.assertEqual(channel_member_1.partner_id, self.user_1.partner_id)
|
||||
|
||||
# Even with SUDO, partner_id of channel.member should not be changed.
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member_1.with_user(self.user_2).sudo().partner_id = self.user_2.partner_id
|
||||
|
||||
def test_group_members(self):
|
||||
"""Test invitation in group part 1 (invite using crud methods)."""
|
||||
self.group.with_user(self.user_1).sudo().add_members(self.user_1.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(len(channel_members), 1)
|
||||
|
||||
# User 2 is not in the group, they can not invite user 3
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.channel.member'].with_user(self.user_2).create({
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
'channel_id': self.group.id,
|
||||
})
|
||||
|
||||
# User 1 is in the group, they can invite other users
|
||||
self.env['mail.channel.member'].with_user(self.user_1).create({
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
'channel_id': self.group.id,
|
||||
})
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
|
||||
|
||||
# But User 3 can not write on the `mail.channel.member` of other user
|
||||
channel_member_1 = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_1.partner_id.id)])
|
||||
channel_member_3 = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_portal.partner_id.id)])
|
||||
channel_member_3.with_user(self.user_portal).custom_channel_name = 'Test'
|
||||
with self.assertRaises(AccessError):
|
||||
channel_member_1.with_user(self.user_2).custom_channel_name = 'Blabla'
|
||||
self.assertNotEqual(channel_member_1.custom_channel_name, 'Blabla')
|
||||
|
||||
def test_group_invite(self):
|
||||
"""Test invitation in group part 2 (use `invite` action)."""
|
||||
self.group.with_user(self.user_1).sudo().add_members(self.user_1.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
# User 2 is not in the group, they can not invite user_portal
|
||||
with self.assertRaises(AccessError):
|
||||
self.group.with_user(self.user_2).add_members(self.user_portal.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
# User 1 is in the group, they can invite user_portal
|
||||
self.group.with_user(self.user_1).add_members(self.user_portal.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
|
||||
|
||||
def test_group_leave(self):
|
||||
"""Test kick/leave channel."""
|
||||
self.group.with_user(self.user_1).sudo().add_members(self.user_1.partner_id.ids)
|
||||
self.group.with_user(self.user_portal).sudo().add_members(self.user_portal.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group.id)])
|
||||
self.assertEqual(len(channel_members), 2)
|
||||
|
||||
# User 2 is not in the group, they can not kick user 1
|
||||
with self.assertRaises(AccessError):
|
||||
channel_members.with_user(self.user_2).unlink()
|
||||
|
||||
# User 3 is in the group, they can kick user 1
|
||||
channel_members.with_user(self.user_portal).unlink()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# GROUP BASED CHANNELS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_group_restricted_channel(self):
|
||||
"""Test basics on group channel."""
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertFalse(channel_members)
|
||||
|
||||
# user 1 is in the channel, they can join the channel
|
||||
self.group_restricted_channel.with_user(self.user_1).add_members(self.user_1.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
# user 3 is not in the channel, they can not join
|
||||
with self.assertRaises(AccessError):
|
||||
self.group_restricted_channel.with_user(self.user_portal).add_members(self.user_portal.partner_id.ids)
|
||||
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
with self.assertRaises(AccessError):
|
||||
channel_members.with_user(self.user_portal).partner_id = self.user_portal.partner_id
|
||||
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
# user 1 can not invite user 3 because they are not in the channel
|
||||
with self.assertRaises(UserError):
|
||||
self.group_restricted_channel.with_user(self.user_1).add_members(self.user_portal.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
# but user 2 is in the channel and can be invited by user 1
|
||||
self.group_restricted_channel.with_user(self.user_1).add_members(self.user_2.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PUBLIC CHANNELS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_public_channel(self):
|
||||
""" Test access on public channels """
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.public_channel.id)])
|
||||
self.assertFalse(channel_members)
|
||||
|
||||
self.public_channel.with_user(self.user_1).add_members(self.user_1.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.public_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
|
||||
|
||||
self.public_channel.with_user(self.user_2).add_members(self.user_2.partner_id.ids)
|
||||
channel_members = self.env['mail.channel.member'].search([('channel_id', '=', self.public_channel.id)])
|
||||
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id)
|
||||
|
||||
# portal/public users still cannot join a public channel, should go through dedicated controllers
|
||||
with self.assertRaises(AccessError):
|
||||
self.public_channel.with_user(self.user_portal).add_members(self.user_portal.partner_id.ids)
|
||||
with self.assertRaises(AccessError):
|
||||
self.public_channel.with_user(self.user_public).add_members(self.user_public.partner_id.ids)
|
||||
|
||||
def test_channel_member_invite_with_guest(self):
|
||||
guest = self.env['mail.guest'].create({'name': 'Guest'})
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'ToInvite',
|
||||
'active': True,
|
||||
'type': 'contact',
|
||||
'user_ids': self.user_1,
|
||||
})
|
||||
self.public_channel.add_members(guest_ids=[guest.id])
|
||||
search = self.env['res.partner'].search_for_channel_invite(partner.name, channel_id=self.public_channel.id)
|
||||
self.assertEqual(len(search['partners']), 1)
|
||||
self.assertEqual(search['partners'][0]['id'], partner.id)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# UNREAD COUNTER TESTS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def test_unread_counter_with_message_post(self):
|
||||
channel_as_user_1 = self.env['mail.channel'].browse(self.env['mail.channel'].with_user(self.user_1).channel_create(group_id=None, name='Public channel')['id'])
|
||||
channel_as_user_1.with_user(self.user_1).add_members(self.user_1.partner_id.ids)
|
||||
channel_as_user_1.with_user(self.user_1).add_members(self.user_2.partner_id.ids)
|
||||
channel_1_rel_user_2 = self.env['mail.channel.member'].search([
|
||||
('channel_id', '=', channel_as_user_1.id),
|
||||
('partner_id', '=', self.user_2.partner_id.id)
|
||||
])
|
||||
self.assertEqual(channel_1_rel_user_2.message_unread_counter, 0, "should not have unread message initially as notification type is ignored")
|
||||
|
||||
channel_as_user_1.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
channel_1_rel_user_2 = self.env['mail.channel.member'].search([
|
||||
('channel_id', '=', channel_as_user_1.id),
|
||||
('partner_id', '=', self.user_2.partner_id.id)
|
||||
])
|
||||
self.assertEqual(channel_1_rel_user_2.message_unread_counter, 1, "should have 1 unread message after someone else posted a message")
|
||||
|
||||
def test_unread_counter_with_message_post_multi_channel(self):
|
||||
channel_1_as_user_1 = self.env['mail.channel'].with_user(self.user_1).browse(self.env['mail.channel'].with_user(self.user_1).channel_create(group_id=None, name='wololo channel')['id'])
|
||||
channel_2_as_user_2 = self.env['mail.channel'].with_user(self.user_2).browse(self.env['mail.channel'].with_user(self.user_2).channel_create(group_id=None, name='walala channel')['id'])
|
||||
channel_1_as_user_1.add_members(self.user_2.partner_id.ids)
|
||||
channel_2_as_user_2.add_members(self.user_1.partner_id.ids)
|
||||
channel_2_as_user_2.add_members(self.user_3.partner_id.ids)
|
||||
channel_1_as_user_1.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
channel_1_as_user_1.message_post(body='Test 2', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
channel_2_as_user_2.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
|
||||
members = self.env['mail.channel.member'].search([('channel_id', 'in', (channel_1_as_user_1 + channel_2_as_user_2).ids)], order="id")
|
||||
self.assertEqual(members.mapped('message_unread_counter'), [
|
||||
0, # channel 1 user 1: posted last message
|
||||
0, # channel 2 user 2: posted last message
|
||||
2, # channel 1 user 2: received 2 messages (from message post)
|
||||
1, # channel 2 user 1: received 1 message (from message post)
|
||||
1, # channel 2 user 3: received 1 message (from message post)
|
||||
])
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import Form, tagged, users
|
||||
from odoo.tests import Form, HttpCase, tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
|
|
@ -14,12 +16,11 @@ class TestMailComposer(MailCommon):
|
|||
def setUpClass(cls):
|
||||
super(TestMailComposer, cls).setUpClass()
|
||||
cls.env['ir.config_parameter'].set_param('mail.restrict.template.rendering', True)
|
||||
cls.user_employee.groups_id -= cls.env.ref('mail.group_mail_template_editor')
|
||||
cls.user_employee.group_ids -= cls.env.ref('mail.group_mail_template_editor')
|
||||
cls.test_record = cls.env['res.partner'].with_context(cls._test_context).create({
|
||||
'name': 'Test',
|
||||
})
|
||||
cls.body_html = """<div>
|
||||
<h1>Hello sir!</h1>
|
||||
cls.body_html = """<h1>Hello sir!</h1>
|
||||
<p>Here! <a href="https://www.example.com">
|
||||
<!--[if mso]>
|
||||
<i style="letter-spacing: 25px; mso-font-width: -100%; mso-text-raise: 30pt;"> </i>
|
||||
|
|
@ -28,19 +29,18 @@ class TestMailComposer(MailCommon):
|
|||
<!--[if mso]>
|
||||
<i style="letter-spacing: 25px; mso-font-width: -100%;"> </i>
|
||||
<![endif]-->
|
||||
</a> Make good use of it.</p>
|
||||
</div>"""
|
||||
</a> Make good use of it.</p>"""
|
||||
|
||||
cls.mail_template = cls.env['mail.template'].create({
|
||||
'auto_delete': True,
|
||||
'body_html': cls.body_html,
|
||||
'lang': '{{ object.lang }}',
|
||||
'model_id': cls.env['ir.model']._get_id('res.partner'),
|
||||
'subject': 'MSO FTW',
|
||||
'name': 'Test template with mso conditionals',
|
||||
'use_default_to': True,
|
||||
'subject': 'MSO FTW',
|
||||
})
|
||||
|
||||
|
||||
@tagged('mail_composer')
|
||||
class TestMailComposerForm(TestMailComposer):
|
||||
""" Test mail composer form view usage. """
|
||||
|
|
@ -48,23 +48,22 @@ class TestMailComposerForm(TestMailComposer):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailComposerForm, cls).setUpClass()
|
||||
|
||||
cls.user_employee.write({'groups_id': [
|
||||
(4, cls.env.ref('base.group_private_addresses').id),
|
||||
(4, cls.env.ref('base.group_partner_manager').id),
|
||||
]})
|
||||
cls.other_company = cls.env['res.company'].create({'name': 'Other Company'})
|
||||
cls.user_employee.write({
|
||||
'company_ids': [(4, cls.other_company.id)]
|
||||
})
|
||||
cls.partner_private, cls.partner_private_2, cls.partner_classic = cls.env['res.partner'].create([
|
||||
{
|
||||
'email': 'private.customer@text.example.com',
|
||||
'phone': '0032455112233',
|
||||
'name': 'Private Customer',
|
||||
'type': 'private',
|
||||
'company_id': cls.other_company.id,
|
||||
},
|
||||
{
|
||||
'email': 'private.customer.2@test.example.com',
|
||||
'phone': '0032455445566',
|
||||
'name': 'Private Customer 2',
|
||||
'type': 'private',
|
||||
'company_id': cls.other_company.id,
|
||||
},
|
||||
{
|
||||
'email': 'not.private@test.example.com',
|
||||
|
|
@ -84,11 +83,11 @@ class TestMailComposerForm(TestMailComposer):
|
|||
form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_partner_ids': partner_classic.ids,
|
||||
'default_model': test_record._name,
|
||||
'default_res_id': test_record.id,
|
||||
'default_res_ids': test_record.ids,
|
||||
}))
|
||||
form.body = '<p>Hello</p>'
|
||||
self.assertEqual(
|
||||
form.partner_ids._get_ids(), partner_classic.ids,
|
||||
form.partner_ids.ids, partner_classic.ids,
|
||||
'Default populates the field'
|
||||
)
|
||||
saved_form = form.save()
|
||||
|
|
@ -103,7 +102,7 @@ class TestMailComposerForm(TestMailComposer):
|
|||
message = self.test_record.message_ids[0]
|
||||
self.assertEqual(message.body, '<p>Hello</p>')
|
||||
self.assertEqual(message.partner_ids, partner_classic)
|
||||
self.assertEqual(message.subject, f'Re: {test_record.name}')
|
||||
self.assertEqual(message.subject, f'{test_record.name}')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('employee')
|
||||
|
|
@ -116,11 +115,11 @@ class TestMailComposerForm(TestMailComposer):
|
|||
form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_partner_ids': (partner_private + partner_classic).ids,
|
||||
'default_model': test_record._name,
|
||||
'default_res_id': test_record.id,
|
||||
'default_res_ids': test_record.ids,
|
||||
}))
|
||||
form.body = '<p>Hello</p>'
|
||||
self.assertEqual(
|
||||
sorted(form.partner_ids._get_ids()),
|
||||
sorted(form.partner_ids.ids),
|
||||
sorted((partner_private + partner_classic).ids),
|
||||
'Default populates the field'
|
||||
)
|
||||
|
|
@ -136,15 +135,15 @@ class TestMailComposerForm(TestMailComposer):
|
|||
message = self.test_record.message_ids[0]
|
||||
self.assertEqual(message.body, '<p>Hello</p>')
|
||||
self.assertEqual(message.partner_ids, partner_private + partner_classic)
|
||||
self.assertEqual(message.subject, f'Re: {test_record.name}')
|
||||
self.assertEqual(message.subject, f'{test_record.name}')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.addons.mail.models.mail_mail')
|
||||
@users('employee')
|
||||
def test_composer_default_recipients_private_norights(self):
|
||||
""" Test usage of a private partner in composer when not having the
|
||||
rights to see them, as default value """
|
||||
self.user_employee.write({'groups_id': [
|
||||
(3, self.env.ref('base.group_private_addresses').id),
|
||||
self.user_employee.write({'company_ids': [
|
||||
(3, self.other_company.id),
|
||||
]})
|
||||
with self.assertRaises(AccessError):
|
||||
_name = self.partner_private.with_env(self.env).name
|
||||
|
|
@ -156,18 +155,62 @@ class TestMailComposerForm(TestMailComposer):
|
|||
_form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_partner_ids': (self.partner_private + partner_classic).ids,
|
||||
'default_model': test_record._name,
|
||||
'default_res_id': test_record.id,
|
||||
'default_res_ids': test_record.ids,
|
||||
}))
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('employee')
|
||||
def test_composer_template_change_recipients_update(self):
|
||||
"""Check that recipients only change when coming or going to a template with specific recipients."""
|
||||
self.mail_template.write({
|
||||
'email_to': self.partner_private.email_formatted,
|
||||
'partner_to': False,
|
||||
'use_default_to': False,
|
||||
})
|
||||
specific_recipient_template = self.mail_template
|
||||
specific_recipient_template_copy = specific_recipient_template.copy(default={
|
||||
'email_to': False,
|
||||
'partner_to': f'{self.partner_private_2.id}',
|
||||
})
|
||||
default_recipient_template = specific_recipient_template.copy(default={
|
||||
'email_to': False, 'partner_to': False, 'use_default_to': True,
|
||||
})
|
||||
default_recipient_template_copy = default_recipient_template.copy()
|
||||
test_record = self.test_record.with_env(self.env)
|
||||
default_recipient = test_record
|
||||
self.assertTrue(default_recipient, self.env.registry['res.partner'])
|
||||
|
||||
cases = [
|
||||
(self.partner_classic, False, specific_recipient_template, self.partner_private, 'No template -> Specific recipients'),
|
||||
(self.partner_classic, specific_recipient_template, specific_recipient_template_copy, self.partner_private_2, 'Specific recipients -> Specific recipients'),
|
||||
(self.partner_classic, specific_recipient_template_copy, default_recipient_template, self.partner_classic, 'Specific recipients -> Default recipients'),
|
||||
(None, default_recipient_template, default_recipient_template_copy, default_recipient, 'Default recipients -> Default recipients'),
|
||||
(self.partner_classic, default_recipient_template, default_recipient_template_copy, self.partner_classic, 'Default recipients -> Default recipients'),
|
||||
(self.partner_classic, default_recipient_template_copy, specific_recipient_template, self.partner_private, 'Default recipients -> Specific recipients'),
|
||||
(None, specific_recipient_template, False, False, 'Specific recipients -> No template'),
|
||||
(None, default_recipient_template, False, False, 'Default recipients -> No template'),
|
||||
]
|
||||
for previous_partners, previous_template, new_template, expected_partners, case_name in cases:
|
||||
with self.subTest(case=case_name):
|
||||
composer_form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_model': test_record._name,
|
||||
'default_res_ids': test_record.ids,
|
||||
'default_template_id': previous_template and previous_template.id,
|
||||
} | ({'default_partner_ids': previous_partners.ids} if previous_partners is not None else {})
|
||||
))
|
||||
composer_form.template_id = new_template or self.env['mail.template']
|
||||
self.assertEqual(sorted(composer_form.partner_ids.ids), sorted(expected_partners.ids if expected_partners else []))
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('employee')
|
||||
def test_composer_template_recipients_private(self):
|
||||
""" Test usage of a private partner in composer, comint from template
|
||||
""" Test usage of a private partner in composer, coming from template
|
||||
value """
|
||||
email_to_new = 'new.customer@test.example.com'
|
||||
self.mail_template.write({
|
||||
'email_to': f'{self.partner_private_2.email_formatted}, {email_to_new}',
|
||||
'partner_to': f'{self.partner_private.id},{self.partner_classic.id}',
|
||||
'use_default_to': False,
|
||||
})
|
||||
template = self.mail_template.with_env(self.env)
|
||||
partner_private = self.partner_private.with_env(self.env)
|
||||
|
|
@ -177,7 +220,7 @@ class TestMailComposerForm(TestMailComposer):
|
|||
|
||||
form = Form(self.env['mail.compose.message'].with_context({
|
||||
'default_model': test_record._name,
|
||||
'default_res_id': test_record.id,
|
||||
'default_res_ids': test_record.ids,
|
||||
'default_template_id': template.id,
|
||||
}))
|
||||
|
||||
|
|
@ -192,15 +235,14 @@ class TestMailComposerForm(TestMailComposer):
|
|||
self.assertEqual(new_partner.type, 'contact', 'Should create a new contact')
|
||||
|
||||
self.assertEqual(
|
||||
sorted(form.partner_ids._get_ids()),
|
||||
sorted(form.partner_ids.ids),
|
||||
sorted((partner_private + partner_classic + partner_private_2 + new_partner).ids),
|
||||
'Template populates the field with both email_to and partner_to'
|
||||
)
|
||||
saved_form = form.save()
|
||||
self.assertEqual(
|
||||
# saved_form.partner_ids, partner_private + partner_classic + partner_private_2 + new_partner,
|
||||
saved_form.partner_ids, partner_classic + new_partner,
|
||||
'Template value is kept at save (FIXME: loosing private partner)'
|
||||
saved_form.partner_ids, partner_private + partner_classic + partner_private_2 + new_partner,
|
||||
'Template value is kept at save'
|
||||
)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
|
|
@ -210,8 +252,8 @@ class TestMailComposerForm(TestMailComposer):
|
|||
self.assertIn('<h1>Hello sir!</h1>', message.body)
|
||||
# self.assertEqual(message.partner_ids, partner_private + partner_classic + partner_private_2 + new_partner)
|
||||
self.assertEqual(
|
||||
message.partner_ids, partner_classic + new_partner,
|
||||
'FIXME: loosing private partner'
|
||||
message.partner_ids, partner_private + partner_classic + partner_private_2 + new_partner,
|
||||
'Should not loosing unreadable partners'
|
||||
)
|
||||
self.assertEqual(message.subject, 'MSO FTW')
|
||||
|
||||
|
|
@ -229,7 +271,7 @@ class TestMailComposerRendering(TestMailComposer):
|
|||
'subject': 'MSO FTW',
|
||||
})
|
||||
|
||||
values = mail_compose_message.get_mail_values(self.partner_employee.ids)
|
||||
values = mail_compose_message._prepare_mail_values(self.partner_employee.ids)
|
||||
|
||||
self.assertIn(
|
||||
self.body_html,
|
||||
|
|
@ -254,10 +296,124 @@ class TestMailComposerRendering(TestMailComposer):
|
|||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
composer._action_send_mail()
|
||||
|
||||
values = composer.get_mail_values(self.partner_employee.ids)
|
||||
values = composer._prepare_mail_values(self.partner_employee.ids)
|
||||
|
||||
self.assertIn(
|
||||
self.body_html,
|
||||
values[self.partner_employee.id]['body_html'],
|
||||
'We must preserve (mso) comments in email html'
|
||||
)
|
||||
|
||||
|
||||
@tagged("mail_composer", "-at_install", "post_install")
|
||||
class TestMailComposerUI(MailCommon, HttpCase):
|
||||
|
||||
def test_mail_composer_test_tour(self):
|
||||
template_data = [
|
||||
{
|
||||
'name': 'Test template',
|
||||
'partner_to': '{{ object.id }}',
|
||||
},
|
||||
{
|
||||
'name': 'Test template for admin',
|
||||
'user_id': self.env.ref('base.user_admin').id,
|
||||
},
|
||||
]
|
||||
self.env['mail.template'].create([
|
||||
{
|
||||
**data,
|
||||
'auto_delete': True,
|
||||
'lang': '{{ object.lang }}',
|
||||
'model_id': self.env['ir.model']._get_id('res.partner'),
|
||||
}
|
||||
for data in template_data
|
||||
])
|
||||
partner = self.env["res.partner"].create({"name": "Jane", "email": "jane@example.com"})
|
||||
user_partner = self.env["res.partner"].create({"name": "Not A Demo User", "email": "NotADemoUser@mail.com"})
|
||||
user = self.env["res.users"].create({
|
||||
"name": "Not A Demo User",
|
||||
"login": "nadu",
|
||||
"partner_id": user_partner.id
|
||||
})
|
||||
partner.message_subscribe(partner_ids=[self.user_admin.partner_id.id])
|
||||
with self.mock_mail_app():
|
||||
self.start_tour(
|
||||
f"/odoo/res.partner/{partner.id}",
|
||||
"mail/static/tests/tours/mail_composer_test_tour.js",
|
||||
login=self.user_employee.login
|
||||
)
|
||||
|
||||
message_1, message_2, message_3 = self._new_msgs.filtered(lambda message: message.author_id == self.user_employee.partner_id)
|
||||
self.assertIn(user.partner_id, message_1.partner_ids)
|
||||
self.assertEqual(
|
||||
sorted(message_1.attachment_ids.mapped('raw')),
|
||||
sorted([b'hello, world', b'hi there']))
|
||||
|
||||
signature_pattern = r'<span data-o-mail-quote="1">--\nErnest</span>'
|
||||
|
||||
# For the first message, the user opened the full composer. Therefore,
|
||||
# the signature should have been appended to the message body. As the user
|
||||
# did not deleted it from the editor, the signature should still be
|
||||
# present in the message body. The signature shouldn't be automatically
|
||||
# added by the server as it has already been added by the full composer.
|
||||
|
||||
self.assertEqual(len(re.findall(signature_pattern, message_1.body)), 1)
|
||||
self.assertFalse(message_1.email_add_signature)
|
||||
|
||||
# For the second message, the user opened the full composer. However, the
|
||||
# user manually deleted the signature. As a result, the signature shouldn't
|
||||
# be present in the message body. The signature shouldn't be automatically
|
||||
# added by the server as it has already been added by the full composer.
|
||||
|
||||
self.assertEqual(len(re.findall(signature_pattern, message_2.body)), 0)
|
||||
self.assertFalse(message_2.email_add_signature)
|
||||
|
||||
# For the third message, the user didn't open the full composer. Therefore,
|
||||
# the signature shouldn't be added to the message body. However, the server
|
||||
# should automatically add it to the message when sending it.
|
||||
|
||||
self.assertEqual(len(re.findall(signature_pattern, message_3.body)), 0)
|
||||
self.assertTrue(message_3.email_add_signature)
|
||||
|
||||
def test_mail_html_composer_test_tour(self):
|
||||
template_data = [
|
||||
{
|
||||
'name': 'Test template',
|
||||
'partner_to': '{{ object.id }}',
|
||||
},
|
||||
{
|
||||
'name': 'Test template for admin',
|
||||
'user_id': self.env.ref('base.user_admin').id,
|
||||
},
|
||||
]
|
||||
self.env['mail.template'].create([
|
||||
{
|
||||
**data,
|
||||
'auto_delete': True,
|
||||
'lang': '{{ object.lang }}',
|
||||
'model_id': self.env['ir.model']._get_id('res.partner'),
|
||||
}
|
||||
for data in template_data
|
||||
])
|
||||
partner = self.env["res.partner"].create({"name": "Jane", "email": "jane@example.com"})
|
||||
partner.message_subscribe(partner_ids=[self.user_admin.partner_id.id])
|
||||
with self.mock_mail_app():
|
||||
self.start_tour(
|
||||
f"/odoo/res.partner/{partner.id}",
|
||||
"mail/static/tests/tours/mail_html_composer_test_tour.js",
|
||||
login=self.user_employee.login,
|
||||
)
|
||||
|
||||
def test_send_attachment_without_body(self):
|
||||
self.start_tour("/odoo/discuss", "create_thread_for_attachment_without_body",login="admin")
|
||||
|
||||
def test_mail_composer_autosave_tour(self):
|
||||
partner = self.env["res.partner"].create(
|
||||
{"name": "Jane", "email": "jane@example.com"})
|
||||
with self.mock_mail_app():
|
||||
self.start_tour(
|
||||
f"/odoo/res.partner/{partner.id}",
|
||||
"mail/static/tests/tours/mail_composer_autosave_tour.js",
|
||||
login=self.user_employee.login
|
||||
)
|
||||
self.assertEqual(partner.function, "Director")
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests.common import tagged, HttpCase
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'mail_composer')
|
||||
class TestMailFullComposer(MailCommon, HttpCase):
|
||||
|
||||
def test_full_composer_tour(self):
|
||||
self.env['mail.template'].create({
|
||||
'name': 'Test template',
|
||||
'partner_to': '{{ object.id }}',
|
||||
'lang': '{{ object.lang }}',
|
||||
'auto_delete': True,
|
||||
'model_id': self.ref('base.model_res_partner'),
|
||||
})
|
||||
user = self.env['res.users'].create({
|
||||
'email': 'testuser@testuser.com',
|
||||
'groups_id': [Command.set([self.ref('base.group_user'), self.ref('base.group_partner_manager')])],
|
||||
'name': 'Test User',
|
||||
'login': 'testuser',
|
||||
'password': 'testuser',
|
||||
})
|
||||
partner = self.env["res.partner"].create({"name": "Jane", "email": "jane@example.com"})
|
||||
with self.mock_mail_app():
|
||||
self.start_tour(f"/web#id={partner.id}&model=res.partner", 'mail/static/tests/tours/mail_full_composer_test_tour.js', login='testuser')
|
||||
message = self._new_msgs.filtered(lambda message: message.author_id == user.partner_id)
|
||||
self.assertEqual(len(message), 1)
|
||||
|
|
@ -19,7 +19,7 @@ class MailCase(TransactionCase):
|
|||
disconnected_smtpsession = mock.MagicMock()
|
||||
disconnected_smtpsession.quit.side_effect = smtplib.SMTPServerDisconnected
|
||||
mail = self.env["mail.mail"].create({})
|
||||
with mock.patch("odoo.addons.base.models.ir_mail_server.IrMailServer.connect", return_value=disconnected_smtpsession):
|
||||
with mock.patch("odoo.addons.base.models.ir_mail_server.IrMail_Server._connect__", return_value=disconnected_smtpsession):
|
||||
with mock.patch("odoo.addons.mail.models.mail_mail._logger.info") as mock_logging_info:
|
||||
mail.send()
|
||||
disconnected_smtpsession.quit.assert_called_once()
|
||||
|
|
@ -27,9 +27,4 @@ class MailCase(TransactionCase):
|
|||
"Ignoring SMTPServerDisconnected while trying to quit non open session"
|
||||
)
|
||||
# if we get here SMTPServerDisconnected was not raised
|
||||
self.assertEqual(mail.state, "exception")
|
||||
self.assertEqual(
|
||||
mail.failure_reason,
|
||||
"Error without exception. Probably due to sending "
|
||||
"an email without computed recipients."
|
||||
)
|
||||
self.assertEqual(mail.state, "outgoing")
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
class TestMailMailStableSelection(TransactionCase):
|
||||
"""Only relevant in stable as a hotfix. May be removed in master."""
|
||||
|
||||
def test_mail_mail_stable_selection(self):
|
||||
# remove all selections
|
||||
message_type_selections = self.env['ir.model.fields']._get('mail.message', 'message_type').selection_ids
|
||||
message_type_selections.filtered(lambda s: s.value == 'auto_comment').unlink()
|
||||
self.env['mail.mail']._fields_get_message_type_update_selection(self.env['mail.message']._fields['message_type'].selection)
|
||||
# force convert to cache with specific language so it has to fetch related from DB
|
||||
mail = self.env['mail.mail'].create({'subject': 'test', 'message_type': 'auto_comment'})
|
||||
mail.invalidate_recordset(['message_type'])
|
||||
self.assertEqual(mail.with_context(lang="en_US").message_type, 'auto_comment')
|
||||
64
odoo-bringout-oca-ocb-mail/mail/tests/test_mail_message.py
Normal file
64
odoo-bringout-oca-ocb-mail/mail/tests/test_mail_message.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# from odoo import exceptions
|
||||
from odoo.addons.mail.tests import common
|
||||
from odoo.tests import new_test_user, tagged, users
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install", "mail_message")
|
||||
class TestMailMessage(common.MailCommon):
|
||||
|
||||
@users("employee")
|
||||
def test_can_star_message_without_write_access(self):
|
||||
message = self.env["mail.message"].sudo().create({
|
||||
"author_id": self.partner_admin.id,
|
||||
"model": "res.partner",
|
||||
"res_id": self.partner_admin.id,
|
||||
"body": "Hey this is me!",
|
||||
})
|
||||
message = message.sudo(False)
|
||||
self.env.user.group_ids -= self.env.ref("base.group_partner_manager")
|
||||
self.assertFalse(message.has_access("write"))
|
||||
message.toggle_message_starred()
|
||||
self.assertIn(self.env.user.partner_id, message.starred_partner_ids)
|
||||
self.env["mail.message"].unstar_all()
|
||||
self.assertNotIn(self.env.user.partner_id, message.starred_partner_ids)
|
||||
|
||||
def test_mail_message_read_inexisting(self):
|
||||
inexisting_message = self.env['mail.message'].with_user(self.user_employee).browse(-434264)
|
||||
self.assertFalse(inexisting_message.exists())
|
||||
self.assertTrue(inexisting_message.browse().has_access('read'), 'Should not crash (can read void)')
|
||||
# TDE to check: cache pollution / inexisting not correctly tracked, ok-ish for stable
|
||||
# with self.assertRaises(exceptions.AccessError):
|
||||
# inexisting_message.check_access_rule('read')
|
||||
|
||||
def test_mail_message_read_access(self):
|
||||
self.env['res.company'].invalidate_model(['name'])
|
||||
message_c1 = self._add_messages(self.env.company, "Company Note 1", author=self.user_employee.partner_id)
|
||||
message_c2 = self._add_messages(self.company_2, "Company Note 2", author=self.user_employee_c2.partner_id)
|
||||
search_result = self.env["mail.message"].with_context(
|
||||
allowed_company_ids=[self.env.company.id]
|
||||
).with_user(self.user_employee).search([("model", "=", "res.company")])
|
||||
self.assertIn(message_c1, search_result)
|
||||
self.assertNotIn(message_c2, search_result)
|
||||
|
||||
def test_unlink_failure_message_notify_author(self):
|
||||
recipient = new_test_user(self.env, login="Bob", email="invalid_email_addr")
|
||||
with self.mock_mail_gateway():
|
||||
message = self.env.user.partner_id.message_post(
|
||||
body="Hello world!", partner_ids=recipient.partner_id.ids
|
||||
)
|
||||
self.assertEqual(message.notification_ids.failure_type, "mail_email_invalid")
|
||||
self.assertEqual(message.notification_ids.res_partner_id, recipient.partner_id)
|
||||
self.assertEqual(message.notification_ids.author_id, self.env.user.partner_id)
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, "res.partner", recipient.partner_id.id),
|
||||
(self.cr.dbname, "res.partner", self.env.user.partner_id.id),
|
||||
],
|
||||
[
|
||||
{"type": "mail.message/delete", "payload": {"message_ids": [message.id]}},
|
||||
{"type": "mail.message/delete", "payload": {"message_ids": [message.id]}},
|
||||
],
|
||||
):
|
||||
message.unlink()
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import requests
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.tests.common import JsonRpcException, new_test_user, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
SAMPLE = {
|
||||
"text": "<p>Al mal tiempo, buena cara.</p>",
|
||||
"src": "es",
|
||||
"en": "<p>To bad weather, good face.</p>",
|
||||
"fr": "<p>Au mauvais temps, bonne tête.</p>",
|
||||
"nl": "<script src='xss-min.js'/><p onclick='XSS()'>Bij slecht weer, goed gezicht.</p>",
|
||||
"lang": {
|
||||
"fr": "espagnol",
|
||||
"en": "Spanish",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def mock_response(fun):
|
||||
def wrapper(self, url, data=False, params=False, timeout=5):
|
||||
response = requests.Response()
|
||||
response.status_code = HTTPStatus.OK
|
||||
content = {"data": fun(self, url=url, data=data, params=params)}
|
||||
if not content["data"]:
|
||||
response.status_code = HTTPStatus.BAD_REQUEST
|
||||
content = {"error": {"message": "Mocked Error"}}
|
||||
response._content = json.dumps(content).encode()
|
||||
return response
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# Google Cloud Translation Documentation: https://cloud.google.com/translate/docs/reference/api-overview?hl=en
|
||||
@tagged("post_install", "-at_install", "mail_message")
|
||||
class TestTranslationController(HttpCaseWithUserDemo):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env["res.lang"]._activate_lang("fr_FR")
|
||||
cls.env["res.lang"]._activate_lang("en_US")
|
||||
cls.env.ref("base.user_admin").write({"lang": "fr_FR"})
|
||||
cls.api_key = "VALIDKEY"
|
||||
cls.env["ir.config_parameter"].set_param("mail.google_translate_api_key", cls.api_key)
|
||||
cls.message = cls.env["mail.message"].create(
|
||||
{
|
||||
"body": SAMPLE["text"],
|
||||
"model": "res.partner",
|
||||
"res_id": cls.env.ref("base.user_admin").partner_id.id,
|
||||
"message_type": "comment",
|
||||
}
|
||||
)
|
||||
cls.request_count = 0
|
||||
|
||||
@mock_response
|
||||
def _patched_post(self, url, data, params, timeout=5):
|
||||
self.request_count += 1
|
||||
if f"/v2/detect?key={self.api_key}" in url:
|
||||
result = {
|
||||
"language": SAMPLE["src"],
|
||||
"isReliable": True,
|
||||
"confidence": 0.98,
|
||||
}
|
||||
return {"detections": [[result]]}
|
||||
if f"/v2/?key={self.api_key}" in url:
|
||||
return {"translations": [{"translatedText": SAMPLE[data.get("target")]}]}
|
||||
|
||||
def _mock_translation_request(self, data):
|
||||
with patch.object(requests, "post", self._patched_post):
|
||||
return self.make_jsonrpc_request("/mail/message/translate", data)
|
||||
|
||||
def test_update_message(self):
|
||||
self.authenticate("admin", "admin")
|
||||
result = self._mock_translation_request({"message_id": self.message.id})
|
||||
self.assertFalse(result.get("error"))
|
||||
self.assertEqual(self.env["mail.message.translation"].search_count([]), 1)
|
||||
# The translation records should not be discarded if the body did not change.
|
||||
self.make_jsonrpc_request(
|
||||
"/mail/message/update_content",
|
||||
{"message_id": self.message.id, "update_data": {"body": None, "attachment_ids": []}},
|
||||
)
|
||||
self.assertEqual(self.env["mail.message.translation"].search_count([]), 1)
|
||||
self.make_jsonrpc_request(
|
||||
"/mail/message/update_content",
|
||||
{
|
||||
"message_id": self.message.id,
|
||||
"update_data": {"body": "update", "attachment_ids": []},
|
||||
},
|
||||
)
|
||||
self.assertFalse(self.env["mail.message.translation"].search_count([]))
|
||||
|
||||
def test_translation_multi_users(self):
|
||||
new_test_user(self.env, "user_test_fr", groups="base.group_user", lang="fr_FR")
|
||||
new_test_user(self.env, "user_test_en", groups="base.group_user", lang="en_US")
|
||||
for login, target_lang in [("user_test_fr", "fr"), ("user_test_en", "en"), ("admin", "fr")]:
|
||||
self.authenticate(login, login)
|
||||
result = self._mock_translation_request({"message_id": self.message.id})
|
||||
self.assertFalse(result.get("error"))
|
||||
self.assertEqual(result["body"], SAMPLE[target_lang])
|
||||
self.assertEqual(result["lang_name"], SAMPLE["lang"][target_lang])
|
||||
# There is one translation record per target language.
|
||||
self.assertEqual(self.env["mail.message.translation"].search_count([]), 2)
|
||||
# No API request should be sent if a translation value or source already exists.
|
||||
self.assertEqual(self.request_count, 3)
|
||||
|
||||
def test_invalid_api_key(self):
|
||||
self.env["ir.config_parameter"].set_param("mail.google_translate_api_key", "INVALIDKEY")
|
||||
self.authenticate("demo", "demo")
|
||||
result = self._mock_translation_request({"message_id": self.message.id})
|
||||
self.assertNotIn("body", result)
|
||||
self.assertNotIn("lang_name", result)
|
||||
self.assertTrue(result["error"])
|
||||
|
||||
def test_html_sanitization(self):
|
||||
self.env["res.lang"]._activate_lang("nl_NL")
|
||||
new_test_user(self.env, "user_test_nl", groups="base.group_user", lang="nl_NL")
|
||||
self.authenticate("user_test_nl", "user_test_nl")
|
||||
result = self._mock_translation_request({"message_id": self.message.id})
|
||||
self.assertFalse(result.get("error"))
|
||||
self.assertHTMLEqual(result["body"], "<p>Bij slecht weer, goed gezicht.</p>")
|
||||
translation = self.env["mail.message.translation"].search([])
|
||||
self.assertEqual(len(translation), 1)
|
||||
self.assertHTMLEqual(translation.body, "<p>Bij slecht weer, goed gezicht.</p>")
|
||||
|
||||
def test_access_right(self):
|
||||
with self.assertRaises(JsonRpcException, msg="odoo.http.SessionExpiredException"):
|
||||
self._mock_translation_request({"message_id": self.message.id})
|
||||
new_test_user(self.env, "user_test_portal", groups="base.group_portal", lang="fr_FR")
|
||||
self.authenticate("user_test_portal", "user_test_portal")
|
||||
with self.assertRaises(JsonRpcException, msg="odoo.exceptions.AccessError"), mute_logger("odoo.http"):
|
||||
self._mock_translation_request({"message_id": self.message.id})
|
||||
|
||||
def test_unknown_language(self):
|
||||
self.authenticate("admin", "admin")
|
||||
with patch.dict(SAMPLE, {"src": "unknown_by_babel_but_known_by_google_api"}):
|
||||
result = self._mock_translation_request({"message_id": self.message.id})
|
||||
self.assertEqual(result["body"], "<p>Au mauvais temps, bonne tête.</p>")
|
||||
26
odoo-bringout-oca-ocb-mail/mail/tests/test_mail_presence.py
Normal file
26
odoo-bringout-oca-ocb-mail/mail/tests/test_mail_presence.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.tests import HttpCase, tagged, new_test_user
|
||||
from ..models.mail_presence import PRESENCE_OUTDATED_TIMER
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestMailPresence(HttpCase):
|
||||
def test_bus_presence_auto_vacuum(self):
|
||||
user = new_test_user(self.env, login="bob_user")
|
||||
more_than_away_timer_ago = datetime.now() - timedelta(seconds=PRESENCE_OUTDATED_TIMER + 1)
|
||||
more_than_away_timer_ago = more_than_away_timer_ago.replace(microsecond=0)
|
||||
with freeze_time(more_than_away_timer_ago):
|
||||
self.env["mail.presence"]._update_presence(user)
|
||||
self.assertEqual(user.presence_ids.last_poll, more_than_away_timer_ago)
|
||||
self.env["mail.presence"]._gc_bus_presence()
|
||||
self.assertFalse(user.presence_ids)
|
||||
|
||||
def test_im_status_invalidation(self):
|
||||
bob_user = new_test_user(self.env, login="bob_user")
|
||||
self.assertEqual(bob_user.im_status, "offline")
|
||||
self.env["mail.presence"]._update_presence(bob_user)
|
||||
self.assertEqual(bob_user.im_status, "online")
|
||||
|
|
@ -4,9 +4,10 @@
|
|||
from markupsafe import Markup
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.mail.tests import common
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged, users
|
||||
from odoo.tests import Form, tagged, users
|
||||
|
||||
|
||||
class TestMailRenderCommon(common.MailCommon):
|
||||
|
|
@ -41,6 +42,9 @@ class TestMailRenderCommon(common.MailCommon):
|
|||
<h1>This is a test</h1>
|
||||
""",
|
||||
"""<b>Test</b>{{ '' if True else '<b>Code not executed</b>' }}""",
|
||||
"""<b>Test</b> {{ '' ||| Bob }}""",
|
||||
"""<b>Test</b> {{ '' ||| Bob }} |||""",
|
||||
"""<b>Test</b> {{ '' ||| Bob }} ||| }}""",
|
||||
]
|
||||
cls.base_inline_template_bits_fr = [
|
||||
'<p>Bonjour</p>',
|
||||
|
|
@ -89,7 +93,15 @@ class TestMailRenderCommon(common.MailCommon):
|
|||
'<p>Hello %s</p>' % cls.render_object.name,
|
||||
"""<p>
|
||||
<span>English Speaker</span>
|
||||
</p>"""
|
||||
</p>""",
|
||||
"""
|
||||
<p>26</p>
|
||||
<h1>This is a test</h1>
|
||||
""",
|
||||
"""<b>Test</b>""",
|
||||
"""<b>Test</b> Bob """,
|
||||
"""<b>Test</b> Bob |||""",
|
||||
"""<b>Test</b> Bob ||| }}"""
|
||||
]
|
||||
cls.base_rendered_fr = [
|
||||
'<p>Bonjour</p>',
|
||||
|
|
@ -98,6 +110,13 @@ class TestMailRenderCommon(common.MailCommon):
|
|||
<span>Autre Narrateur</span>
|
||||
</p>"""
|
||||
]
|
||||
cls.base_rendered_void = [
|
||||
'<p>Hello</p>',
|
||||
'<p>Hello </p>',
|
||||
"""<p>
|
||||
<span>English Speaker</span>
|
||||
</p>"""
|
||||
]
|
||||
|
||||
# link to mail template
|
||||
cls.test_template = cls.env['mail.template'].create({
|
||||
|
|
@ -105,7 +124,8 @@ class TestMailRenderCommon(common.MailCommon):
|
|||
'subject': cls.base_inline_template_bits[0],
|
||||
'body_html': cls.base_qweb_bits[1],
|
||||
'model_id': cls.env['ir.model']._get('res.partner').id,
|
||||
'lang': '{{ object.lang }}'
|
||||
'lang': '{{ object.lang }}',
|
||||
'use_default_to': True,
|
||||
})
|
||||
|
||||
# some translations
|
||||
|
|
@ -131,8 +151,8 @@ class TestMailRenderCommon(common.MailCommon):
|
|||
notification_type='inbox',
|
||||
signature='--\nErnest'
|
||||
)
|
||||
cls.user_rendering_restricted.groups_id -= cls.env.ref('mail.group_mail_template_editor')
|
||||
cls.user_employee.groups_id += cls.env.ref('mail.group_mail_template_editor')
|
||||
cls.env.ref('mail.group_mail_template_editor').write({'implied_by_ids': [Command.clear()]})
|
||||
cls.user_employee.group_ids += cls.env.ref('mail.group_mail_template_editor')
|
||||
|
||||
|
||||
@tagged('mail_render')
|
||||
|
|
@ -172,7 +192,7 @@ class TestMailRender(TestMailRenderCommon):
|
|||
preview = 'foo{{"false" if 1 > 2 else "true"}}bar'
|
||||
result = self.env['mail.render.mixin']._prepend_preview(Markup(body), preview)
|
||||
self.assertEqual(result, '''<div style="display:none;font-size:1px;height:0px;width:0px;opacity:0;">
|
||||
foo<t t-out=""false" if 1 > 2 else "true""/>bar
|
||||
foo<t t-out=""false" if 1 > 2 else "true""></t>bar
|
||||
</div>body''')
|
||||
|
||||
@users('employee')
|
||||
|
|
@ -200,6 +220,43 @@ class TestMailRender(TestMailRenderCommon):
|
|||
)[partner.id]
|
||||
self.assertEqual(rendered, expected)
|
||||
|
||||
@users('employee')
|
||||
def test_render_field_no_records(self):
|
||||
""" Test rendering on void IDs, or a list with dummy / falsy ID """
|
||||
template = self.test_template.with_env(self.env)
|
||||
partner = self.render_object.with_env(self.env)
|
||||
for res_ids in ([], (), [False], [''], [None], [False, partner.id]): # various corner cases
|
||||
for fname, expected_obj, expected_void in zip(['subject', 'body_html'], self.base_rendered, self.base_rendered_void):
|
||||
with self.subTest():
|
||||
rendered_all = template._render_field(
|
||||
fname,
|
||||
res_ids,
|
||||
compute_lang=True
|
||||
)
|
||||
if res_ids:
|
||||
self.assertTrue(res_ids[0] in rendered_all,
|
||||
f'Rendering: key {repr(res_ids[0])} is considered as valid and should have an entry')
|
||||
self.assertEqual(rendered_all[res_ids[0]], expected_void)
|
||||
if len(res_ids) == 2: # second is partner
|
||||
self.assertTrue(res_ids[1] in rendered_all)
|
||||
self.assertEqual(rendered_all[res_ids[1]], expected_obj)
|
||||
if not res_ids:
|
||||
self.assertFalse(rendered_all,
|
||||
'Rendering: void input -> void output')
|
||||
|
||||
@users('employee')
|
||||
def test_render_field_not_existing(self):
|
||||
""" Test trying to render a not-existing field: raise a proper ValueError
|
||||
instead of crashing / raising a KeyError """
|
||||
template = self.env['mail.template'].browse(self.test_template.ids)
|
||||
partner = self.env['res.partner'].browse(self.render_object_fr.ids)
|
||||
with self.assertRaises(ValueError):
|
||||
_rendered = template._render_field(
|
||||
'not_existing',
|
||||
partner.ids,
|
||||
compute_lang=True
|
||||
)[partner.id]
|
||||
|
||||
@users('employee')
|
||||
def test_render_template_inline_template(self):
|
||||
partner = self.env['res.partner'].browse(self.render_object.ids)
|
||||
|
|
@ -219,7 +276,7 @@ class TestMailRender(TestMailRenderCommon):
|
|||
partner_ids = self.env['res.partner'].sudo().create([{
|
||||
'name': f'test partner {n}'
|
||||
} for n in range(20)]).ids
|
||||
with patch('odoo.models.Model.get_base_url', new=_mock_get_base_url), self.assertQueryCount(7):
|
||||
with patch('odoo.models.Model.get_base_url', new=_mock_get_base_url), self.assertQueryCount(13):
|
||||
# make sure name isn't already in cache
|
||||
self.env['res.partner'].browse(partner_ids).invalidate_recordset(['name', 'display_name'])
|
||||
render_results = self.env['mail.render.mixin']._render_template(
|
||||
|
|
@ -227,7 +284,7 @@ class TestMailRender(TestMailRenderCommon):
|
|||
'res.partner',
|
||||
partner_ids,
|
||||
engine='inline_template',
|
||||
post_process=True,
|
||||
options={'post_process': True},
|
||||
)
|
||||
Partner = self.env['res.partner'].with_prefetch(partner_ids)
|
||||
for partner_id, render_result in render_results.items():
|
||||
|
|
@ -355,6 +412,107 @@ class TestMailRender(TestMailRenderCommon):
|
|||
self.assertEqual(rendered, expected)
|
||||
|
||||
|
||||
@tagged('mail_render', 'regex_render')
|
||||
class TestRegexRendering(common.MailCommon):
|
||||
|
||||
def test_qweb_regex_rendering(self):
|
||||
record = self.env['res.partner'].create({'name': 'Alice'})
|
||||
|
||||
def render(template):
|
||||
return self.env['mail.render.mixin']._render_template_qweb(template, 'res.partner', record.ids)[record.id]
|
||||
|
||||
static_templates = (
|
||||
('''<h1> Title </h1>''', '<h1> Title </h1>'),
|
||||
('''<p t-out="object.name"/>''', '<p>Alice</p>'),
|
||||
('''<p t-out="object.name"></p>''', '<p>Alice</p>'),
|
||||
('''<P t-out="object.name" ></p >''', '<p>Alice</p>'),
|
||||
('''<t t-out="object.name"/>''', 'Alice'),
|
||||
('''<T t-out="object.name"/>''', 'Alice'),
|
||||
('''<div><T t-out="object.name"/></div>''', '<div>Alice</div>'),
|
||||
('''<h1 t-out="object.name"/>''', '<h1>Alice</h1>'),
|
||||
('''<p t-out='object.name'/>''', '<p>Alice</p>'),
|
||||
('''<p t-out="object.contact_name"/>''', '<p></p>'),
|
||||
('''<p t-out="object.name">Default</p>''', '<p>Alice</p>'),
|
||||
('''<p t-out='object.name'>Default</p>''', '<p>Alice</p>'),
|
||||
('''<p t-out="object.contact_name">Default</p>''', '<p>Default</p>'),
|
||||
('''<p t-out="object.name"/><p t-out="object.name">Default</p>''', '<p>Alice</p><p>Alice</p>'),
|
||||
('''<p t-out="object.name"/><p t-out="object.contact_name">Default</p>''', '<p>Alice</p><p>Default</p>'),
|
||||
('''<p
|
||||
t-out="object.name"
|
||||
/>''', '<p>Alice</p>'),
|
||||
('''<p
|
||||
t-out="object.contact_name"
|
||||
>
|
||||
Default
|
||||
</p>''', '<p>Default</p>'),
|
||||
('''<div><p t-out="object.name"/></div>''', '<div><p>Alice</p></div>'),
|
||||
('''<div/aa t-out="object.name"></div/aa>''', '<div>Alice</div>'),
|
||||
('''<div/aa='x' t-out="object.name"></div/aa='x'>''', '<div>Alice</div>'),
|
||||
)
|
||||
o_qweb_render = self.env['ir.qweb']._render
|
||||
for template, expected in static_templates:
|
||||
with (patch('odoo.addons.base.models.ir_qweb.IrQweb._render', side_effect=o_qweb_render) as qweb_render,
|
||||
patch('odoo.addons.base.models.ir_qweb.unsafe_eval', side_effect=eval) as unsafe_eval):
|
||||
self.assertEqual(render(template), expected)
|
||||
self.assertFalse(qweb_render.called)
|
||||
self.assertFalse(unsafe_eval.called)
|
||||
|
||||
with (patch('odoo.addons.base.models.ir_qweb.IrQweb._render', side_effect=o_qweb_render) as qweb_render,
|
||||
patch('odoo.addons.base.models.ir_qweb.unsafe_eval', side_effect=eval) as unsafe_eval):
|
||||
self.assertNotIn("<55", render('''<55 t-out="object.name"></55>'''))
|
||||
self.assertFalse(qweb_render.called)
|
||||
self.assertFalse(unsafe_eval.called)
|
||||
|
||||
# double check that we are able to catch the eval
|
||||
non_static_templates = (
|
||||
('''<p t-out=""/>''', '<p>()</p>'),
|
||||
('''<p t-out="1+1"/>''', '<p>2</p>'),
|
||||
('''<p t-out="env.context.get('test')"/>''', ''),
|
||||
('''<p t-out="object.name" title="Test"/>''', '<p title="Test">Alice</p>'),
|
||||
('''<p title="Test" t-out="object.name"/>''', '<p title="Test">Alice</p>'),
|
||||
('''<p t-out="object.name"><img/></p>''', '<p>Alice</p>'),
|
||||
('''<p t-out="object.parent_id.name"><img/></p>''', '<p><img/></p>'),
|
||||
('''<p t-out="'<h1>test</h1>'"/>''', '<p><h1>test</h1></p>'),
|
||||
)
|
||||
for template, expected in non_static_templates:
|
||||
with (patch('odoo.addons.base.models.ir_qweb.IrQweb._render', side_effect=o_qweb_render) as qweb_render,
|
||||
patch('odoo.addons.base.models.ir_qweb.unsafe_eval', side_effect=eval) as unsafe_eval):
|
||||
rendered = render(template)
|
||||
self.assertTrue(isinstance(rendered, Markup))
|
||||
self.assertEqual(rendered, expected)
|
||||
self.assertTrue(qweb_render.called)
|
||||
self.assertTrue(unsafe_eval.called)
|
||||
|
||||
def test_inline_regex_rendering(self):
|
||||
record = self.env['res.partner'].create({'name': 'Alice'})
|
||||
|
||||
def render(template):
|
||||
return self.env['mail.render.mixin']._render_template_inline_template(template, 'res.partner', record.ids)[record.id]
|
||||
|
||||
static_templates = (
|
||||
('''{{object.name}}''', 'Alice'),
|
||||
('''{{object.contact_name}}''', ''),
|
||||
('''{{object.name ||| Default}}''', 'Alice'),
|
||||
('''{{object.contact_name ||| Default}}''', 'Default'),
|
||||
)
|
||||
for template, expected in static_templates:
|
||||
with patch('odoo.tools.safe_eval.unsafe_eval', side_effect=eval) as unsafe_eval:
|
||||
self.assertEqual(render(template), expected)
|
||||
self.assertFalse(unsafe_eval.called)
|
||||
self.assertFalse(self.env['mail.render.mixin']._has_unsafe_expression_template_inline_template(template, 'res.partner'))
|
||||
|
||||
non_static_templates = (
|
||||
('''{{''}}''', ''),
|
||||
('''{{1+1}}''', '2'),
|
||||
('''{{object.env.context.get('test')}}''', ''),
|
||||
)
|
||||
for template, expected in non_static_templates:
|
||||
with patch('odoo.tools.safe_eval.unsafe_eval', side_effect=eval) as unsafe_eval:
|
||||
self.assertEqual(render(template), expected)
|
||||
self.assertTrue(unsafe_eval.called)
|
||||
self.assertTrue(self.env['mail.render.mixin']._has_unsafe_expression_template_inline_template(template, 'res.partner'))
|
||||
|
||||
|
||||
@tagged('mail_render')
|
||||
class TestMailRenderSecurity(TestMailRenderCommon):
|
||||
""" Test security of rendering, based on qweb finding + restricted rendering
|
||||
|
|
@ -415,6 +573,50 @@ class TestMailRenderSecurity(TestMailRenderCommon):
|
|||
)[res_ids[0]]
|
||||
self.assertIn('26', result, 'Template Editor should be able to render inline_template code')
|
||||
|
||||
@users('user_rendering_restricted')
|
||||
def test_render_restricted_allow_template_defaults(self):
|
||||
"""Check that default template values are implicitly allowed for the specific field they define."""
|
||||
def patched_mail_template_default_values(model):
|
||||
return {
|
||||
'email_cc': '{{ object.user_ids and object.user_ids[0].email }}', # inline
|
||||
'lang': '{{ object.user_ids and object.user_ids[0].lang }}', # inline
|
||||
'body_html': '<p>Hi <t t-out="object.user_ids and object.user_ids[0].name"/></p>', # qweb
|
||||
}
|
||||
template_defaults = patched_mail_template_default_values(self.env['mail.template'])
|
||||
partner_model_id = self.env['ir.model']._get_id('res.partner')
|
||||
|
||||
# check no default
|
||||
template = Form(self.env['mail.template'].with_context({
|
||||
'default_name': 'test_allow_template_defaults_nodefault_valid',
|
||||
'default_model_id': partner_model_id,
|
||||
}))
|
||||
template = template.save()
|
||||
self.assertFalse(template.lang)
|
||||
self.assertFalse(template.email_cc)
|
||||
self.assertFalse(template.body_html)
|
||||
|
||||
# sanity check, make sure the expressions are not allowed before the test (not in default allow list, etc...)
|
||||
with self.assertRaises(AccessError, msg="Complex inline expression should fail if it is not the default."):
|
||||
template.lang = template_defaults['lang']
|
||||
with self.assertRaises(AccessError, msg="Complex qweb expression should fail if it is not the default."):
|
||||
template.body_html = template_defaults['body_html']
|
||||
|
||||
with patch(
|
||||
'odoo.addons.base.models.res_partner.ResPartner._mail_template_default_values',
|
||||
new=patched_mail_template_default_values, create=True,
|
||||
):
|
||||
template = Form(self.env['mail.template'].with_context({
|
||||
'default_name': 'test_allow_template_with_default',
|
||||
'default_model_id': partner_model_id,
|
||||
}))
|
||||
template = template.save()
|
||||
self.assertEqual(template.lang, template_defaults['lang'])
|
||||
self.assertEqual(template.email_cc, template_defaults['email_cc'])
|
||||
self.assertEqual(template.body_html, template_defaults['body_html'])
|
||||
|
||||
with self.assertRaises(AccessError, msg="Complex expressions should only be allowed if they are the default for that field."):
|
||||
template.email_cc = template_defaults['lang']
|
||||
|
||||
@users('user_rendering_restricted')
|
||||
def test_render_template_qweb_restricted(self):
|
||||
model = 'res.partner'
|
||||
|
|
@ -480,8 +682,8 @@ class TestMailRenderSecurity(TestMailRenderCommon):
|
|||
def test_security_qweb_template_restricted(self):
|
||||
"""Test if we correctly detect condition block (which might contains code)."""
|
||||
res_ids = self.env['res.partner'].search([], limit=1).ids
|
||||
with self.assertRaises(AccessError, msg='Simple user should not be able to render qweb code'):
|
||||
self.env['mail.render.mixin']._render_template_qweb(self.base_qweb_bits[1], 'res.partner', res_ids)
|
||||
with self.assertRaises(AccessError, msg='Simple user should not be able to render complex qweb code'):
|
||||
self.env['mail.render.mixin']._render_template_qweb(self.base_qweb_bits[2], 'res.partner', res_ids)
|
||||
|
||||
@users('user_rendering_restricted')
|
||||
def test_security_qweb_template_restricted_cached(self):
|
||||
|
|
@ -489,13 +691,15 @@ class TestMailRenderSecurity(TestMailRenderCommon):
|
|||
res_ids = self.env['res.partner'].search([], limit=1).ids
|
||||
|
||||
# Render with the admin first to fill the cache
|
||||
self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_qweb(
|
||||
self.base_qweb_bits[1], 'res.partner', res_ids)
|
||||
result = self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_qweb(
|
||||
self.base_qweb_bits[2], 'res.partner', res_ids)
|
||||
|
||||
self.assertEqual(result[res_ids[0]], "<p>\n <span>English Speaker</span>\n</p>")
|
||||
|
||||
# Check that it raise even when rendered previously by an admin
|
||||
with self.assertRaises(AccessError, msg='Simple user should not be able to render qweb code'):
|
||||
with self.assertRaises(AccessError, msg='Simple user should not be able to render complex qweb code'):
|
||||
self.env['mail.render.mixin']._render_template_qweb(
|
||||
self.base_qweb_bits[1], 'res.partner', res_ids)
|
||||
self.base_qweb_bits[2], 'res.partner', res_ids)
|
||||
|
||||
@users('employee')
|
||||
def test_security_qweb_template_unrestricted(self):
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@
|
|||
from markupsafe import Markup
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.modules.module import get_module_resource
|
||||
from odoo.tests import Form, tagged, users
|
||||
from odoo.tools import convert_file
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.exceptions import AccessError, ValidationError, UserError
|
||||
from odoo.tests import Form, HttpCase, tagged, users
|
||||
from odoo.tools import convert_file, mute_logger
|
||||
|
||||
|
||||
@tagged('mail_template')
|
||||
|
|
@ -18,7 +17,11 @@ class TestMailTemplate(MailCommon):
|
|||
super(TestMailTemplate, cls).setUpClass()
|
||||
# Enable the Jinja rendering restriction
|
||||
cls.env['ir.config_parameter'].set_param('mail.restrict.template.rendering', True)
|
||||
cls.user_employee.groups_id -= cls.env.ref('mail.group_mail_template_editor')
|
||||
cls.user_employee.group_ids -= cls.env.ref('mail.group_mail_template_editor')
|
||||
cls.test_partner = cls.env['res.partner'].create({
|
||||
'email': 'test.rendering@test.example.com',
|
||||
'name': 'Test Rendering',
|
||||
})
|
||||
|
||||
cls.mail_template = cls.env['mail.template'].create({
|
||||
'name': 'Test template',
|
||||
|
|
@ -27,11 +30,67 @@ class TestMailTemplate(MailCommon):
|
|||
'lang': '{{ object.lang }}',
|
||||
'auto_delete': True,
|
||||
'model_id': cls.env.ref('base.model_res_partner').id,
|
||||
'use_default_to': False,
|
||||
})
|
||||
|
||||
cls.user_employee_2 = mail_new_test_user(
|
||||
cls.env,
|
||||
company_id=cls.company_admin.id,
|
||||
email='employee_2@test.com',
|
||||
groups='base.group_user',
|
||||
login='employee_2',
|
||||
name='Albertine Another Employee',
|
||||
)
|
||||
|
||||
@users('admin')
|
||||
@mute_logger('odoo.addons.mail.models.mail_template')
|
||||
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
|
||||
def test_invalid_template_on_save(self):
|
||||
mail_template = self.env['mail.template'].create({
|
||||
'name': 'Test template',
|
||||
'model_id': self.env['ir.model']._get_id('res.users'),
|
||||
'subject': 'Template {{ object.company_id.email }}',
|
||||
'lang': '{{ object.partner_id.lang }}'
|
||||
})
|
||||
|
||||
for fname in [
|
||||
'body_html', 'email_cc', 'email_from', 'email_to',
|
||||
'lang', 'partner_to', 'reply_to', 'scheduled_date',
|
||||
'subject'
|
||||
]:
|
||||
with self.subTest(fname=fname):
|
||||
if fname == 'body_html':
|
||||
value_field = '<p>Hello <t t-out="object.unknown_field"/></p>'
|
||||
value_fun = '<p>Hello <t t-out="object.is_portal_0()"/></p>'
|
||||
else:
|
||||
value_field = '{{ object.unknown_field }}'
|
||||
value_fun = '{{ object.is_portal_0() }}'
|
||||
# cannot update with a wrong field
|
||||
with self.assertRaises(ValidationError):
|
||||
mail_template.write({
|
||||
fname: value_field,
|
||||
})
|
||||
with self.assertRaises(ValidationError):
|
||||
mail_template.write({
|
||||
fname: value_fun,
|
||||
})
|
||||
# Check templates having invalid object references can't be created
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['mail.template'].create({
|
||||
'name': 'Test template',
|
||||
'model_id': self.env['ir.model']._get('res.users').id,
|
||||
fname: value_field,
|
||||
})
|
||||
|
||||
# new model would crash at rendering
|
||||
with self.assertRaises(ValidationError):
|
||||
mail_template.write({
|
||||
'model_id': self.env['ir.model']._get_id('res.partner'),
|
||||
})
|
||||
|
||||
@users('employee')
|
||||
def test_mail_compose_message_content_from_template(self):
|
||||
form = Form(self.env['mail.compose.message'])
|
||||
form = Form(self.env['mail.compose.message'].with_context(default_model='res.partner', active_ids=self.test_partner.ids))
|
||||
form.template_id = self.mail_template
|
||||
mail_compose_message = form.save()
|
||||
|
||||
|
|
@ -46,11 +105,31 @@ class TestMailTemplate(MailCommon):
|
|||
'subject': '{{ 1 + 5 }}',
|
||||
})
|
||||
|
||||
values = mail_compose_message.get_mail_values(self.partner_employee.ids)
|
||||
values = mail_compose_message._prepare_mail_values(self.partner_employee.ids)
|
||||
|
||||
self.assertEqual(values[self.partner_employee.id]['subject'], '6', 'We must trust mail template values')
|
||||
self.assertIn('13', values[self.partner_employee.id]['body_html'], 'We must trust mail template values')
|
||||
|
||||
@users('admin')
|
||||
def test_mail_template_abstract_model(self):
|
||||
"""Check abstract models cannot be set on templates."""
|
||||
# create
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['mail.template'].create({
|
||||
'name': 'Test abstract template',
|
||||
'model_id': self.env['ir.model']._get('mail.thread').id, # abstract model
|
||||
})
|
||||
# write
|
||||
template = self.env['mail.template'].create({
|
||||
'name': 'Test abstract template',
|
||||
'model_id': self.env['ir.model']._get('res.partner').id,
|
||||
})
|
||||
with self.assertRaises(ValidationError):
|
||||
template.write({
|
||||
'name': 'Test abstract template',
|
||||
'model_id': self.env['ir.model']._get('mail.thread').id,
|
||||
})
|
||||
|
||||
def test_mail_template_acl(self):
|
||||
# Sanity check
|
||||
self.assertTrue(self.user_admin.has_group('mail.group_mail_template_editor'))
|
||||
|
|
@ -58,48 +137,198 @@ class TestMailTemplate(MailCommon):
|
|||
self.assertFalse(self.user_employee.has_group('mail.group_mail_template_editor'))
|
||||
self.assertFalse(self.user_employee.has_group('base.group_sanitize_override'))
|
||||
|
||||
model = self.env['ir.model']._get_id('res.users')
|
||||
record = self.user_employee
|
||||
|
||||
# Group System can create / write / unlink mail template
|
||||
mail_template = self.env['mail.template'].with_user(self.user_admin).create({'name': 'Test template'})
|
||||
mail_template = self.env['mail.template'].with_user(self.user_admin).create({
|
||||
'name': 'Test template',
|
||||
'model_id': model,
|
||||
})
|
||||
self.assertEqual(mail_template.name, 'Test template')
|
||||
|
||||
mail_template.with_user(self.user_admin).name = 'New name'
|
||||
self.assertEqual(mail_template.name, 'New name')
|
||||
|
||||
# Standard employee can create and edit non-dynamic templates
|
||||
employee_template = self.env['mail.template'].with_user(self.user_employee).create({'body_html': '<p>foo</p>'})
|
||||
|
||||
employee_template = self.env['mail.template'].with_user(self.user_employee).create({'body_html': '<p>foo</p>', 'model_id': model})
|
||||
employee_template.with_user(self.user_employee).body_html = '<p>bar</p>'
|
||||
|
||||
employee_template = self.env['mail.template'].with_user(self.user_employee).create({'email_to': 'foo@bar.com'})
|
||||
employee_template = self.env['mail.template'].with_user(self.user_employee).create({
|
||||
'email_to': 'foo@bar.com',
|
||||
'model_id': model,
|
||||
})
|
||||
employee_template = employee_template.with_user(self.user_employee)
|
||||
|
||||
employee_template.with_user(self.user_employee).email_to = 'bar@foo.com'
|
||||
employee_template.email_to = 'bar@foo.com'
|
||||
|
||||
# Standard employee cannot create and edit templates with dynamic qweb
|
||||
# Standard employee cannot create and edit templates with forbidden expression
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.template'].with_user(self.user_employee).create({'body_html': '<p t-esc="\'foo\'"></p>'})
|
||||
self.env['mail.template'].with_user(self.user_employee).create({'body_html': '''<p t-out="'foo'"></p>''', 'model_id': model})
|
||||
|
||||
# If no model is specify, he can not write allowed expression
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.template'].with_user(self.user_employee).create({'body_html': '''<p t-out="object.name"></p>'''})
|
||||
|
||||
# Standard employee cannot edit templates from another user, non-dynamic and dynamic
|
||||
with self.assertRaises(AccessError):
|
||||
mail_template.with_user(self.user_employee).body_html = '<p>foo</p>'
|
||||
with self.assertRaises(AccessError):
|
||||
mail_template.with_user(self.user_employee).body_html = '<p t-esc="\'foo\'"></p>'
|
||||
mail_template.with_user(self.user_employee).body_html = '''<p t-out="'foo'"></p>'''
|
||||
|
||||
# Standard employee can edit his own templates if not dynamic
|
||||
employee_template.with_user(self.user_employee).body_html = '<p>foo</p>'
|
||||
employee_template.body_html = '<p>foo</p>'
|
||||
|
||||
# Standard employee cannot create and edit templates with dynamic inline fields
|
||||
with self.assertRaises(AccessError):
|
||||
self.env['mail.template'].with_user(self.user_employee).create({'email_to': '{{ object.partner_id.email }}'})
|
||||
self.env['mail.template'].with_user(self.user_employee).create({'email_to': '{{ object.partner_id.email }}', 'model_id': model})
|
||||
|
||||
# Standard employee cannot edit his own templates if dynamic
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.with_user(self.user_employee).body_html = '<p t-esc="\'foo\'"></p>'
|
||||
employee_template.body_html = '''<p t-out="'foo'"></p>'''
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.with_user(self.user_employee).email_to = '{{ object.partner_id.email }}'
|
||||
forbidden_expressions = (
|
||||
'object.partner_id.email',
|
||||
'object.password',
|
||||
"object.name or (1+1)",
|
||||
'user.password',
|
||||
'object.name or object.name',
|
||||
'[a for a in (1,)]',
|
||||
"object.name or f''",
|
||||
"object.name or ''.format",
|
||||
"object.name or f'{1+1}'",
|
||||
"object.name or len('')",
|
||||
"'abcd' or object.name",
|
||||
"object.name and ''",
|
||||
)
|
||||
for expression in forbidden_expressions:
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.email_to = '{{ %s }}' % expression
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.email_to = '{{ %s ||| Bob}}' % expression
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.body_html = '<p t-out="%s"></p>' % expression
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.body_html = '<p t-esc="%s"></p>' % expression
|
||||
|
||||
# try to cheat with the context
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.with_context(raise_on_forbidden_code=False).email_to = '{{ %s }}' % expression
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.with_context(raise_on_forbidden_code=False).body_html = '<p t-esc="%s"></p>' % expression
|
||||
|
||||
# check that an admin can use the expression
|
||||
mail_template.with_user(self.user_admin).email_to = '{{ %s }}' % expression
|
||||
mail_template.with_user(self.user_admin).email_to = '{{ %s ||| Bob }}' % expression
|
||||
mail_template.with_user(self.user_admin).body_html = '<p t-out="%s">Default</p>' % expression
|
||||
mail_template.with_user(self.user_admin).body_html = '<p t-esc="%s">Default</p>' % expression
|
||||
|
||||
# hide qweb code in t-inner-content
|
||||
code = '''<t t-inner-content="<p t-out='1+11'>Test</p>"></t>'''
|
||||
body = self.env['mail.render.mixin']._render_template_qweb(code, 'res.partner', record.ids)[record.id]
|
||||
self.assertNotIn('12', body)
|
||||
code = '''<t t-inner-content="<p t-out='1+11'>Test</p>"></t>'''
|
||||
body = self.env['mail.render.mixin']._render_template_qweb(code, 'res.partner', record.ids)[record.id]
|
||||
self.assertNotIn('12', body)
|
||||
|
||||
forbidden_qweb_expressions = (
|
||||
'<p t-out="partner_id.name"></p>',
|
||||
'<p t-esc="partner_id.name"></p>',
|
||||
'<p t-debug=""></p>',
|
||||
'<p t-set="x" t-value="object.name"></p>',
|
||||
'<p t-set="x" t-value="object.name"></p>',
|
||||
'<p t-groups="base.group_system"></p>',
|
||||
'<t t-call="template"/>',
|
||||
'<t t-set="namn" t-value="Hello {{world}} !"/>',
|
||||
'<t t-att-test="object.name"/>',
|
||||
'<p t-att-title="object.name"></p>',
|
||||
# allowed expression with other attribute
|
||||
'<p t-out="object.name" title="Test"></p>',
|
||||
# allowed expression with child
|
||||
'<p t-out="object.name"><img/></p>',
|
||||
'<p t-out="object.password"></p>',
|
||||
)
|
||||
for expression in forbidden_qweb_expressions:
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.body_html = expression
|
||||
self.assertTrue(self.env['mail.render.mixin']._has_unsafe_expression_template_qweb(expression, 'res.partner'))
|
||||
|
||||
# allowed expressions
|
||||
allowed_qweb_expressions = (
|
||||
'<p t-out="object.name"></p>',
|
||||
'<p t-out="object.name"></p><img/>',
|
||||
'<p t-out="object.name"></p><img title="Test"/>',
|
||||
'<p t-out="object.name">Default</p>',
|
||||
'<p t-out="object.partner_id.name">Default</p>',
|
||||
|
||||
)
|
||||
o_qweb_render = self.env['ir.qweb']._render
|
||||
for expression in allowed_qweb_expressions:
|
||||
template = self.env['mail.template'].with_user(self.user_employee).create({
|
||||
'body_html': expression,
|
||||
'model_id': model,
|
||||
})
|
||||
self.assertFalse(self.env['mail.render.mixin']._has_unsafe_expression_template_qweb(expression, 'res.partner'))
|
||||
|
||||
with (patch('odoo.addons.base.models.ir_qweb.IrQweb._render', side_effect=o_qweb_render) as qweb_render,
|
||||
patch('odoo.addons.base.models.ir_qweb.unsafe_eval', side_effect=eval) as unsafe_eval):
|
||||
rendered = template._render_field('body_html', record.ids)[record.id]
|
||||
self.assertNotIn('t-out', rendered)
|
||||
self.assertFalse(qweb_render.called)
|
||||
self.assertFalse(unsafe_eval.called)
|
||||
|
||||
# double check that we can detect the qweb rendering
|
||||
mail_template.body_html = '<t t-out="1+1"/>'
|
||||
with (patch('odoo.addons.base.models.ir_qweb.IrQweb._render', side_effect=o_qweb_render) as qweb_render,
|
||||
patch('odoo.addons.base.models.ir_qweb.unsafe_eval', side_effect=eval) as unsafe_eval):
|
||||
rendered = mail_template._render_field('body_html', record.ids)[record.id]
|
||||
self.assertNotIn('t-out', rendered)
|
||||
self.assertTrue(qweb_render.called)
|
||||
self.assertTrue(unsafe_eval.called)
|
||||
|
||||
employee_template.email_to = 'Test {{ object.name }}'
|
||||
with patch('odoo.tools.safe_eval.unsafe_eval', side_effect=eval) as unsafe_eval:
|
||||
employee_template._render_field('email_to', record.ids)
|
||||
self.assertFalse(unsafe_eval.called)
|
||||
|
||||
# double check that we can detect the eval call
|
||||
mail_template.email_to = 'Test {{ 1+1 }}'
|
||||
with patch('odoo.tools.safe_eval.unsafe_eval', side_effect=eval) as unsafe_eval:
|
||||
mail_template._render_field('email_to', record.ids)
|
||||
self.assertTrue(unsafe_eval.called)
|
||||
|
||||
# malformed HTML (html_normalize should prevent the regex rendering on the malformed HTML)
|
||||
templates = (
|
||||
# here sanitizer adds an 'equals void' after object.name as properties
|
||||
# should have values
|
||||
('''<p ou="<p t-out="object.name">"</p>''', '<p ou="<p t-out=" object.name="">"</p>'),
|
||||
('''<p title="'<p t-out='object.name'/>">''', '''<p title="'<p t-out='object.name'/>"></p>'''),
|
||||
)
|
||||
o_render = self.env['mail.render.mixin']._render_template_qweb_regex
|
||||
for template, excepted in templates:
|
||||
mail_template.body_html = template
|
||||
with patch('odoo.addons.mail.models.mail_render_mixin.MailRenderMixin._render_template_qweb_regex', side_effect=o_render) as render:
|
||||
rendered = mail_template._render_field('body_html', record.ids)[record.id]
|
||||
self.assertEqual(rendered, excepted)
|
||||
self.assertTrue(render.called)
|
||||
|
||||
record.name = '<b> test </b>'
|
||||
mail_template.body_html = '<t t-out="object.name"/>'
|
||||
with patch('odoo.addons.mail.models.mail_render_mixin.MailRenderMixin._render_template_qweb_regex', side_effect=o_render) as render:
|
||||
rendered = mail_template._render_field('body_html', record.ids)[record.id]
|
||||
self.assertEqual(rendered, "<b> test </b>")
|
||||
self.assertTrue(render.called)
|
||||
|
||||
# Check that the environment is the evaluation context
|
||||
mail_template.with_user(self.user_admin).email_to = '{{ env.user.name }}'
|
||||
rendered = mail_template._render_field('email_to', record.ids)[record.id]
|
||||
self.assertIn(self.user_admin.name, rendered)
|
||||
|
||||
def test_mail_template_acl_translation(self):
|
||||
''' Test that a user that doenn't have the group_mail_template_editor cannot create / edit
|
||||
''' Test that a user that doesn't have the group_mail_template_editor cannot create / edit
|
||||
translation with dynamic code if he cannot write dynamic code on the related record itself.
|
||||
'''
|
||||
|
||||
|
|
@ -131,9 +360,108 @@ class TestMailTemplate(MailCommon):
|
|||
|
||||
# cannot write dynamic code on mail_template translation for employee without the group mail_template_editor.
|
||||
with self.assertRaises(AccessError):
|
||||
employee_template.with_context(lang='fr_FR').subject = '{{ object.foo }}'
|
||||
employee_template.with_context(lang='fr_FR').subject = '{{ object.city }}'
|
||||
|
||||
employee_template.with_context(lang='fr_FR').sudo().subject = '{{ object.foo }}'
|
||||
employee_template.with_context(lang='fr_FR').sudo().subject = '{{ object.city }}'
|
||||
|
||||
def test_mail_template_copy(self):
|
||||
(self.user_employee + self.user_employee_2).write({
|
||||
'group_ids': [(4, self.env.ref('mail.group_mail_template_editor').id)],
|
||||
})
|
||||
attachment_data_list = self._generate_attachments_data(4, self.mail_template._name, self.mail_template.id)
|
||||
self.mail_template.write({
|
||||
'attachment_ids': [
|
||||
(0, 0, attachment_data)
|
||||
for attachment_data in attachment_data_list[:2]
|
||||
],
|
||||
})
|
||||
original_attachments = self.mail_template.attachment_ids
|
||||
# users access template, can read attachments
|
||||
for test_user in (self.user_employee, self.user_employee_2):
|
||||
with self.subTest(user_name=test_user.name):
|
||||
template = self.mail_template.with_user(test_user)
|
||||
self.assertEqual(
|
||||
set(template.attachment_ids.mapped('name')),
|
||||
{'AttFileName_00.txt', 'AttFileName_01.txt'},
|
||||
)
|
||||
# other template for multi copy support
|
||||
mail_template_2 = self.env['mail.template'].create({
|
||||
'name': 'Test Template 2',
|
||||
})
|
||||
|
||||
# employee make a private copy -> other template should still be readable
|
||||
new_template, new_template_2 = (self.mail_template + mail_template_2).with_user(self.user_employee).copy()
|
||||
new_template.user_id = self.user_employee
|
||||
self.assertEqual(
|
||||
set(new_template.attachment_ids.mapped('name')),
|
||||
{'AttFileName_00.txt', 'AttFileName_01.txt'},
|
||||
)
|
||||
self.assertFalse(
|
||||
new_template.attachment_ids & original_attachments,
|
||||
'Template copy should copy attachments, not keep the same, to avoid ACLs / ownership issues',
|
||||
)
|
||||
self.assertFalse(new_template_2.attachment_ids, 'Should not take attachments from first template in multi copy')
|
||||
self.assertEqual(new_template.name, f'{self.mail_template.name} (copy)', 'Default name should be the old one + copy')
|
||||
self.assertEqual(new_template_2.name, f'{mail_template_2.name} (copy)', 'Default name should be the old one + copy')
|
||||
# linked to their respective template
|
||||
self.assertEqual(new_template.attachment_ids.mapped('res_id'), new_template.ids * 2)
|
||||
self.assertEqual(original_attachments.mapped('res_id'), self.mail_template.ids * 2)
|
||||
|
||||
new_template_as2 = new_template.with_user(self.user_employee_2)
|
||||
self.assertEqual(
|
||||
set(new_template_as2.attachment_ids.mapped('name')),
|
||||
{'AttFileName_00.txt', 'AttFileName_01.txt'},
|
||||
)
|
||||
|
||||
# check default is correctly used instead of copy
|
||||
newer_template, newer_template_2 = (self.mail_template + mail_template_2).with_user(self.user_employee).copy(default={
|
||||
'attachment_ids': [
|
||||
(0, 0, attachment_data_list[2]),
|
||||
(0, 0, attachment_data_list[3]),
|
||||
],
|
||||
'name': 'My Copy',
|
||||
})
|
||||
self.assertEqual(
|
||||
set(newer_template.attachment_ids.mapped('name')),
|
||||
{'AttFileName_02.txt', 'AttFileName_03.txt'},
|
||||
)
|
||||
self.assertEqual(
|
||||
set(newer_template_2.attachment_ids.mapped('name')),
|
||||
{'AttFileName_02.txt', 'AttFileName_03.txt'},
|
||||
)
|
||||
self.assertFalse(
|
||||
newer_template.attachment_ids & (original_attachments & new_template.attachment_ids),
|
||||
'Template copy should copy attachments, not keep the same, to avoid ACLs / ownership issues',
|
||||
)
|
||||
self.assertFalse(
|
||||
newer_template_2.attachment_ids & newer_template.attachment_ids,
|
||||
'Template copy should copy attachments, not keep the same, to avoid ACLs / ownership issues',
|
||||
)
|
||||
self.assertEqual(newer_template.name, 'My Copy', 'Copy should respect given default')
|
||||
self.assertEqual(newer_template_2.name, 'My Copy', 'Copy should respect given default')
|
||||
# linked to their respective template
|
||||
self.assertEqual(newer_template.attachment_ids.mapped('res_id'), newer_template.ids * 2)
|
||||
self.assertEqual(newer_template_2.attachment_ids.mapped('res_id'), newer_template_2.ids * 2)
|
||||
self.assertEqual(newer_template.attachment_ids.mapped('res_model'), [newer_template._name] * 2)
|
||||
self.assertEqual(newer_template.attachment_ids.mapped('res_id'), newer_template.ids * 2)
|
||||
self.assertEqual(newer_template.attachment_ids.mapped('res_model'), [newer_template._name] * 2)
|
||||
self.assertEqual(original_attachments.mapped('res_id'), self.mail_template.ids * 2)
|
||||
self.assertEqual(original_attachments.mapped('res_model'), [self.mail_template._name] * 2)
|
||||
|
||||
def test_mail_template_parse_partner_to(self):
|
||||
for partner_to, expected in [
|
||||
('1', [1]),
|
||||
('1,2,3', [1, 2, 3]),
|
||||
('1, 2, 3', [1, 2, 3]), # remove spaces
|
||||
('[1, 2, 3]', [1, 2, 3]), # %r of a list
|
||||
('(1, 2, 3)', [1, 2, 3]), # %r of a tuple
|
||||
('1,[],2,"3"', [1, 2, 3]), # type tolerant
|
||||
('(1, "wrong", 2, "partner_name", "3")', [1, 2, 3]), # fault tolerant
|
||||
('res.partner(1, 2, 3)', [2]), # invalid input but avoid crash
|
||||
]:
|
||||
with self.subTest(partner_to=partner_to):
|
||||
parsed = self.mail_template._parse_partner_to(partner_to)
|
||||
self.assertListEqual(parsed, expected)
|
||||
|
||||
def test_server_archived_usage_protection(self):
|
||||
""" Test the protection against using archived server (servers used cannot be archived) """
|
||||
|
|
@ -154,13 +482,14 @@ class TestMailTemplate(MailCommon):
|
|||
@tagged('mail_template')
|
||||
class TestMailTemplateReset(MailCommon):
|
||||
|
||||
def _load(self, module, *args):
|
||||
convert_file(self.cr, module='mail',
|
||||
filename=get_module_resource(module, *args),
|
||||
idref={}, mode='init', noupdate=False, kind='test')
|
||||
def _load(self, module, filepath):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
convert_file(self.env, module='mail',
|
||||
filename=filepath,
|
||||
idref={}, mode='init', noupdate=False)
|
||||
|
||||
def test_mail_template_reset(self):
|
||||
self._load('mail', 'tests', 'test_mail_template.xml')
|
||||
self._load('mail', 'tests/test_mail_template.xml')
|
||||
|
||||
mail_template = self.env.ref('mail.mail_template_test').with_context(lang=self.env.user.lang)
|
||||
|
||||
|
|
@ -192,9 +521,9 @@ class TestMailTemplateReset(MailCommon):
|
|||
|
||||
def test_mail_template_reset_translation(self):
|
||||
""" Test if a translated value can be reset correctly when its translation exists/doesn't exist in the po file of the directory """
|
||||
self._load('mail', 'tests', 'test_mail_template.xml')
|
||||
self._load('mail', 'tests/test_mail_template.xml')
|
||||
|
||||
self.env['res.lang']._activate_lang('en_UK')
|
||||
self.env['res.lang']._activate_lang('en_GB')
|
||||
self.env['res.lang']._activate_lang('fr_FR')
|
||||
mail_template = self.env.ref('mail.mail_template_test').with_context(lang='en_US')
|
||||
mail_template.write({
|
||||
|
|
@ -202,7 +531,7 @@ class TestMailTemplateReset(MailCommon):
|
|||
'name': 'Mail: Mail Template',
|
||||
})
|
||||
|
||||
mail_template.with_context(lang='en_UK').write({
|
||||
mail_template.with_context(lang='en_GB').write({
|
||||
'body_html': '<div>Hello UK</div>',
|
||||
'name': 'Mail: Mail Template UK',
|
||||
})
|
||||
|
|
@ -226,24 +555,137 @@ class TestMailTemplateReset(MailCommon):
|
|||
self.assertTrue(reset_action)
|
||||
|
||||
self.assertEqual(mail_template.body_html.strip(), Markup('<div>Hello Odoo</div>'))
|
||||
self.assertEqual(mail_template.with_context(lang='en_UK').body_html.strip(), Markup('<div>Hello Odoo</div>'))
|
||||
self.assertEqual(mail_template.with_context(lang='en_GB').body_html.strip(), Markup('<div>Hello Odoo</div>'))
|
||||
self.assertEqual(mail_template.with_context(lang='fr_FR').body_html.strip(), Markup('<div>Hello Odoo FR</div>'))
|
||||
|
||||
self.assertEqual(mail_template.name, 'Mail: Test Mail Template')
|
||||
self.assertEqual(mail_template.with_context(lang='en_UK').name, 'Mail: Test Mail Template')
|
||||
self.assertEqual(mail_template.with_context(lang='en_GB').name, 'Mail: Test Mail Template')
|
||||
self.assertEqual(mail_template.with_context(lang='fr_FR').name, 'Mail: Test Mail Template FR')
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestConfigRestrictEditor(MailCommon):
|
||||
@tagged("mail_template", "-at_install", "post_install")
|
||||
class TestMailTemplateUI(HttpCase):
|
||||
|
||||
def test_mail_template_dynamic_placeholder_tour(self):
|
||||
# keep debug for technical fields visibility
|
||||
self.start_tour('/odoo?debug=1', 'mail_template_dynamic_placeholder_tour', login='admin')
|
||||
|
||||
|
||||
@tagged("mail_template", "-at_install", "post_install")
|
||||
class TestTemplateConfigRestrictEditor(MailCommon):
|
||||
|
||||
def test_switch_icp_value(self):
|
||||
# Sanity check
|
||||
group = self.env.ref('mail.group_mail_template_editor')
|
||||
|
||||
self.assertTrue(self.user_employee.has_group('mail.group_mail_template_editor'))
|
||||
self.assertFalse(self.user_employee.has_group('base.group_system'))
|
||||
|
||||
# Check that the group is on the user via the settings configuration and not that
|
||||
# the right has been added specifically to this person.
|
||||
self.assertIn(group, self.user_employee.all_group_ids)
|
||||
self.assertNotIn(group, self.user_employee.group_ids)
|
||||
|
||||
self.env['ir.config_parameter'].set_param('mail.restrict.template.rendering', True)
|
||||
self.assertFalse(self.user_employee.has_group('mail.group_mail_template_editor'))
|
||||
|
||||
self.env['ir.config_parameter'].set_param('mail.restrict.template.rendering', False)
|
||||
self.assertTrue(self.user_employee.has_group('mail.group_mail_template_editor'))
|
||||
|
||||
|
||||
@tagged("mail_template", "-at_install", "post_install")
|
||||
class TestSearchTemplateCategory(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
MailTemplate = cls.env['mail.template'].with_context(active_test=False)
|
||||
ModelData = cls.env['ir.model.data']
|
||||
|
||||
cls.existing = MailTemplate.search([])
|
||||
|
||||
# Create templates
|
||||
# 2 Hidden templates
|
||||
cls.hidden_templates = MailTemplate.create([
|
||||
{'name': 'Hidden Template 1', 'active': False},
|
||||
{'name': 'Hidden Template 2', 'description': ''},
|
||||
])
|
||||
last = cls.hidden_templates[-1]
|
||||
ModelData.create({
|
||||
'name': f'mail_template_{last.id}',
|
||||
'module': 'test_module',
|
||||
'model': 'mail.template',
|
||||
'res_id': last.id
|
||||
})
|
||||
|
||||
# 5 Custom templates
|
||||
cls.custom_templates = MailTemplate.create([
|
||||
{'name': f'Custom Template {i + 1}', 'description': f'Desc {i + 1}'}
|
||||
for i in range(4)
|
||||
])
|
||||
cls.custom_templates |= MailTemplate.create({'name': 'Custom Template empty', 'description': ''})
|
||||
|
||||
# 4 Base templates with XML ID
|
||||
cls.base_templates = MailTemplate.create([
|
||||
{'name': f'Base Template {i + 1}', 'description': f'Desc Base {i + 1}'}
|
||||
for i in range(4)
|
||||
])
|
||||
|
||||
for template in cls.base_templates:
|
||||
ModelData.create({
|
||||
'name': f'mail_template_{template.id}',
|
||||
'module': 'test_module',
|
||||
'model': 'mail.template',
|
||||
'res_id': template.id
|
||||
})
|
||||
|
||||
@users('employee')
|
||||
def test_search_template_category(self):
|
||||
MailTemplate = self.env['mail.template'].with_context(active_test=False)
|
||||
|
||||
# Search by hidden templates
|
||||
hidden_domain = [('template_category', 'in', ['hidden_template'])]
|
||||
hidden_templates = MailTemplate.search(hidden_domain) - self.existing
|
||||
self.assertEqual(len(hidden_templates), len(self.hidden_templates), "Hidden templates count mismatch")
|
||||
self.assertEqual(set(hidden_templates.mapped('template_category')), {'hidden_template'}, "Computed field doesn't match 'hidden_template'")
|
||||
|
||||
# Search by base templates
|
||||
base_domain = [('template_category', 'in', ['base_template'])]
|
||||
base_templates = MailTemplate.search(base_domain) - self.existing
|
||||
self.assertEqual(len(base_templates), len(self.base_templates), "Base templates count mismatch")
|
||||
self.assertEqual(set(base_templates.mapped('template_category')), {'base_template'}, "Computed field doesn't match 'base_template'")
|
||||
|
||||
# Search by custom templates
|
||||
custom_domain = [('template_category', 'in', ['custom_template'])]
|
||||
custom_templates = MailTemplate.search(custom_domain) - self.existing
|
||||
self.assertEqual(len(custom_templates), len(self.custom_templates), "Custom templates count mismatch")
|
||||
self.assertEqual(set(custom_templates.mapped('template_category')), {'custom_template'}, "Computed field doesn't match 'custom_template'")
|
||||
|
||||
# Combined search
|
||||
combined_domain = [('template_category', 'in', ['hidden_template', 'base_template', 'custom_template'])]
|
||||
combined_templates = MailTemplate.search(combined_domain) - self.existing
|
||||
total_templates = len(self.hidden_templates) + len(self.base_templates) + len(self.custom_templates)
|
||||
self.assertEqual(len(combined_templates), total_templates, "Combined templates count mismatch")
|
||||
|
||||
# Search with '=' operator
|
||||
hidden_domain = [('template_category', '=', 'hidden_template')]
|
||||
hidden_templates = MailTemplate.search(hidden_domain) - self.existing
|
||||
self.assertEqual(len(hidden_templates), len(self.hidden_templates), "Hidden templates count mismatch")
|
||||
|
||||
# Search with '!=' operator
|
||||
not_in_domain = [('template_category', '!=', 'hidden_template')]
|
||||
not_in_templates = MailTemplate.search(not_in_domain) - self.existing
|
||||
expected_templates = len(self.base_templates) + len(self.custom_templates)
|
||||
self.assertEqual(len(not_in_templates), expected_templates, "Not in templates count mismatch")
|
||||
|
||||
# Search with 'not in' operator
|
||||
not_in_domain = [('template_category', 'not in', ['hidden_template'])]
|
||||
not_in_templates = MailTemplate.search(not_in_domain) - self.existing
|
||||
expected_templates = len(self.base_templates) + len(self.custom_templates)
|
||||
self.assertEqual(len(not_in_templates), expected_templates, "Not in templates count mismatch")
|
||||
|
||||
# Search with 'not in' operator
|
||||
not_in_domain = [('template_category', 'not in', ['hidden_template', 'base_template'])]
|
||||
not_in_templates = MailTemplate.search(not_in_domain) - self.existing
|
||||
expected_templates = len(self.custom_templates)
|
||||
self.assertEqual(len(not_in_templates), expected_templates, "Not in multi templates count mismatch")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="email_from">"{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}></field>
|
||||
<field name="email_to">{{ object.email_formatted }}</field>
|
||||
<field name="use_default_to" eval="False"/>
|
||||
<field name="body_html" type="html">
|
||||
<div>Hello Odoo</div>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import tagged, users
|
||||
from odoo import tools
|
||||
|
||||
|
||||
@tagged('mail_tools')
|
||||
@tagged('mail_tools', 'res_partner')
|
||||
class TestMailTools(MailCommon):
|
||||
|
||||
@classmethod
|
||||
|
|
@ -17,37 +16,12 @@ class TestMailTools(MailCommon):
|
|||
cls.test_partner = cls.env['res.partner'].create({
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'email': cls._test_email,
|
||||
'mobile': '0456001122',
|
||||
'name': 'Alfred Astaire',
|
||||
'phone': '0456334455',
|
||||
})
|
||||
|
||||
cls.sources = [
|
||||
# single email
|
||||
'alfred.astaire@test.example.com',
|
||||
' alfred.astaire@test.example.com ',
|
||||
'Fredo The Great <alfred.astaire@test.example.com>',
|
||||
'"Fredo The Great" <alfred.astaire@test.example.com>',
|
||||
'Fredo "The Great" <alfred.astaire@test.example.com>',
|
||||
# multiple emails
|
||||
'alfred.astaire@test.example.com, evelyne.gargouillis@test.example.com',
|
||||
'Fredo The Great <alfred.astaire@test.example.com>, Evelyne The Goat <evelyne.gargouillis@test.example.com>',
|
||||
'"Fredo The Great" <alfred.astaire@test.example.com>, evelyne.gargouillis@test.example.com',
|
||||
'"Fredo The Great" <alfred.astaire@test.example.com>, <evelyne.gargouillis@test.example.com>',
|
||||
# text containing email
|
||||
'Hello alfred.astaire@test.example.com how are you ?',
|
||||
'<p>Hello alfred.astaire@test.example.com</p>',
|
||||
# text containing emails
|
||||
'Hello "Fredo" <alfred.astaire@test.example.com>, evelyne.gargouillis@test.example.com',
|
||||
'Hello "Fredo" <alfred.astaire@test.example.com> and evelyne.gargouillis@test.example.com',
|
||||
# falsy
|
||||
'<p>Hello Fredo</p>',
|
||||
'j\'adore écrire des @gmail.com ou "@gmail.com" a bit randomly',
|
||||
'',
|
||||
]
|
||||
|
||||
@users('employee')
|
||||
def test_mail_find_partner_from_emails(self):
|
||||
def test_find_partner_from_emails(self):
|
||||
Partner = self.env['res.partner']
|
||||
test_partner = Partner.browse(self.test_partner.ids)
|
||||
self.assertEqual(test_partner.email, self._test_email)
|
||||
|
|
@ -70,9 +44,6 @@ class TestMailTools(MailCommon):
|
|||
# test with wildcard "_"
|
||||
found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com'])
|
||||
self.assertEqual(found, [self.env['res.partner']])
|
||||
# sub-check: this search does not consider _ as a wildcard
|
||||
found = Partner._mail_search_on_partner(['alfred_astaire@test.example.com'])
|
||||
self.assertEqual(found, self.env['res.partner'])
|
||||
|
||||
# test partners with encapsulated emails
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -94,11 +65,43 @@ class TestMailTools(MailCommon):
|
|||
# test with wildcard "_"
|
||||
found = Partner._mail_find_partner_from_emails(['alfred_astaire@test.example.com'])
|
||||
self.assertEqual(found, [self.env['res.partner']])
|
||||
# sub-check: this search does not consider _ as a wildcard
|
||||
found = Partner._mail_search_on_partner(['alfred_astaire@test.example.com'])
|
||||
self.assertEqual(found, self.env['res.partner'])
|
||||
|
||||
@users('admin')
|
||||
def test_mail_find_partner_from_emails_alias_localpart(self):
|
||||
""" Test mail_find_partner_from_emails when dealing with aliases that
|
||||
have alias_incoming_local enabled and emails include the local part. """
|
||||
self.env['mail.alias'].create([{
|
||||
'alias_name': 'test_localpart',
|
||||
'alias_domain_id': self.env.company.alias_domain_id.id,
|
||||
'alias_incoming_local': True,
|
||||
'alias_model_id': self.env.ref('mail.model_res_partner').id,
|
||||
}])
|
||||
|
||||
found = self.env['mail.thread']._partner_find_from_emails_single(['test_localpart@gmail.com'], no_create=False)
|
||||
self.assertFalse(found, f'Found {found.email} / {found.name} instead of empty recordset')
|
||||
|
||||
# limit incoming-compat aliases to a fixed set of domains
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', "tartopoils.com, brutijus.com")
|
||||
for test_email, email_normalized, done in [
|
||||
('"Customer" <test_localpart@gmail.com>', 'test_localpart@gmail.com', True),
|
||||
('"Customer" <test_localpart@tartopoils.com>', 'test_localpart@tartopoils.com', False),
|
||||
('"Customer" <test_localpart@brutijus.com>', 'test_localpart@brutijus.com', False),
|
||||
('"Customer" <test_localpart@brutijus.fr.com>', 'test_localpart@brutijus.fr.com', True),
|
||||
]:
|
||||
with self.subTest(check="Allowed domain support", test_email=test_email):
|
||||
found = self.env['mail.thread']._partner_find_from_emails_single([test_email], no_create=False)
|
||||
if not done:
|
||||
self.assertFalse(found, f'Found {found.email} / {found.name} instead of empty recordset')
|
||||
else:
|
||||
self.assertTrue(found, 'Should have created a partner')
|
||||
self.assertEqual(found.email_normalized, email_normalized)
|
||||
self.assertEqual(found.name, 'Customer')
|
||||
|
||||
found = self.env['mail.thread']._partner_find_from_emails_single(['"Customer" <test_no_localpart@gmail.com>'], no_create=False)
|
||||
self.assertTrue(found, 'Should have created a partner')
|
||||
self.assertEqual(found.email_normalized, 'test_no_localpart@gmail.com')
|
||||
self.assertEqual(found.name, 'Customer')
|
||||
|
||||
@users('employee')
|
||||
def test_mail_find_partner_from_emails_followers(self):
|
||||
""" Test '_mail_find_partner_from_emails' when dealing with records on
|
||||
which followers have to be found based on email. Check multi email
|
||||
|
|
@ -113,19 +116,19 @@ class TestMailTools(MailCommon):
|
|||
test_partner = self.test_partner.with_env(self.env)
|
||||
|
||||
# standard test, no multi-email, to assert base behavior
|
||||
sources = [(self._test_email, True), (self._test_email, False),]
|
||||
expected = [follower_partner, test_partner]
|
||||
for (source, follower_check), expected in zip(sources, expected):
|
||||
cases = [(self._test_email, True), (self._test_email, False)]
|
||||
for source, follower_check in cases:
|
||||
expected_partner = follower_partner if follower_check else test_partner
|
||||
with self.subTest(source=source, follower_check=follower_check):
|
||||
partner = self.env['res.partner']._mail_find_partner_from_emails(
|
||||
[source], records=linked_record if follower_check else None
|
||||
)[0]
|
||||
self.assertEqual(partner, expected)
|
||||
self.assertEqual(partner, expected_partner)
|
||||
|
||||
# formatted email
|
||||
encapsulated_test_email = f'"Robert Astaire" <{self._test_email}>'
|
||||
(follower_partner + test_partner).sudo().write({'email': encapsulated_test_email})
|
||||
sources = [
|
||||
cases = [
|
||||
(self._test_email, True), # normalized
|
||||
(self._test_email, False), # normalized
|
||||
(encapsulated_test_email, True), # encapsulated, same
|
||||
|
|
@ -133,41 +136,34 @@ class TestMailTools(MailCommon):
|
|||
(f'"AnotherName" <{self._test_email}', True), # same normalized, other name
|
||||
(f'"AnotherName" <{self._test_email}', False), # same normalized, other name
|
||||
]
|
||||
expected = [follower_partner, test_partner,
|
||||
follower_partner, test_partner,
|
||||
follower_partner, test_partner,
|
||||
follower_partner, test_partner]
|
||||
for (source, follower_check), expected in zip(sources, expected):
|
||||
for source, follower_check in cases:
|
||||
expected_partner = follower_partner if follower_check else test_partner
|
||||
with self.subTest(source=source, follower_check=follower_check):
|
||||
partner = self.env['res.partner']._mail_find_partner_from_emails(
|
||||
[source], records=linked_record if follower_check else None
|
||||
)[0]
|
||||
self.assertEqual(partner, expected,
|
||||
self.assertEqual(partner, expected_partner,
|
||||
'Mail: formatted email is recognized through usage of normalized email')
|
||||
|
||||
# multi-email
|
||||
_test_email_2 = '"Robert Astaire" <not.alfredoastaire@test.example.com>'
|
||||
(follower_partner + test_partner).sudo().write({'email': f'{self._test_email}, {_test_email_2}'})
|
||||
sources = [
|
||||
(self._test_email, True), # first email
|
||||
(self._test_email, False), # first email
|
||||
(_test_email_2, True), # second email
|
||||
(_test_email_2, False), # second email
|
||||
('not.alfredoastaire@test.example.com', True), # normalized second email in field
|
||||
('not.alfredoastaire@test.example.com', False), # normalized second email in field
|
||||
(f'{self._test_email}, {_test_email_2}', True), # multi-email, both matching, depends on comparison
|
||||
(f'{self._test_email}, {_test_email_2}', False) # multi-email, both matching, depends on comparison
|
||||
cases = [
|
||||
(self._test_email, True, follower_partner), # first email
|
||||
(self._test_email, False, test_partner), # first email
|
||||
(_test_email_2, True, self.env['res.partner']), # second email
|
||||
(_test_email_2, False, self.env['res.partner']), # second email
|
||||
('not.alfredoastaire@test.example.com', True, self.env['res.partner']), # normalized second email in field
|
||||
('not.alfredoastaire@test.example.com', False, self.env['res.partner']), # normalized second email in field
|
||||
(f'{self._test_email}, {_test_email_2}', True, follower_partner), # multi-email, both matching, depends on comparison
|
||||
(f'{self._test_email}, {_test_email_2}', False, test_partner), # multi-email, both matching, depends on comparison
|
||||
]
|
||||
expected = [follower_partner, test_partner,
|
||||
self.env['res.partner'], self.env['res.partner'],
|
||||
self.env['res.partner'], self.env['res.partner'],
|
||||
follower_partner, test_partner]
|
||||
for (source, follower_check), expected in zip(sources, expected):
|
||||
for source, follower_check, expected_partner in cases:
|
||||
with self.subTest(source=source, follower_check=follower_check):
|
||||
partner = self.env['res.partner']._mail_find_partner_from_emails(
|
||||
[source], records=linked_record if follower_check else None
|
||||
)[0]
|
||||
self.assertEqual(partner, expected,
|
||||
self.assertEqual(partner, expected_partner,
|
||||
'Mail (FIXME): partial recognition of multi email through email_normalize')
|
||||
|
||||
# test users with same email, priority given to current user
|
||||
|
|
@ -180,13 +176,13 @@ class TestMailTools(MailCommon):
|
|||
""" Test _mail_find_partner_from_emails when dealing with records in
|
||||
a multicompany environment, returning a partner record with matching
|
||||
company_id. """
|
||||
self._activate_multi_company()
|
||||
Partner = self.env['res.partner']
|
||||
self.test_partner.company_id = self.company_2
|
||||
self.test_partner.write({'name': 'Original - Company2'})
|
||||
|
||||
test_partner_no_company = self.test_partner.copy({'company_id': False})
|
||||
test_partner_no_company = self.test_partner.copy({'name': 'NoCompany', 'company_id': False})
|
||||
test_partner_company_2 = self.test_partner
|
||||
test_partner_company_3 = test_partner_no_company.copy({'company_id': self.company_3.id})
|
||||
test_partner_company_3 = test_partner_no_company.copy({'name': 'Company3', 'company_id': self.company_3.id})
|
||||
records = [
|
||||
None,
|
||||
*Partner.create([
|
||||
|
|
@ -201,91 +197,33 @@ class TestMailTools(MailCommon):
|
|||
(test_partner_company_3, "Prefer same company as reference record."),
|
||||
(test_partner_no_company, "Prefer non-specific partner for non-specific records."),
|
||||
]
|
||||
for record, (expected_partner, msg) in zip(records, expected_partners):
|
||||
found = Partner._mail_find_partner_from_emails([self._test_email], records=record)
|
||||
self.assertEqual(found, [expected_partner], msg)
|
||||
for record, (expected, msg) in zip(records, expected_partners):
|
||||
with self.subTest(record=record.name if record else 'NoRecord'):
|
||||
found = Partner._mail_find_partner_from_emails([self._test_email], records=record)
|
||||
self.assertEqual(found, [expected], f'Found {found[0].name} instead of {expected[0].name}: {msg}')
|
||||
|
||||
@users('employee')
|
||||
def test_tools_email_re(self):
|
||||
expected = [
|
||||
# single email
|
||||
['alfred.astaire@test.example.com'],
|
||||
['alfred.astaire@test.example.com'],
|
||||
['alfred.astaire@test.example.com'],
|
||||
['alfred.astaire@test.example.com'],
|
||||
['alfred.astaire@test.example.com'],
|
||||
# multiple emails
|
||||
['alfred.astaire@test.example.com', 'evelyne.gargouillis@test.example.com'],
|
||||
['alfred.astaire@test.example.com', 'evelyne.gargouillis@test.example.com'],
|
||||
['alfred.astaire@test.example.com', 'evelyne.gargouillis@test.example.com'],
|
||||
['alfred.astaire@test.example.com', 'evelyne.gargouillis@test.example.com'],
|
||||
# text containing email
|
||||
['alfred.astaire@test.example.com'],
|
||||
['alfred.astaire@test.example.com'],
|
||||
# text containing emails
|
||||
['alfred.astaire@test.example.com', 'evelyne.gargouillis@test.example.com'],
|
||||
['alfred.astaire@test.example.com', 'evelyne.gargouillis@test.example.com'],
|
||||
# falsy
|
||||
[], [], [],
|
||||
]
|
||||
|
||||
for src, exp in zip(self.sources, expected):
|
||||
res = tools.email_re.findall(src)
|
||||
self.assertEqual(
|
||||
res, exp,
|
||||
'Seems email_re is broken with %s (expected %r, received %r)' % (src, exp, res)
|
||||
)
|
||||
@tagged('mail_tools', 'mail_init')
|
||||
class TestMailUtils(MailCommon):
|
||||
|
||||
@users('employee')
|
||||
def test_tools_email_split_tuples(self):
|
||||
expected = [
|
||||
# single email
|
||||
[('', 'alfred.astaire@test.example.com')],
|
||||
[('', 'alfred.astaire@test.example.com')],
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com')],
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com')],
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com')],
|
||||
# multiple emails
|
||||
[('', 'alfred.astaire@test.example.com'), ('', 'evelyne.gargouillis@test.example.com')],
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com'), ('Evelyne The Goat', 'evelyne.gargouillis@test.example.com')],
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com'), ('', 'evelyne.gargouillis@test.example.com')],
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com'), ('', 'evelyne.gargouillis@test.example.com')],
|
||||
# text containing email -> fallback on parsing to extract text from email
|
||||
[('Hello', 'alfred.astaire@test.example.comhowareyou?')],
|
||||
[('Hello', 'alfred.astaire@test.example.com')],
|
||||
[('Hello Fredo', 'alfred.astaire@test.example.com'), ('', 'evelyne.gargouillis@test.example.com')],
|
||||
[('Hello Fredo', 'alfred.astaire@test.example.com'), ('and', 'evelyne.gargouillis@test.example.com')],
|
||||
# falsy -> probably not designed for that
|
||||
[],
|
||||
[('j\'adore écrire', "des@gmail.comou"), ('', '@gmail.com')], [],
|
||||
]
|
||||
def test_migrate_icp_to_domain(self):
|
||||
""" Test ICP to alias domain migration """
|
||||
self.env["ir.config_parameter"].set_param("mail.catchall.domain", "test.migration.com")
|
||||
self.env["ir.config_parameter"].set_param("mail.bounce.alias", "migrate+bounce")
|
||||
self.env["ir.config_parameter"].set_param("mail.catchall.alias", "migrate+catchall")
|
||||
self.env["ir.config_parameter"].set_param("mail.default.from", "migrate+default_from")
|
||||
|
||||
for src, exp in zip(self.sources, expected):
|
||||
res = tools.email_split_tuples(src)
|
||||
self.assertEqual(
|
||||
res, exp,
|
||||
'Seems email_split_tuples is broken with %s (expected %r, received %r)' % (src, exp, res)
|
||||
)
|
||||
existing = self.env["mail.alias.domain"].search([('name', '=', 'test.migration.com')])
|
||||
self.assertFalse(existing)
|
||||
|
||||
@users('employee')
|
||||
def test_tools_single_email_re(self):
|
||||
expected = [
|
||||
# single email
|
||||
['alfred.astaire@test.example.com'],
|
||||
[], [], [], [], # formatting issue for single email re
|
||||
# multiple emails -> couic
|
||||
[], [], [], [],
|
||||
# text containing email -> couic
|
||||
[], [],
|
||||
# text containing emails -> couic
|
||||
[], [],
|
||||
# falsy
|
||||
[], [], [],
|
||||
]
|
||||
new = self.env["mail.alias.domain"]._migrate_icp_to_domain()
|
||||
self.assertEqual(new.name, "test.migration.com")
|
||||
self.assertEqual(new.bounce_alias, "migrate+bounce")
|
||||
self.assertEqual(new.catchall_alias, "migrate+catchall")
|
||||
self.assertEqual(new.default_from, "migrate+default_from")
|
||||
|
||||
for src, exp in zip(self.sources, expected):
|
||||
res = tools.single_email_re.findall(src)
|
||||
self.assertEqual(
|
||||
res, exp,
|
||||
'Seems single_email_re is broken with %s (expected %r, received %r)' % (src, exp, res)
|
||||
)
|
||||
again = self.env["mail.alias.domain"]._migrate_icp_to_domain()
|
||||
self.assertEqual(again.name, "test.migration.com")
|
||||
|
||||
existing = self.env["mail.alias.domain"].search([('name', '=', 'test.migration.com')])
|
||||
self.assertEqual(len(existing), 1, 'Should not migrate twice')
|
||||
|
|
|
|||
|
|
@ -1,111 +1,197 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from contextlib import contextmanager
|
||||
from markupsafe import Markup
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import tools
|
||||
from odoo.addons.base.models.res_partner import ResPartner
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.tests.common import Form, users
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests import Form, tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
# samples use effective TLDs from the Mozilla public suffix
|
||||
# list at http://publicsuffix.org
|
||||
SAMPLES = [
|
||||
('"Raoul Grosbedon" <raoul@chirurgiens-dentistes.fr> ', 'Raoul Grosbedon', 'raoul@chirurgiens-dentistes.fr'),
|
||||
('ryu+giga-Sushi@aizubange.fukushima.jp', '', 'ryu+giga-Sushi@aizubange.fukushima.jp'),
|
||||
('Raoul chirurgiens-dentistes.fr', 'Raoul chirurgiens-dentistes.fr', ''),
|
||||
(" Raoul O'hara <!@historicalsociety.museum>", "Raoul O'hara", '!@historicalsociety.museum'),
|
||||
('Raoul Grosbedon <raoul@CHIRURGIENS-dentistes.fr> ', 'Raoul Grosbedon', 'raoul@CHIRURGIENS-dentistes.fr'),
|
||||
('Raoul megaraoul@chirurgiens-dentistes.fr', 'Raoul', 'megaraoul@chirurgiens-dentistes.fr'),
|
||||
('"Patrick Da Beast Poilvache" <PATRICK@example.com>', 'Patrick Poilvache', 'patrick@example.com'),
|
||||
('Patrick Caché <patrick@EXAMPLE.COM>', 'Patrick Poilvache', 'patrick@example.com'),
|
||||
('Patrick Caché <2patrick@EXAMPLE.COM>', 'Patrick Caché', '2patrick@example.com'),
|
||||
|
||||
]
|
||||
|
||||
@tagged('res_partner', 'mail_tools')
|
||||
@tagged('res_partner', 'mail_tools', 'mail_thread_api')
|
||||
class TestPartner(MailCommon):
|
||||
|
||||
def _check_find_or_create(self, test_string, expected_name, expected_email, expected_email_normalized=False, check_partner=False, should_create=False):
|
||||
expected_email_normalized = expected_email_normalized or expected_email
|
||||
partner = self.env['res.partner'].find_or_create(test_string)
|
||||
if should_create and check_partner:
|
||||
self.assertTrue(partner.id > check_partner.id, 'find_or_create failed - should have found existing')
|
||||
elif check_partner:
|
||||
self.assertEqual(partner, check_partner, 'find_or_create failed - should have found existing')
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.samples = [
|
||||
('"Raoul Grosbedon" <raoul@chirurgiens-dentistes.fr> ', 'Raoul Grosbedon', 'raoul@chirurgiens-dentistes.fr'),
|
||||
('ryu+giga-Sushi@aizubange.fukushima.jp', 'ryu+giga-sushi@aizubange.fukushima.jp', 'ryu+giga-sushi@aizubange.fukushima.jp'),
|
||||
('Raoul chirurgiens-dentistes.fr', 'Raoul chirurgiens-dentistes.fr', ''),
|
||||
(" Raoul O'hara <!@historicalsociety.museum>", "Raoul O'hara", '!@historicalsociety.museum'),
|
||||
('Raoul Grosbedon <raoul@CHIRURGIENS-dentistes.fr> ', 'Raoul Grosbedon', 'raoul@chirurgiens-dentistes.fr'),
|
||||
('Raoul megaraoul@chirurgiens-dentistes.fr', 'Raoul', 'megaraoul@chirurgiens-dentistes.fr'),
|
||||
('"Patrick Da Beast Poilvache" <PATRICK@example.com>', 'Patrick Da Beast Poilvache', 'patrick@example.com'),
|
||||
('Patrick Caché <patrick@EXAMPLE.COM>', 'Patrick Da Beast Poilvache', 'patrick@example.com'),
|
||||
('Patrick Caché <patrick.2@EXAMPLE.COM>', 'Patrick Caché', 'patrick.2@example.com'),
|
||||
# multi email
|
||||
('"Multi Email" <multi.email@example.com>, multi.email.2@example.com', 'Multi Email', 'multi.email@example.com')
|
||||
]
|
||||
@contextmanager
|
||||
def mockPartnerCalls(self):
|
||||
_original_create = ResPartner.create
|
||||
_original_search = ResPartner.search
|
||||
self._new_partners = self.env['res.partner']
|
||||
|
||||
def _res_partner_create(model, *args, **kwargs):
|
||||
records = _original_create(model, *args, **kwargs)
|
||||
self._new_partners += records.sudo()
|
||||
return records
|
||||
|
||||
with patch.object(ResPartner, 'create',
|
||||
autospec=True, side_effect=_res_partner_create) as mock_partner_create, \
|
||||
patch.object(ResPartner, 'search',
|
||||
autospec=True, side_effect=_original_search) as mock_partner_search:
|
||||
self._mock_partner_create = mock_partner_create
|
||||
self._mock_partner_search = mock_partner_search
|
||||
yield
|
||||
|
||||
def _check_find_or_create(self, test_string, expected_name, expected_email,
|
||||
expected_email_normalized=False,
|
||||
expected_partner=False):
|
||||
expected_email_normalized = expected_email_normalized or tools.email_normalize(expected_email) or ''
|
||||
with self.mockPartnerCalls():
|
||||
partner = self.env['res.partner'].find_or_create(test_string)
|
||||
if expected_partner:
|
||||
self.assertEqual(
|
||||
partner, expected_partner,
|
||||
f'Should have found {expected_partner.name} ({expected_partner.id}), found {partner.name} ({partner.id}) instead')
|
||||
self.assertFalse(self._new_partners)
|
||||
else:
|
||||
self.assertEqual(
|
||||
partner, self._new_partners,
|
||||
f'Should have created a partner, found {partner.name} ({partner.id}) instead'
|
||||
)
|
||||
self.assertEqual(partner.name, expected_name)
|
||||
self.assertEqual(partner.email or '', expected_email)
|
||||
self.assertEqual(partner.email_normalized or '', expected_email_normalized)
|
||||
return partner
|
||||
|
||||
@users('admin')
|
||||
def test_res_partner_find_or_create(self):
|
||||
Partner = self.env['res.partner']
|
||||
|
||||
partner = Partner.browse(Partner.name_create(SAMPLES[0][0])[0])
|
||||
self._check_find_or_create(
|
||||
SAMPLES[0][0], SAMPLES[0][1], SAMPLES[0][2],
|
||||
check_partner=partner, should_create=False
|
||||
)
|
||||
|
||||
partner_2 = Partner.browse(Partner.name_create('sarah.john@connor.com')[0])
|
||||
found_2 = self._check_find_or_create(
|
||||
'john@connor.com', 'john@connor.com', 'john@connor.com',
|
||||
check_partner=partner_2, should_create=True
|
||||
)
|
||||
|
||||
new = self._check_find_or_create(
|
||||
SAMPLES[1][0], SAMPLES[1][2].lower(), SAMPLES[1][2].lower(),
|
||||
check_partner=found_2, should_create=True
|
||||
)
|
||||
|
||||
new2 = self._check_find_or_create(
|
||||
SAMPLES[2][0], SAMPLES[2][1], SAMPLES[2][2],
|
||||
check_partner=new, should_create=True
|
||||
)
|
||||
|
||||
self._check_find_or_create(
|
||||
SAMPLES[3][0], SAMPLES[3][1], SAMPLES[3][2],
|
||||
check_partner=new2, should_create=True
|
||||
)
|
||||
|
||||
new4 = self._check_find_or_create(
|
||||
SAMPLES[4][0], SAMPLES[0][1], SAMPLES[0][2],
|
||||
check_partner=partner, should_create=False
|
||||
)
|
||||
|
||||
self._check_find_or_create(
|
||||
SAMPLES[5][0], SAMPLES[5][1], SAMPLES[5][2],
|
||||
check_partner=new4, should_create=True
|
||||
)
|
||||
|
||||
existing = Partner.create({
|
||||
'name': SAMPLES[6][1],
|
||||
'email': SAMPLES[6][0],
|
||||
def test_address_tracking(self):
|
||||
self.env.company.name = 'YourCompany'
|
||||
company_partner = self.env.company.partner_id
|
||||
# use some wacky formatting to check inlining
|
||||
company_partner.country_id.address_format = """%(street)s
|
||||
\n\n\n%(street2)s
|
||||
%(city)s %(state_code)s %(zip)s
|
||||
\n%(country_name)s\n
|
||||
\n """
|
||||
company_partner.write({
|
||||
'city': 'Some City Name',
|
||||
'street': 'Some Street Name',
|
||||
'type': 'contact',
|
||||
'zip': '94134',
|
||||
'state_id': self.env.ref('base.state_us_5').id,
|
||||
'country_id': self.env.ref('base.us').id,
|
||||
})
|
||||
self.assertEqual(existing.name, SAMPLES[6][1])
|
||||
self.assertEqual(existing.email, SAMPLES[6][0])
|
||||
self.assertEqual(existing.email_normalized, SAMPLES[6][2])
|
||||
child_partner = self.env['res.partner'].create({
|
||||
'name': 'Some Guy',
|
||||
'parent_id': company_partner.id,
|
||||
})
|
||||
self.env.flush_all()
|
||||
self.cr.precommit.run()
|
||||
# keep track of setup messages
|
||||
partners = (company_partner, child_partner)
|
||||
partner_original_messages = (company_partner.message_ids, child_partner.message_ids)
|
||||
|
||||
new6 = self._check_find_or_create(
|
||||
SAMPLES[7][0], SAMPLES[6][1], SAMPLES[6][0],
|
||||
expected_email_normalized=SAMPLES[6][2],
|
||||
check_partner=existing, should_create=False
|
||||
company_partner.street = 'Some Other Street Name'
|
||||
company_partner.city = 'Some Other City Name'
|
||||
self.env.flush_all()
|
||||
self.cr.precommit.run()
|
||||
for partner, original_messages in zip(partners, partner_original_messages):
|
||||
change_messages = partner.message_ids - original_messages
|
||||
self.assertEqual(len(change_messages), 1)
|
||||
tracking_values = change_messages.tracking_value_ids
|
||||
self.assertIn(f'{self.env.company.name}, Some Street Name, Some City Name CA 94134, United States',
|
||||
tracking_values.old_value_char)
|
||||
self.assertIn(f'{self.env.company.name}, Some Other Street Name, Some Other City Name CA 94134, United States',
|
||||
tracking_values.new_value_char)
|
||||
# none of the address fields are logged at the same time
|
||||
self.assertEqual(set(), set(partner._address_fields()) & set(tracking_values.sudo().field_id.mapped('name')))
|
||||
|
||||
def test_discuss_mention_suggestions_priority(self):
|
||||
name = uuid4() # unique name to avoid conflict with already existing users
|
||||
self.env['res.partner'].create([{'name': f'{name}-{i}-not-user'} for i in range(0, 2)])
|
||||
for i in range(0, 2):
|
||||
mail_new_test_user(self.env, login=f'{name}-{i}-portal-user', groups='base.group_portal')
|
||||
mail_new_test_user(self.env, login=f'{name}-{i}-internal-user', groups='base.group_user')
|
||||
|
||||
# suggest portal user of this company in another company
|
||||
suggested_partners = self.env["res.partner"].with_user(self.user_employee_c2).get_mention_suggestions("portal-user")
|
||||
|
||||
porter_user_suggested = [
|
||||
p for p in suggested_partners['res.partner']
|
||||
if p["name"] == f'{name}-1-portal-user (base.group_portal)'
|
||||
]
|
||||
self.assertEqual(len(porter_user_suggested), 1, "porter_user_suggested should contain one user")
|
||||
store_data = self.env["res.partner"].get_mention_suggestions(name, limit=5)
|
||||
partners_format = store_data["res.partner"]
|
||||
self.assertEqual(len(partners_format), 5, "should have found limit (5) partners")
|
||||
# return format for user is either a dict (there is a user and the dict is data) or a list of command (clear)
|
||||
self.assertEqual(
|
||||
[
|
||||
next(
|
||||
(
|
||||
not u["share"]
|
||||
for u in store_data["res.users"]
|
||||
if u["id"] == p["main_user_id"]
|
||||
),
|
||||
False,
|
||||
)
|
||||
for p in partners_format
|
||||
],
|
||||
[True, True, False, False, False],
|
||||
"should return internal users in priority",
|
||||
)
|
||||
self.assertEqual(
|
||||
[bool(p["main_user_id"]) for p in partners_format],
|
||||
[True, True, True, True, False],
|
||||
"should return partners without users last",
|
||||
)
|
||||
|
||||
self._check_find_or_create(
|
||||
SAMPLES[8][0], SAMPLES[8][1], SAMPLES[8][2],
|
||||
check_partner=new6, should_create=True
|
||||
@users('admin')
|
||||
def test_find_or_create(self):
|
||||
""" Test 'find_or_create' method, calling name_create while parsing
|
||||
input to find name and email. """
|
||||
original_partner = self.env['res.partner'].browse(
|
||||
self.env['res.partner'].name_create(self.samples[0][0])[0]
|
||||
)
|
||||
all_partners = []
|
||||
|
||||
for (text_input, expected_name, expected_email), expected_partner, find_idx in zip(
|
||||
self.samples,
|
||||
[original_partner, False, False, False, original_partner, False,
|
||||
# patrick example
|
||||
False, False, False,
|
||||
# multi email
|
||||
False],
|
||||
[0, 0, 0, 0, 0, 0, 0, 6, 0, 0],
|
||||
):
|
||||
with self.subTest(text_input=text_input):
|
||||
if not expected_partner and find_idx:
|
||||
expected_partner = all_partners[find_idx]
|
||||
all_partners.append(
|
||||
self._check_find_or_create(
|
||||
text_input, expected_name, expected_email,
|
||||
expected_partner=expected_partner,
|
||||
)
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.env['res.partner'].find_or_create("Raoul chirurgiens-dentistes.fr", assert_valid_email=True)
|
||||
|
||||
@users('admin')
|
||||
def test_res_partner_find_or_create_email(self):
|
||||
def test_find_or_create_email_field(self):
|
||||
""" Test 'find_or_create' tool used in mail, notably when linking emails
|
||||
found in recipients to partners when sending emails using the mail
|
||||
composer. """
|
||||
composer. Test various combinations of problematic use cases like
|
||||
formatting, multi-emails, ... """
|
||||
partners = self.env['res.partner'].create([
|
||||
{
|
||||
'email': 'classic.format@test.example.com',
|
||||
|
|
@ -156,7 +242,7 @@ class TestPartner(MailCommon):
|
|||
self.assertEqual(partner.email, email_input)
|
||||
partner.unlink() # do not mess with subsequent tests
|
||||
|
||||
# now input is multi email -> '_parse_partner_name' used in 'find_or_create'
|
||||
# now input is multi email -> 'parse_contact_from_email' used in 'find_or_create'
|
||||
# before trying to normalize is quite tolerant, allowing positive checks
|
||||
for email_input, match_partner, exp_email_partner in [
|
||||
('classic.format@test.example.com,another.email@test.example.com',
|
||||
|
|
@ -177,19 +263,234 @@ class TestPartner(MailCommon):
|
|||
if partner not in partners:
|
||||
partner.unlink() # do not mess with subsequent tests
|
||||
|
||||
def test_res_partner_get_mention_suggestions_priority(self):
|
||||
name = uuid4() # unique name to avoid conflict with already existing users
|
||||
self.env['res.partner'].create([{'name': f'{name}-{i}-not-user'} for i in range(0, 2)])
|
||||
for i in range(0, 2):
|
||||
mail_new_test_user(self.env, login=f'{name}-{i}-portal-user', groups='base.group_portal')
|
||||
mail_new_test_user(self.env, login=f'{name}-{i}-internal-user', groups='base.group_user')
|
||||
partners_format = self.env['res.partner'].get_mention_suggestions(name, limit=5)
|
||||
self.assertEqual(len(partners_format), 5, "should have found limit (5) partners")
|
||||
# return format for user is either a dict (there is a user and the dict is data) or a list of command (clear)
|
||||
self.assertEqual(list(map(lambda p: isinstance(p['user'], dict) and p['user']['isInternalUser'], partners_format)), [True, True, False, False, False], "should return internal users in priority")
|
||||
self.assertEqual(list(map(lambda p: isinstance(p['user'], dict), partners_format)), [True, True, True, True, False], "should return partners without users last")
|
||||
@users('employee_c2')
|
||||
def test_find_or_create_from_emails(self):
|
||||
""" Test for '_find_or_create_from_emails' allowing to find or create
|
||||
partner based on emails in a batch-enabled and optimized fashion. """
|
||||
with self.mockPartnerCalls():
|
||||
partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
[item[0] for item in self.samples],
|
||||
additional_values=None,
|
||||
)
|
||||
self.assertEqual(len(partners), len(self.samples))
|
||||
self.assertEqual(len(self._new_partners), len(self.samples) - 2, 'Two duplicates in samples')
|
||||
|
||||
def test_res_partner_log_portal_group(self):
|
||||
for (sample, exp_name, exp_email), partner in zip(self.samples, partners):
|
||||
# specific to '_from_emails': name used as email is no email found
|
||||
exp_email = exp_email or exp_name
|
||||
with self.subTest(sample=sample):
|
||||
self.assertFalse(partner.company_id)
|
||||
self.assertEqual(partner.email, exp_email)
|
||||
self.assertEqual(partner.email_normalized, tools.email_normalize(exp_email))
|
||||
self.assertTrue(partner.id)
|
||||
self.assertEqual(partner.lang, 'en_US')
|
||||
self.assertEqual(partner.name, exp_name)
|
||||
|
||||
new_samples = self.samples + [
|
||||
# new
|
||||
('"New Customer" <new.customer@test.EXAMPLE.com>', 'New Customer', 'new.customer@test.example.com'),
|
||||
# duplicate (see in sample)
|
||||
('"Duplicated Raoul" <RAOUL@chirurgiens-dentistes.fr>', 'Raoul Grosbedon', 'raoul@chirurgiens-dentistes.fr'),
|
||||
# new (even if invalid)
|
||||
('Invalid', 'Invalid', ''),
|
||||
# ignored, completely invalid
|
||||
(False, False, False),
|
||||
(None, False, False),
|
||||
(' ', False, False),
|
||||
('', False, False),
|
||||
]
|
||||
all_emails = [item[0] for item in new_samples]
|
||||
with self.mockPartnerCalls():
|
||||
partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
all_emails,
|
||||
additional_values={
|
||||
tools.email_normalize(email) or email: {
|
||||
'company_id': self.env.company.id,
|
||||
}
|
||||
for email in all_emails if email and email.strip()
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(partners), len(new_samples))
|
||||
self.assertEqual(len(self._new_partners), 2, 'Only 2 real new partners in new sample')
|
||||
|
||||
for (sample, exp_name, exp_email), partner in zip(new_samples, partners):
|
||||
with self.subTest(sample=sample, exp_name=exp_name, exp_email=exp_email, partner=partner):
|
||||
# specific to '_from_emails': name used as email is no email found
|
||||
exp_email = exp_email or exp_name
|
||||
exp_company = self.env.company if sample in [
|
||||
'"New Customer" <new.customer@test.EXAMPLE.com>', # valid email, not known -> new customer
|
||||
'Invalid' # invalid email, not known -> create a new partner
|
||||
] else self.env['res.company']
|
||||
if sample in [False, None, ' ', '']:
|
||||
self.assertFalse(partner)
|
||||
else:
|
||||
exp_email_normalized = tools.email_normalize(exp_email)
|
||||
self.assertEqual(partner.company_id, exp_company)
|
||||
self.assertEqual(partner.email_normalized, exp_email_normalized)
|
||||
self.assertEqual(partner.name, exp_name)
|
||||
|
||||
@users('employee_c2')
|
||||
def test_find_or_create_from_emails_dupes_email_field(self):
|
||||
""" Specific test for duplicates management: based on email to avoid
|
||||
creating similar partners. """
|
||||
# all same partner, same email 'test.customer@test.dupe.example.com'
|
||||
email_dupes_samples = [
|
||||
'"Formatted Customer" <test.customer@TEST.DUPE.EXAMPLE.COM>',
|
||||
'test.customer@test.dupe.example.com',
|
||||
'"Another Name" <test.customer@TEST.DUPE.EXAMPLE.COM>',
|
||||
'"Mix of both" <test.customer@test.dupe.EXAMPLE.COM',
|
||||
]
|
||||
email_expected_name = "Formatted Customer" # first found email will setup partner info
|
||||
email_expected_email = 'test.customer@test.dupe.example.com' # normalized version of given email
|
||||
# all same partner, same invalid email 'test.customer.invalid.email'
|
||||
name_dupes_samples = [
|
||||
'test.customer.invalid.email',
|
||||
'test.customer.invalid.email',
|
||||
]
|
||||
name_expected_name = 'test.customer.invalid.email' # invalid email kept as both name and email
|
||||
name_expected_email = 'test.customer.invalid.email' # invalid email kept as both name and email
|
||||
|
||||
partners = self.env['res.partner']
|
||||
for samples, (expected_name, expected_email) in [
|
||||
(email_dupes_samples, (email_expected_name, email_expected_email)),
|
||||
(name_dupes_samples, (name_expected_name, name_expected_email)),
|
||||
]:
|
||||
with self.subTest(samples=samples, expected_name=expected_name, expected_email=expected_email):
|
||||
with self.mockPartnerCalls():
|
||||
partner_list = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
samples,
|
||||
additional_values=None,
|
||||
)
|
||||
# calls
|
||||
self.assertEqual(self._mock_partner_create.call_count, 1)
|
||||
self.assertEqual(self._mock_partner_search.call_count, 1)
|
||||
self.assertEqual(len(self._new_partners), 1)
|
||||
# results
|
||||
self.assertEqual(len(partner_list), len(samples))
|
||||
self.assertTrue(len(set(partner.id for partner in partner_list)) == 1 and partner_list[0].id, 'Should have a unique new partner')
|
||||
for partner in partner_list:
|
||||
self.assertEqual(partner.email, expected_email)
|
||||
self.assertEqual(partner.name, expected_name)
|
||||
|
||||
partners += partner_list[0]
|
||||
|
||||
self.assertEqual(len(partners), 2,
|
||||
'Should have created one partner for valid email, one for invalid email')
|
||||
|
||||
new_samples = [
|
||||
'"Another Customer" <test.different.1@TEST.DUPE.EXAMPLE.COM', # actually a new valid email
|
||||
'"First Duplicate" <test.customer@TEST.DUPE.example.com', # duplicated of valid email created above
|
||||
'test.customer.invalid.email', # duplicate of an invalid email created above
|
||||
# multi email
|
||||
'"Multi Email Another" <TEST.different.1@test.dupe.example.com>, other.customer@other.example.com',
|
||||
'"Multi Email" <other.customer.2@test.dupe.example.com>, test.different.1@test.dupe.example.com',
|
||||
'Invalid, Multi Format other.customer.😊@test.dupe.example.com, "A Name" <yt.another.customer@new.example.com>',
|
||||
'"Unicode Formatted" <other.customer.😊@test.dupe.example.com>', # duplicate of above
|
||||
]
|
||||
expected = [
|
||||
(False, "Another Customer", "test.different.1@test.dupe.example.com"),
|
||||
(partners[0], "Formatted Customer", "test.customer@test.dupe.example.com"),
|
||||
(partners[1], "test.customer.invalid.email", "test.customer.invalid.email"),
|
||||
# multi email support
|
||||
(False, "Another Customer", "test.different.1@test.dupe.example.com"),
|
||||
(False, "Multi Email", "other.customer.2@test.dupe.example.com"),
|
||||
(False, "Multi Format", "other.customer.😊@test.dupe.example.com"),
|
||||
(False, "Multi Format", "other.customer.😊@test.dupe.example.com"),
|
||||
]
|
||||
with self.mockPartnerCalls():
|
||||
new_partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
new_samples,
|
||||
additional_values=None,
|
||||
)
|
||||
# calls
|
||||
self.assertEqual(self._mock_partner_create.call_count, 1)
|
||||
self.assertEqual(self._mock_partner_search.call_count, 1,
|
||||
'Search once, even with both normalized and invalid emails')
|
||||
self.assertEqual(len(self._new_partners), 3)
|
||||
self.assertEqual(
|
||||
sorted(self._new_partners.mapped('email')),
|
||||
sorted(['other.customer.2@test.dupe.example.com',
|
||||
'other.customer.😊@test.dupe.example.com',
|
||||
'test.different.1@test.dupe.example.com']))
|
||||
# results
|
||||
self.assertEqual(len(new_partners), len(new_samples))
|
||||
for partner, (expected_partner, expected_name, expected_email) in zip(new_partners, expected):
|
||||
with self.subTest(partner=partner, expected_name=expected_name):
|
||||
if expected_partner:
|
||||
self.assertEqual(partner, expected_partner)
|
||||
else:
|
||||
self.assertIn(partner, self._new_partners)
|
||||
self.assertEqual(partner.email, expected_email)
|
||||
self.assertEqual(partner.name, expected_name)
|
||||
|
||||
no_new_samples = [
|
||||
# only duplicates
|
||||
'"Another Duplicate" <test.different.1@TEST.DUPE.EXAMPLE.COM',
|
||||
'"First Duplicate2" <test.customer@TEST.DUPE.example.com',
|
||||
# falsy values
|
||||
'"Falsy" <falsy>',
|
||||
'falsy',
|
||||
' ',
|
||||
]
|
||||
expected = [
|
||||
(new_partners[0], "Another Customer", "test.different.1@test.dupe.example.com"),
|
||||
(partners[0], "Formatted Customer", "test.customer@test.dupe.example.com"),
|
||||
(False, '"Falsy" <falsy>', '"Falsy" <falsy>'),
|
||||
(False, "falsy", "falsy"),
|
||||
(False, False, False),
|
||||
]
|
||||
with self.mockPartnerCalls():
|
||||
no_new_partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
no_new_samples,
|
||||
additional_values=None,
|
||||
)
|
||||
# calls
|
||||
self.assertEqual(self._mock_partner_create.call_count, 1)
|
||||
self.assertEqual(self._mock_partner_search.call_count, 1)
|
||||
self.assertEqual(len(self._new_partners), 2)
|
||||
self.assertEqual(sorted(self._new_partners.mapped('email')), ['"Falsy" <falsy>', "falsy"])
|
||||
for partner, (expected_partner, expected_name, expected_email) in zip(no_new_partners, expected):
|
||||
with self.subTest(partner=partner, expected_name=expected_name):
|
||||
if expected_partner:
|
||||
self.assertEqual(partner, expected_partner)
|
||||
elif not expected_name and not expected_email:
|
||||
self.assertEqual(partner, self.env['res.partner'])
|
||||
else:
|
||||
self.assertIn(partner, self._new_partners)
|
||||
self.assertEqual(partner.email, expected_email)
|
||||
self.assertEqual(partner.name, expected_name)
|
||||
|
||||
def test_message_get_default_recipients(self):
|
||||
""" Specific use case: partner should contact himself """
|
||||
partners = self.env['res.partner'].create([
|
||||
{'name': 'Raoulette', 'email': '"Raoulette" <raoulette@example.com>'},
|
||||
{'name': 'Ignassette', 'email': 'wrong'}
|
||||
])
|
||||
defaults = partners._message_get_default_recipients()
|
||||
for partner in partners:
|
||||
with self.subTest(partner=partner.name):
|
||||
self.assertEqual(defaults[partner.id], {
|
||||
'email_cc': '', 'email_to': '',
|
||||
'partner_ids': partner.ids,
|
||||
})
|
||||
|
||||
def test_message_get_suggested_recipients(self):
|
||||
""" Specific use case: partner should contact himself """
|
||||
partners = self.env['res.partner'].create([
|
||||
{'name': 'Raoulette', 'email': '"Raoulette" <raoulette@example.com>'},
|
||||
{'name': 'Ignassette', 'email': 'wrong'}
|
||||
])
|
||||
for partner in partners:
|
||||
with self.subTest(partner_name=partner.name):
|
||||
suggested = partner._message_get_suggested_recipients()
|
||||
self.assertEqual(suggested, [{
|
||||
'create_values': {},
|
||||
'email': partner.email_normalized,
|
||||
'name': partner.name,
|
||||
'partner_id': partner.id,
|
||||
}])
|
||||
|
||||
def test_log_portal_group(self):
|
||||
Users = self.env['res.users']
|
||||
subtype_note = self.env.ref('mail.mt_note')
|
||||
group_portal, group_user = self.env.ref('base.group_portal'), self.env.ref('base.group_user')
|
||||
|
|
@ -205,7 +506,7 @@ class TestPartner(MailCommon):
|
|||
self.assertNotIn('Portal Access Granted', new_msg.body)
|
||||
self.assertIn('Contact created', new_msg.body)
|
||||
|
||||
new_user.write({'groups_id': [(4, group_portal.id), (3, group_user.id)]})
|
||||
new_user.write({'group_ids': [(4, group_portal.id), (3, group_user.id)]})
|
||||
new_msg = new_user.message_ids[0]
|
||||
self.assertIn('Portal Access Granted', new_msg.body)
|
||||
self.assertEqual(new_msg.subtype_id, subtype_note)
|
||||
|
|
@ -213,7 +514,7 @@ class TestPartner(MailCommon):
|
|||
# check at create
|
||||
new_user = Users.create({
|
||||
'email': 'micheline.2@test.example.com',
|
||||
'groups_id': [(4, group_portal.id)],
|
||||
'group_ids': [(4, group_portal.id)],
|
||||
'login': 'michmich.2',
|
||||
'name': 'Micheline Portal',
|
||||
})
|
||||
|
|
@ -223,7 +524,41 @@ class TestPartner(MailCommon):
|
|||
self.assertEqual(new_msg.subtype_id, subtype_note)
|
||||
|
||||
@users('admin')
|
||||
def test_res_partner_merge_wizards(self):
|
||||
def test_name_create_corner_cases(self):
|
||||
""" Test parsing (and fallback) or name given to name_create that should
|
||||
try to correctly find name and email, even with malformed input. Relies
|
||||
on 'parse_contact_from_email' and 'email_normalize'. """
|
||||
samples = [
|
||||
'Raoul raoul@grosbedon.fr',
|
||||
'Raoul chirurgiens-dentistes.fr',
|
||||
'invalid',
|
||||
'False',
|
||||
# (simili) void values
|
||||
'', ' ', False, None,
|
||||
# email only
|
||||
'lenny.bar@gmail.com',
|
||||
]
|
||||
expected = [
|
||||
('Raoul', 'raoul@grosbedon.fr'),
|
||||
('Raoul chirurgiens-dentistes.fr', False),
|
||||
('invalid', False),
|
||||
('False', False),
|
||||
# (simili) void values: always False
|
||||
('', False), ('', False), ('', False), ('', False),
|
||||
# email only: email used as both name and email
|
||||
('lenny.bar@gmail.com', 'lenny.bar@gmail.com')
|
||||
]
|
||||
for (expected_name, expected_email), sample in zip(expected, samples):
|
||||
with self.subTest(sample=sample):
|
||||
partner = self.env['res.partner'].browse(
|
||||
self.env['res.partner'].name_create(sample)[0]
|
||||
)
|
||||
self.assertEqual(partner.name, expected_name)
|
||||
self.assertEqual(partner.email, expected_email)
|
||||
|
||||
@users('admin')
|
||||
@mute_logger('odoo.addons.base.partner.merge', 'odoo.tests')
|
||||
def test_partner_merge_wizards(self):
|
||||
Partner = self.env['res.partner']
|
||||
|
||||
p1 = Partner.create({'name': 'Customer1', 'email': 'test1@test.example.com'})
|
||||
|
|
@ -234,10 +569,10 @@ class TestPartner(MailCommon):
|
|||
|
||||
# add some mail related documents
|
||||
p1.message_subscribe(partner_ids=p3.ids)
|
||||
p1_act1 = p1.activity_schedule(act_type_xmlid='mail.mail_activity_data_todo')
|
||||
p1_act1 = p1.activity_schedule(act_type_xmlid='mail.mail_activity_data_todo', user_id=self.user_admin.id)
|
||||
p1_msg1 = p1.message_post(
|
||||
body='<p>Log on P1</p>',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id
|
||||
body=Markup('<p>Log on P1</p>'),
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
self.assertEqual(p1.activity_ids, p1_act1)
|
||||
self.assertEqual(p1.message_follower_ids.partner_id, self.partner_admin + p3)
|
||||
|
|
|
|||
60
odoo-bringout-oca-ocb-mail/mail/tests/test_res_role.py
Normal file
60
odoo-bringout-oca-ocb-mail/mail/tests/test_res_role.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestResRole(MailCommon, HttpCase):
|
||||
def test_post_mention_role(self):
|
||||
"""Test mention with role"""
|
||||
contact = self.env["res.partner"].create({"name": "A contact"})
|
||||
role_discuss = self.env["res.role"].create({"name": "rd-Discuss"})
|
||||
role_js = self.env["res.role"].create({"name": "rd-JS"})
|
||||
user_discuss = mail_new_test_user(
|
||||
self.env,
|
||||
login="user_d",
|
||||
name="Discuss User",
|
||||
notification_type="inbox",
|
||||
role_ids=[Command.link(role_discuss.id)],
|
||||
)
|
||||
user_js = mail_new_test_user(
|
||||
self.env,
|
||||
login="user_js",
|
||||
name="JS User",
|
||||
notification_type="inbox",
|
||||
role_ids=[Command.link(role_js.id)],
|
||||
)
|
||||
user_discuss_js = mail_new_test_user(
|
||||
self.env,
|
||||
login="user_djs",
|
||||
name="Discuss JS User",
|
||||
notification_type="inbox",
|
||||
role_ids=[Command.link(role_discuss.id), Command.link(role_js.id)],
|
||||
)
|
||||
self.authenticate("employee", "employee")
|
||||
for [roles, expected_users] in [
|
||||
(self.env["res.role"], self.env["res.users"]),
|
||||
(role_discuss, user_discuss + user_discuss_js),
|
||||
(role_js, user_js + user_discuss_js),
|
||||
(role_discuss + role_js, user_discuss + user_js + user_discuss_js),
|
||||
]:
|
||||
data = self.make_jsonrpc_request(
|
||||
"/mail/message/post",
|
||||
{
|
||||
"thread_model": "res.partner",
|
||||
"thread_id": contact.id,
|
||||
"post_data": {
|
||||
"body": "irrelevant",
|
||||
"message_type": "comment",
|
||||
"role_ids": roles.ids,
|
||||
"subtype_xmlid": "mail.mt_note",
|
||||
},
|
||||
},
|
||||
)
|
||||
message = next(filter(lambda m: m["id"] == data["message_id"], data["store_data"]["mail.message"]))
|
||||
self.assertEqual(
|
||||
message["partner_ids"],
|
||||
expected_users.partner_id.ids
|
||||
)
|
||||
|
|
@ -1,17 +1,64 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
from psycopg2 import IntegrityError
|
||||
from unittest import skip
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.addons.base.models.res_users import ResUsersPatchedInTest
|
||||
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
|
||||
from odoo.tests import RecordCapturer, tagged
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.tests import RecordCapturer, tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'mail_tools', 'res_users')
|
||||
class TestNotifySecurityUpdate(MailCommon):
|
||||
|
||||
@users('employee')
|
||||
def test_security_update_email(self):
|
||||
""" User should be notified on old email address when the email changes """
|
||||
with self.mock_mail_gateway():
|
||||
self.env.user.write({'email': 'new@example.com'})
|
||||
|
||||
self.assertSentEmail(
|
||||
'"YourTestCompany" <your.company@example.com>',
|
||||
['e.e@example.com'],
|
||||
subject='Security Update: Email Changed',
|
||||
)
|
||||
|
||||
@users('employee')
|
||||
def test_security_update_login(self):
|
||||
with self.mock_mail_gateway():
|
||||
self.env.user.write({'login': 'newlogin'})
|
||||
|
||||
self.assertSentEmail(
|
||||
'"YourTestCompany" <your.company@example.com>',
|
||||
[self.env.user.email_formatted],
|
||||
subject='Security Update: Login Changed',
|
||||
)
|
||||
|
||||
@users('employee')
|
||||
def test_security_update_password(self):
|
||||
with self.mock_mail_gateway():
|
||||
self.env.user.write({'password': 'newpassword'})
|
||||
|
||||
self.assertSentEmail(
|
||||
'"YourTestCompany" <your.company@example.com>',
|
||||
[self.env.user.email_formatted],
|
||||
subject='Security Update: Password Changed',
|
||||
)
|
||||
|
||||
@tagged('-at_install', 'post_install', 'mail_tools', 'res_users')
|
||||
class TestUser(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.portal_user = cls._create_portal_user()
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
def test_notification_type_constraint(self):
|
||||
with self.assertRaises(IntegrityError, msg='Portal user can not receive notification in Odoo'):
|
||||
|
|
@ -24,27 +71,101 @@ class TestUser(MailCommon):
|
|||
groups='base.group_portal',
|
||||
)
|
||||
|
||||
def test_notification_type_convert_internal_inbox_to_portal(self):
|
||||
"""Tests an internal user using inbox notifications converted to portal
|
||||
is automatically set to email notifications"""
|
||||
user = mail_new_test_user(
|
||||
self.env,
|
||||
login='user_test_constraint_3',
|
||||
name='Test User 3',
|
||||
email='user_test_constraint_3@test.example.com',
|
||||
notification_type='inbox',
|
||||
groups='base.group_user',
|
||||
)
|
||||
|
||||
# Ensure the internal user has well the inbox notification type
|
||||
self.assertEqual(user.notification_type, 'inbox')
|
||||
self.assertIn(self.env.ref('mail.group_mail_notification_type_inbox'), user.group_ids)
|
||||
|
||||
# Change the internal user to portal, and make sure it automatically converts from inbox to email notifications
|
||||
user.write({'group_ids': [
|
||||
(3, self.env.ref('base.group_user').id),
|
||||
(4, self.env.ref('base.group_portal').id),
|
||||
]})
|
||||
self.assertEqual(user.notification_type, 'email')
|
||||
self.assertNotIn(self.env.ref('mail.group_mail_notification_type_inbox'), user.group_ids)
|
||||
|
||||
admin = mail_new_test_user(
|
||||
self.env,
|
||||
login="user_test_constraint_4",
|
||||
name="Test User 4",
|
||||
email="user_test_constraint_3@test.example.com",
|
||||
notification_type='inbox',
|
||||
groups='base.group_erp_manager',
|
||||
)
|
||||
# Re-check that no error occurs when we have overlapping writes on admin user
|
||||
admin.write({
|
||||
'notification_type': 'email',
|
||||
'group_ids': [
|
||||
(3, self.env.ref('base.group_user').id),
|
||||
(3, self.env.ref('base.group_erp_manager').id),
|
||||
(4, self.env.ref('base.group_portal').id),
|
||||
],
|
||||
})
|
||||
self.assertFalse(admin._is_admin())
|
||||
self.assertTrue(admin._is_portal())
|
||||
self.assertEqual(admin.notification_type, 'email')
|
||||
self.assertNotIn(self.env.ref('mail.group_mail_notification_type_inbox'), admin.group_ids)
|
||||
|
||||
@freeze_time("2025-06-18 08:45:12")
|
||||
def test_out_of_office(self):
|
||||
""" Test Out-of-Office computation, defined on user itself. """
|
||||
test_user = self.user_employee.with_user(self.user_employee)
|
||||
portal_user = self.portal_user
|
||||
now = datetime(2025, 6, 8, 8, 45, 12)
|
||||
for ooo_from, ooo_to, exp_ooo in [
|
||||
(False, False, False),
|
||||
(now - timedelta(hours=1), False, True), # only a from is ok
|
||||
(False, now + timedelta(hours=1), False), # invalid interval
|
||||
(now - timedelta(hours=1), now + timedelta(hours=1), True),
|
||||
(now, now, True),
|
||||
(now - timedelta(hours=4), now - timedelta(hours=2), False), # past
|
||||
(now + timedelta(hours=2), now + timedelta(hours=4), False), # future
|
||||
(now + timedelta(hours=2), False, False), # future, from only
|
||||
]:
|
||||
with self.subTest(ooo_from=ooo_from, ooo_to=ooo_to):
|
||||
with self.mock_datetime_and_now(now): # also mock cr.now()
|
||||
test_user.write({
|
||||
'out_of_office_from': ooo_from,
|
||||
'out_of_office_to': ooo_to,
|
||||
})
|
||||
self.assertEqual(test_user.is_out_of_office, exp_ooo)
|
||||
|
||||
portal_user.write({
|
||||
'out_of_office_from': ooo_from,
|
||||
'out_of_office_to': ooo_to,
|
||||
})
|
||||
self.assertFalse(portal_user.is_out_of_office, 'Portal users are never OOO')
|
||||
|
||||
def test_web_create_users(self):
|
||||
src = [
|
||||
'POILUCHETTE@test.example.com',
|
||||
'"Jean Poilvache" <POILVACHE@test.example.com>',
|
||||
]
|
||||
with self.mock_mail_gateway(), \
|
||||
RecordCapturer(self.env['res.users'], []) as capture:
|
||||
RecordCapturer(self.env['res.users']) as capture:
|
||||
self.env['res.users'].web_create_users(src)
|
||||
|
||||
exp_emails = ['poiluchette@test.example.com', 'poilvache@test.example.com']
|
||||
# check reset password are effectively sent
|
||||
for user_email in exp_emails:
|
||||
self.assertMailMailWEmails(
|
||||
[user_email], 'sent',
|
||||
author=self.user_root.partner_id,
|
||||
email_values={
|
||||
'email_from': self.env.company.partner_id.email_formatted,
|
||||
},
|
||||
fields_values={
|
||||
'email_from': self.env.company.partner_id.email_formatted,
|
||||
},
|
||||
# do not use assertMailMailWEmails as mails are removed whatever we
|
||||
# try to do, code is using a savepoint to avoid storing mail.mail
|
||||
# in DB
|
||||
self.assertSentEmail(
|
||||
self.env.company.partner_id.email_formatted,
|
||||
[user_email],
|
||||
email_from=self.env.company.partner_id.email_formatted,
|
||||
)
|
||||
|
||||
# order does not seem guaranteed
|
||||
|
|
@ -58,7 +179,8 @@ class TestUser(MailCommon):
|
|||
sorted(exp_emails)
|
||||
)
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
|
||||
@tagged('-at_install', 'post_install', 'res_users')
|
||||
class TestUserTours(HttpCaseWithUserDemo):
|
||||
|
||||
def test_user_modify_own_profile(self):
|
||||
|
|
@ -69,6 +191,82 @@ class TestUserTours(HttpCaseWithUserDemo):
|
|||
'name': 'Marc Demo',
|
||||
'user_id': self.user_demo.id,
|
||||
})
|
||||
self.user_demo.group_ids += self.env.ref('hr.group_hr_user')
|
||||
self.user_demo.tz = "Europe/Brussels"
|
||||
self.start_tour("/web", "mail/static/tests/tours/user_modify_own_profile_tour.js", login="demo")
|
||||
self.assertEqual(self.user_demo.email, "updatedemail@example.com")
|
||||
self.user_demo.notification_type = "email"
|
||||
|
||||
# avoid 'reload_context' action in the middle of the tour to ease steps and form save checks
|
||||
with patch.object(ResUsersPatchedInTest, 'preference_save', lambda self: True):
|
||||
self.start_tour(
|
||||
"/odoo",
|
||||
"mail/static/tests/tours/user_modify_own_profile_tour.js",
|
||||
login="demo",
|
||||
)
|
||||
self.assertEqual(self.user_demo.notification_type, "inbox")
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestUserSettings(MailCommon):
|
||||
|
||||
@skip('Crashes in post_install, probably because other modules force creation through inverse (e.g. voip)')
|
||||
def test_create_portal_user(self):
|
||||
portal_group = self.env.ref('base.group_portal')
|
||||
user = self.env.user.create({
|
||||
'name': 'A portal user',
|
||||
'login': 'portal_test',
|
||||
'group_ids': [(6, 0, [portal_group.id])],
|
||||
})
|
||||
self.assertFalse(user.res_users_settings_ids, 'Portal users should not have settings by default')
|
||||
|
||||
def test_create_internal_user(self):
|
||||
user = self.env.user.create({
|
||||
'name': 'A internal user',
|
||||
'login': 'test_user',
|
||||
})
|
||||
self.assertTrue(user.res_users_settings_ids, 'Internal users should have settings by default')
|
||||
|
||||
@users('employee')
|
||||
def test_find_or_create_for_user_should_create_record_if_not_existing(self):
|
||||
self.user_employee.res_users_settings_ids.unlink() # pre autocreate or a portal user switching to internal user
|
||||
settings = self.user_employee.res_users_settings_ids
|
||||
self.assertFalse(settings, "no records should exist")
|
||||
|
||||
self.env['res.users.settings']._find_or_create_for_user(self.user_employee)
|
||||
settings = self.user_employee.res_users_settings_ids
|
||||
self.assertTrue(settings, "a record should be created after _find_or_create_for_user is called")
|
||||
|
||||
@users('employee')
|
||||
def test_find_or_create_for_user_should_return_correct_res_users_settings(self):
|
||||
self.user_employee.res_users_settings_ids.unlink()
|
||||
settings = self.env['res.users.settings'].create({
|
||||
'user_id': self.user_employee.id,
|
||||
})
|
||||
result = self.env['res.users.settings']._find_or_create_for_user(self.user_employee)
|
||||
self.assertEqual(result, settings, "Correct mail user settings should be returned")
|
||||
|
||||
@users('employee')
|
||||
def test_set_res_users_settings_should_send_notification_on_bus(self):
|
||||
settings = self.user_employee.res_users_settings_id
|
||||
settings.is_discuss_sidebar_category_chat_open = False
|
||||
settings.is_discuss_sidebar_category_channel_open = False
|
||||
|
||||
with self.assertBus(
|
||||
[(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||
[{
|
||||
'type': 'res.users.settings',
|
||||
'payload': {
|
||||
'id': settings.id,
|
||||
'is_discuss_sidebar_category_chat_open': True,
|
||||
},
|
||||
}]):
|
||||
settings.set_res_users_settings({'is_discuss_sidebar_category_chat_open': True})
|
||||
|
||||
@users('employee')
|
||||
def test_set_res_users_settings_should_set_settings_properly(self):
|
||||
settings = self.user_employee.res_users_settings_id
|
||||
settings.set_res_users_settings({'is_discuss_sidebar_category_chat_open': True})
|
||||
self.assertEqual(
|
||||
settings.is_discuss_sidebar_category_chat_open,
|
||||
True,
|
||||
"category state should be updated correctly"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests.common import users
|
||||
|
||||
|
||||
class TestResUsersSettings(MailCommon):
|
||||
|
||||
@users('employee')
|
||||
def test_find_or_create_for_user_should_create_record_if_not_existing(self):
|
||||
settings = self.user_employee.res_users_settings_ids
|
||||
self.assertFalse(settings, "no records should exist")
|
||||
|
||||
self.env['res.users.settings']._find_or_create_for_user(self.user_employee)
|
||||
settings = self.user_employee.res_users_settings_ids
|
||||
self.assertTrue(settings, "a record should be created after _find_or_create_for_user is called")
|
||||
|
||||
@users('employee')
|
||||
def test_find_or_create_for_user_should_return_correct_res_users_settings(self):
|
||||
settings = self.env['res.users.settings'].create({
|
||||
'user_id': self.user_employee.id,
|
||||
})
|
||||
result = self.env['res.users.settings']._find_or_create_for_user(self.user_employee)
|
||||
self.assertEqual(result, settings, "Correct mail user settings should be returned")
|
||||
|
||||
@users('employee')
|
||||
def test_set_res_users_settings_should_send_notification_on_bus(self):
|
||||
settings = self.env['res.users.settings'].create({
|
||||
'is_discuss_sidebar_category_channel_open': False,
|
||||
'is_discuss_sidebar_category_chat_open': False,
|
||||
'user_id': self.user_employee.id,
|
||||
})
|
||||
|
||||
with self.assertBus(
|
||||
[(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||
[{
|
||||
'type': 'res.users.settings/insert',
|
||||
'payload': {
|
||||
'id': settings.id,
|
||||
'is_discuss_sidebar_category_chat_open': True,
|
||||
},
|
||||
}]):
|
||||
settings.set_res_users_settings({'is_discuss_sidebar_category_chat_open': True})
|
||||
|
||||
@users('employee')
|
||||
def test_set_res_users_settings_should_set_settings_properly(self):
|
||||
settings = self.env['res.users.settings'].create({
|
||||
'is_discuss_sidebar_category_channel_open': False,
|
||||
'is_discuss_sidebar_category_chat_open': False,
|
||||
'user_id': self.user_employee.id,
|
||||
})
|
||||
settings.set_res_users_settings({'is_discuss_sidebar_category_chat_open': True})
|
||||
self.assertEqual(
|
||||
settings.is_discuss_sidebar_category_chat_open,
|
||||
True,
|
||||
"category state should be updated correctly"
|
||||
)
|
||||
|
|
@ -1,873 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import users
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('RTC')
|
||||
class TestChannelInternals(MailCommon):
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_01_join_call(self):
|
||||
"""Join call should remove existing sessions, remove invitation, create a new session, and return data."""
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_create(name='Test Channel', group_id=self.env.ref('base.group_user').id)['id'])
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # end of previous session
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update sessions
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update sessions
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.channel.rtc.session/ended',
|
||||
'payload': {
|
||||
'sessionId': channel_member.rtc_session_ids.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert-and-unlink', [{'id': channel_member.rtc_session_ids.id}])],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert', [{
|
||||
'id': channel_member.rtc_session_ids.id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"persona": {
|
||||
"partner": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
"im_status": channel_member.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
}])],
|
||||
},
|
||||
},
|
||||
]
|
||||
):
|
||||
res = channel_member._rtc_join_call()
|
||||
self.assertEqual(res, {
|
||||
'iceServers': False,
|
||||
'rtcSessions': [
|
||||
('insert', [{
|
||||
'id': channel_member.rtc_session_ids.id,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"persona": {
|
||||
"partner": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
"im_status": channel_member.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
}]),
|
||||
('insert-and-unlink', [{'id': channel_member.rtc_session_ids.id - 1}]),
|
||||
],
|
||||
'sessionId': channel_member.rtc_session_ids.id,
|
||||
})
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_10_start_call_in_chat_should_invite_all_members_to_call(self):
|
||||
test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'})
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].channel_get(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id'])
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member_test_user = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == test_user.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
last_rtc_session_id = channel_member.rtc_session_ids.id
|
||||
channel_member._rtc_leave_call()
|
||||
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update new session
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # message_post "started a live conference" (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of invitations
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert', [{
|
||||
'id': last_rtc_session_id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"persona": {
|
||||
"partner": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
"im_status": channel_member.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
}])],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'invitedMembers': [('insert', [{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'persona': {
|
||||
'partner': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
'im_status': channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
}])],
|
||||
},
|
||||
},
|
||||
]
|
||||
):
|
||||
res = channel_member._rtc_join_call()
|
||||
self.assertIn('invitedMembers', res)
|
||||
self.assertEqual(res['invitedMembers'], [('insert', [{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'persona': {
|
||||
'partner': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
'im_status': channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
}])])
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_11_start_call_in_group_should_invite_all_members_to_call(self):
|
||||
test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'})
|
||||
test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"})
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id'])
|
||||
channel.add_members(guest_ids=test_guest.ids)
|
||||
channel_member_test_user = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == test_user.partner_id)
|
||||
channel_member_test_guest = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.guest_id == test_guest)
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
last_rtc_session_id = channel_member.rtc_session_ids.id
|
||||
channel_member._rtc_leave_call()
|
||||
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update new session
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # message_post "started a live conference" (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation
|
||||
(self.cr.dbname, 'mail.guest', test_guest.id), # incoming invitation
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of invitations
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert', [{
|
||||
'id': last_rtc_session_id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"persona": {
|
||||
"partner": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
"im_status": channel_member.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
}])],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert', [{
|
||||
'id': last_rtc_session_id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"persona": {
|
||||
"partner": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
"im_status": channel_member.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
}])],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'invitedMembers': [('insert', [
|
||||
{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'persona': {
|
||||
'partner': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
'im_status': channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'persona': {
|
||||
'guest': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
'im_status': channel_member_test_guest.guest_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
])],
|
||||
},
|
||||
},
|
||||
]
|
||||
):
|
||||
res = channel_member._rtc_join_call()
|
||||
self.assertIn('invitedMembers', res)
|
||||
self.assertEqual(res['invitedMembers'], [('insert', [
|
||||
{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'persona': {
|
||||
'partner': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
'im_status': channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'persona': {
|
||||
'guest': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
'im_status': channel_member_test_guest.guest_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
])])
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_20_join_call_should_cancel_pending_invitations(self):
|
||||
test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'})
|
||||
test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"})
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id'])
|
||||
channel.add_members(guest_ids=test_guest.ids)
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
|
||||
channel_member_test_user = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == test_user.partner_id)
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update invitation
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of invitations
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update sessions
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'rtcInvitingSession': [('unlink',)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'invitedMembers': [('insert-and-unlink', [{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'persona': {
|
||||
'partner': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
'im_status': channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
}])],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert', [
|
||||
{
|
||||
'id': channel_member.rtc_session_ids.id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member_test_user.id,
|
||||
"channel": {"id": channel_member_test_user.channel_id.id},
|
||||
"persona": {
|
||||
"partner": {
|
||||
"id": channel_member_test_user.partner_id.id,
|
||||
"name": channel_member_test_user.partner_id.name,
|
||||
"im_status": channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
},
|
||||
])],
|
||||
},
|
||||
},
|
||||
]
|
||||
):
|
||||
channel_member_test_user._rtc_join_call()
|
||||
|
||||
channel_member_test_guest = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.guest_id == test_guest)
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'mail.guest', test_guest.id), # update invitation
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of invitations
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update sessions
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'rtcInvitingSession': [('unlink',)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'invitedMembers': [('insert-and-unlink', [{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'persona': {
|
||||
'guest': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
'im_status': channel_member_test_guest.guest_id.im_status,
|
||||
},
|
||||
},
|
||||
}])],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert', [
|
||||
{
|
||||
'id': channel_member.rtc_session_ids.id + 2,
|
||||
'channelMember': {
|
||||
"id": channel_member_test_guest.id,
|
||||
"channel": {"id": channel_member_test_guest.channel_id.id},
|
||||
"persona": {
|
||||
"guest": {
|
||||
"id": channel_member_test_guest.guest_id.id,
|
||||
"name": channel_member_test_guest.guest_id.name,
|
||||
'im_status': channel_member_test_guest.guest_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
},
|
||||
])],
|
||||
},
|
||||
},
|
||||
]
|
||||
):
|
||||
channel_member_test_guest._rtc_join_call()
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_21_leave_call_should_cancel_pending_invitations(self):
|
||||
test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'})
|
||||
test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"})
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id'])
|
||||
channel.add_members(guest_ids=test_guest.ids)
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
|
||||
channel_member_test_user = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == test_user.partner_id)
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update invitation
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of invitations
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'rtcInvitingSession': [('unlink',)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'invitedMembers': [('insert-and-unlink', [{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'persona': {
|
||||
'partner': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
'im_status': channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
}])],
|
||||
},
|
||||
},
|
||||
]
|
||||
):
|
||||
channel_member_test_user._rtc_leave_call()
|
||||
|
||||
channel_member_test_guest = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.guest_id == test_guest)
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'mail.guest', test_guest.id), # update invitation
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of invitations
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'rtcInvitingSession': [('unlink',)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'invitedMembers': [('insert-and-unlink', [{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'persona': {
|
||||
'guest': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
'im_status': channel_member_test_guest.guest_id.im_status,
|
||||
},
|
||||
},
|
||||
}])],
|
||||
},
|
||||
},
|
||||
]
|
||||
):
|
||||
channel_member_test_guest._rtc_leave_call()
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_25_lone_call_participant_leaving_call_should_cancel_pending_invitations(self):
|
||||
test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'})
|
||||
test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"})
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=(self.user_employee.partner_id + test_user.partner_id).ids)['id'])
|
||||
channel.add_members(guest_ids=test_guest.ids)
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member_test_user = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == test_user.partner_id)
|
||||
channel_member_test_guest = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.guest_id == test_guest)
|
||||
channel_member._rtc_join_call()
|
||||
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # end session
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update invitation
|
||||
(self.cr.dbname, 'mail.guest', test_guest.id), # update invitation
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of invitations
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update sessions
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.channel.rtc.session/ended',
|
||||
'payload': {
|
||||
'sessionId': channel_member.rtc_session_ids.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'rtcInvitingSession': [('unlink',)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'rtcInvitingSession': [('unlink',)],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'invitedMembers': [('insert-and-unlink', [
|
||||
{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'persona': {
|
||||
'partner': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
'im_status': channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'persona': {
|
||||
'guest': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
'im_status': channel_member_test_guest.guest_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
])],
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert-and-unlink', [{'id': channel_member.rtc_session_ids.id}])],
|
||||
},
|
||||
},
|
||||
]
|
||||
):
|
||||
channel_member._rtc_leave_call()
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_30_add_members_while_in_call_should_invite_new_members_to_call(self):
|
||||
test_user = self.env['res.users'].sudo().create({'name': "Test User", 'login': 'test'})
|
||||
test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"})
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id'])
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda member: member.partner_id == self.user_employee.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
|
||||
with self.mock_bus():
|
||||
channel.add_members(partner_ids=test_user.partner_id.ids, guest_ids=test_guest.ids, invite_to_rtc_call=True)
|
||||
|
||||
channel_member_test_user = channel.sudo().channel_member_ids.filtered(lambda member: member.partner_id == test_user.partner_id)
|
||||
channel_member_test_guest = channel.sudo().channel_member_ids.filtered(lambda member: member.guest_id == test_guest)
|
||||
found_bus_notifs = self.assertBusNotifications(
|
||||
[
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # channel joined (not asserted below)
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # message_post "invited" (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below)
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # new members (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # incoming invitation
|
||||
(self.cr.dbname, 'mail.guest', test_guest.id), # incoming invitation
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of invitations
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # update of last interest (not asserted below)
|
||||
(self.cr.dbname, 'res.partner', test_user.partner_id.id), # update of last interest (not asserted below)
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # new member (guest) (not asserted below)
|
||||
(self.cr.dbname, 'mail.guest', test_guest.id), # channel joined for guest (not asserted below)
|
||||
],
|
||||
message_items=[
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'rtcInvitingSession': {
|
||||
'id': channel_member.rtc_session_ids.id,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"persona": {
|
||||
"partner": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
"im_status": channel_member.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'rtcInvitingSession': {
|
||||
'id': channel_member.rtc_session_ids.id,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"persona": {
|
||||
"partner": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
"im_status": channel_member.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
'isCameraOn': False,
|
||||
'isDeaf': False,
|
||||
'isSelfMuted': False,
|
||||
'isScreenSharingOn': False,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.thread/insert',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'invitedMembers': [('insert', [
|
||||
{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'persona': {
|
||||
'partner': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
'im_status': channel_member_test_user.partner_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'persona': {
|
||||
'guest': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
'im_status': channel_member_test_guest.guest_id.im_status,
|
||||
},
|
||||
},
|
||||
},
|
||||
])],
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
self.assertEqual(self._new_bus_notifs, found_bus_notifs)
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_40_leave_call_should_remove_existing_sessions_of_user_in_channel_and_return_data(self):
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id'])
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # end session
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of sessions
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.channel.rtc.session/ended',
|
||||
'payload': {
|
||||
'sessionId': channel_member.rtc_session_ids.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert-and-unlink', [{'id': channel_member.rtc_session_ids.id}])],
|
||||
},
|
||||
},
|
||||
],
|
||||
):
|
||||
channel_member._rtc_leave_call()
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_50_garbage_collect_should_remove_old_sessions_and_notify_data(self):
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id'])
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
channel_member.rtc_session_ids.flush_model()
|
||||
channel_member.rtc_session_ids._write({'write_date': fields.Datetime.now() - relativedelta(days=2)})
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # session ended
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of sessions
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.channel.rtc.session/ended',
|
||||
'payload': {
|
||||
'sessionId': channel_member.rtc_session_ids.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert-and-unlink', [{'id': channel_member.rtc_session_ids.id}])],
|
||||
},
|
||||
},
|
||||
],
|
||||
):
|
||||
self.env['mail.channel.rtc.session'].sudo()._gc_inactive_sessions()
|
||||
self.assertFalse(channel_member.rtc_session_ids)
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_51_action_disconnect_should_remove_selected_session_and_notify_data(self):
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id'])
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
channel_member._rtc_join_call()
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'res.partner', self.user_employee.partner_id.id), # session ended
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of sessions
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.channel.rtc.session/ended',
|
||||
'payload': {
|
||||
'sessionId': channel_member.rtc_session_ids.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert-and-unlink', [{'id': channel_member.rtc_session_ids.id}])],
|
||||
},
|
||||
},
|
||||
],
|
||||
):
|
||||
channel_member.rtc_session_ids.action_disconnect()
|
||||
self.assertFalse(channel_member.rtc_session_ids)
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_60_rtc_sync_sessions_should_gc_and_return_outdated_and_active_sessions(self):
|
||||
channel = self.env['mail.channel'].browse(self.env['mail.channel'].create_group(partners_to=self.user_employee.partner_id.ids)['id'])
|
||||
channel_member = channel.sudo().channel_member_ids.filtered(lambda channel_member: channel_member.partner_id == self.user_employee.partner_id)
|
||||
join_call_values = channel_member._rtc_join_call()
|
||||
test_guest = self.env['mail.guest'].sudo().create({'name': "Test Guest"})
|
||||
test_channel_member = self.env['mail.channel.member'].create({
|
||||
'guest_id': test_guest.id,
|
||||
'channel_id': channel.id,
|
||||
})
|
||||
test_session = self.env['mail.channel.rtc.session'].sudo().create({'channel_member_id': test_channel_member.id})
|
||||
test_session.flush_model()
|
||||
test_session._write({'write_date': fields.Datetime.now() - relativedelta(days=2)})
|
||||
unused_ids = [9998, 9999]
|
||||
self.env['bus.bus'].sudo().search([]).unlink()
|
||||
with self.assertBus(
|
||||
[
|
||||
(self.cr.dbname, 'mail.guest', test_guest.id), # session ended
|
||||
(self.cr.dbname, 'mail.channel', channel.id), # update list of sessions
|
||||
],
|
||||
[
|
||||
{
|
||||
'type': 'mail.channel.rtc.session/ended',
|
||||
'payload': {
|
||||
'sessionId': test_session.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'mail.channel/rtc_sessions_update',
|
||||
'payload': {
|
||||
'id': channel.id,
|
||||
'rtcSessions': [('insert-and-unlink', [{'id': test_session.id}])],
|
||||
},
|
||||
},
|
||||
],
|
||||
):
|
||||
current_rtc_sessions, outdated_rtc_sessions = channel_member._rtc_sync_sessions(check_rtc_session_ids=[join_call_values['sessionId']] + unused_ids)
|
||||
self.assertEqual(channel_member.rtc_session_ids, current_rtc_sessions)
|
||||
self.assertEqual(unused_ids, outdated_rtc_sessions.ids)
|
||||
self.assertFalse(outdated_rtc_sessions.exists())
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
|
||||
|
||||
class TestUserModifyOwnProfile(HttpCaseWithUserDemo):
|
||||
pass
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.http import root, SESSION_ROTATION_INTERVAL
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.addons.bus.models.bus import channel_with_db, json_dump
|
||||
|
||||
|
||||
class TestWebsocketController(HttpCaseWithUserDemo):
|
||||
def test_im_status_offline_on_websocket_closed(self):
|
||||
self.authenticate("demo", "demo")
|
||||
self.env["mail.presence"]._update_presence(self.user_demo)
|
||||
self.env.cr.precommit.run() # trigger the creation of bus.bus records
|
||||
self.env["bus.bus"].search([]).unlink()
|
||||
self.make_jsonrpc_request("/websocket/on_closed", {})
|
||||
self.env.cr.precommit.run() # trigger the creation of bus.bus records
|
||||
message = self.make_jsonrpc_request(
|
||||
"/websocket/peek_notifications",
|
||||
{
|
||||
"channels": [f"odoo-presence-res.partner_{self.partner_demo.id}"],
|
||||
"last": 0,
|
||||
"is_first_poll": True,
|
||||
},
|
||||
)["notifications"][0]["message"]
|
||||
self.assertEqual(message["type"], "bus.bus/im_status_updated")
|
||||
self.assertEqual(message["payload"]["partner_id"], self.partner_demo.id)
|
||||
self.assertEqual(message["payload"]["im_status"], "offline")
|
||||
self.assertEqual(message["payload"]["presence_status"], "offline")
|
||||
|
||||
def test_receive_missed_presences_on_peek_notifications(self):
|
||||
self.authenticate("demo", "demo")
|
||||
self.env["mail.presence"]._update_presence(self.user_demo)
|
||||
self.env.cr.precommit.run() # trigger the creation of bus.bus records
|
||||
# First request will get notifications and trigger the creation
|
||||
# of the missed presences one.
|
||||
last_id = self.env["bus.bus"]._bus_last_id()
|
||||
self.make_jsonrpc_request(
|
||||
"/websocket/peek_notifications",
|
||||
{
|
||||
"channels": [f"odoo-presence-res.partner_{self.partner_demo.id}"],
|
||||
"last": last_id,
|
||||
"is_first_poll": True,
|
||||
},
|
||||
)
|
||||
self.env.cr.precommit.run() # trigger the creation of bus.bus records
|
||||
notification = self.make_jsonrpc_request(
|
||||
"/websocket/peek_notifications",
|
||||
{
|
||||
"channels": [f"odoo-presence-res.partner_{self.partner_demo.id}"],
|
||||
"last": last_id,
|
||||
"is_first_poll": True,
|
||||
},
|
||||
)["notifications"][0]
|
||||
bus_record = self.env["bus.bus"].search([("id", "=", int(notification["id"]))])
|
||||
self.assertEqual(
|
||||
bus_record.channel, json_dump(channel_with_db(self.env.cr.dbname, self.partner_demo))
|
||||
)
|
||||
self.assertEqual(notification["message"]["type"], "bus.bus/im_status_updated")
|
||||
self.assertEqual(notification["message"]["payload"]["partner_id"], self.partner_demo.id)
|
||||
self.assertEqual(notification["message"]["payload"]["im_status"], "online")
|
||||
self.assertEqual(notification["message"]["payload"]["presence_status"], "online")
|
||||
|
||||
def test_do_not_rotate_session_when_updating_presence(self):
|
||||
self.authenticate('admin', 'admin')
|
||||
self.url_open('/odoo')
|
||||
original_session = self.opener.cookies['session_id']
|
||||
original_session_obj = root.session_store.get(original_session)
|
||||
original_session_obj['create_time'] -= SESSION_ROTATION_INTERVAL
|
||||
root.session_store.save(original_session_obj)
|
||||
self.make_jsonrpc_request('/websocket/peek_notifications', {
|
||||
'channels': [],
|
||||
'last': 0,
|
||||
'is_first_poll': True,
|
||||
})
|
||||
self.make_jsonrpc_request('/websocket/update_bus_presence', {'inactivity_period': 0})
|
||||
self.assertEqual(self.opener.cookies['session_id'], original_session)
|
||||
self.url_open("/odoo")
|
||||
self.assertNotEqual(self.opener.cookies['session_id'], original_session)
|
||||
Loading…
Add table
Add a link
Reference in a new issue