Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,22 @@
# 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_link_preview
from . import test_mail_channel
from . import test_mail_channel_as_guest
from . import test_mail_channel_member
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_render
from . import test_mail_template
from . import test_mail_tools
from . import test_res_partner
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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,99 @@
# 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)

View file

@ -0,0 +1,209 @@
# -*- 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

View file

@ -0,0 +1,50 @@
# -*- 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'])

View file

@ -0,0 +1,107 @@
# -*- 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 unittest.mock import patch
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
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.public_channel = cls.env['mail.channel'].create({
'name': 'Public channel of user 1',
'channel_type': 'channel',
})
cls.public_channel.channel_member_ids.unlink()
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 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 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)

View file

@ -0,0 +1,578 @@
# -*- 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)

View file

@ -0,0 +1,86 @@
# -*- 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)

View file

@ -0,0 +1,278 @@
# -*- 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)
])

View file

@ -0,0 +1,263 @@
# -*- 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.exceptions import AccessError
from odoo.tests import Form, tagged, users
from odoo.tools import mute_logger
@tagged('mail_composer')
class TestMailComposer(MailCommon):
@classmethod
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.test_record = cls.env['res.partner'].with_context(cls._test_context).create({
'name': 'Test',
})
cls.body_html = """<div>
<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;">&nbsp;</i>
<![endif]-->
A link for you! <!-- my favorite example -->
<!--[if mso]>
<i style="letter-spacing: 25px; mso-font-width: -100%;">&nbsp;</i>
<![endif]-->
</a> Make good use of it.</p>
</div>"""
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',
})
@tagged('mail_composer')
class TestMailComposerForm(TestMailComposer):
""" Test mail composer form view usage. """
@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.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',
},
{
'email': 'private.customer.2@test.example.com',
'phone': '0032455445566',
'name': 'Private Customer 2',
'type': 'private',
},
{
'email': 'not.private@test.example.com',
'phone': '0032455778899',
'name': 'Classic Customer',
'type': 'contact',
}
])
@mute_logger('odoo.addons.mail.models.mail_mail')
@users('employee')
def test_composer_default_recipients(self):
""" Test usage of a private partner in composer, as default value """
partner_classic = self.partner_classic.with_env(self.env)
test_record = self.test_record.with_env(self.env)
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,
}))
form.body = '<p>Hello</p>'
self.assertEqual(
form.partner_ids._get_ids(), partner_classic.ids,
'Default populates the field'
)
saved_form = form.save()
self.assertEqual(
saved_form.partner_ids, partner_classic,
'Default value is kept at save'
)
with self.mock_mail_gateway():
saved_form._action_send_mail()
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}')
@mute_logger('odoo.addons.mail.models.mail_mail')
@users('employee')
def test_composer_default_recipients_private(self):
""" Test usage of a private partner in composer, as default value """
partner_private = self.partner_private.with_env(self.env)
partner_classic = self.partner_classic.with_env(self.env)
test_record = self.test_record.with_env(self.env)
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,
}))
form.body = '<p>Hello</p>'
self.assertEqual(
sorted(form.partner_ids._get_ids()),
sorted((partner_private + partner_classic).ids),
'Default populates the field'
)
saved_form = form.save()
self.assertEqual(
saved_form.partner_ids, partner_private + partner_classic,
'Default value is kept at save'
)
with self.mock_mail_gateway():
saved_form._action_send_mail()
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}')
@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),
]})
with self.assertRaises(AccessError):
_name = self.partner_private.with_env(self.env).name
partner_classic = self.partner_classic.with_env(self.env)
test_record = self.test_record.with_env(self.env)
with self.assertRaises(AccessError):
_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,
}))
@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
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}',
})
template = self.mail_template.with_env(self.env)
partner_private = self.partner_private.with_env(self.env)
partner_private_2 = self.partner_private_2.with_env(self.env)
partner_classic = self.partner_classic.with_env(self.env)
test_record = self.test_record.with_env(self.env)
form = Form(self.env['mail.compose.message'].with_context({
'default_model': test_record._name,
'default_res_id': test_record.id,
'default_template_id': template.id,
}))
# transformation from email_to into partner_ids: find or create
existing_partner = self.env['res.partner'].search(
[('email_normalized', '=', self.partner_private_2.email_normalized)]
)
self.assertEqual(existing_partner, partner_private_2, 'Should find existing private contact')
new_partner = self.env['res.partner'].search(
[('email_normalized', '=', email_to_new)]
)
self.assertEqual(new_partner.type, 'contact', 'Should create a new contact')
self.assertEqual(
sorted(form.partner_ids._get_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)'
)
with self.mock_mail_gateway():
saved_form._action_send_mail()
message = self.test_record.message_ids[0]
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'
)
self.assertEqual(message.subject, 'MSO FTW')
@tagged('mail_composer')
class TestMailComposerRendering(TestMailComposer):
""" Test rendering and support of various html tweaks in composer """
@users('employee')
def test_mail_mass_mode_template_with_mso(self):
mail_compose_message = self.env['mail.compose.message'].create({
'composition_mode': 'mass_mail',
'model': 'res.partner',
'template_id': self.mail_template.id,
'subject': 'MSO FTW',
})
values = mail_compose_message.get_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'
)
@mute_logger('odoo.addons.mail.models.mail_mail')
@users('employee')
def test_mail_mass_mode_compose_with_mso(self):
composer = self.env['mail.compose.message'].with_context({
'default_model': self.test_record._name,
'default_composition_mode': 'mass_mail',
'active_ids': [self.test_record.id],
'active_model': self.test_record._name,
'active_id': self.test_record.id
}).create({
'body': self.body_html,
'partner_ids': [(4, self.partner_employee.id)],
'composition_mode': 'mass_mail',
})
with self.mock_mail_gateway(mail_unlink_sent=True):
composer._action_send_mail()
values = composer.get_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'
)

View file

@ -0,0 +1,31 @@
# -*- 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)

View file

@ -0,0 +1,35 @@
from odoo.tests import TransactionCase
from unittest import mock
import smtplib
class MailCase(TransactionCase):
def test_mail_send_non_connected_smtp_session(self):
"""Check to avoid SMTPServerDisconnected error while trying to
disconnect smtp session that is not connected.
This used to happens while trying to connect to a
google smtp server with an expired token.
Or here testing non recipients emails with non connected
smtp session, we won't get SMTPServerDisconnected that would
hide the other error that is raised earlier.
"""
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.mail.models.mail_mail._logger.info") as mock_logging_info:
mail.send()
disconnected_smtpsession.quit.assert_called_once()
mock_logging_info.assert_any_call(
"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."
)

View file

@ -0,0 +1,16 @@
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')

View file

@ -0,0 +1,505 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from markupsafe import Markup
from unittest.mock import patch
from odoo.addons.mail.tests import common
from odoo.exceptions import AccessError
from odoo.tests import tagged, users
class TestMailRenderCommon(common.MailCommon):
@classmethod
def setUpClass(cls):
super(TestMailRenderCommon, cls).setUpClass()
# activate multi language support
cls.env['res.lang']._activate_lang('fr_FR')
cls.user_admin.write({'lang': 'en_US'})
# test records
cls.render_object = cls.env['res.partner'].create({
'name': 'TestRecord',
'lang': 'en_US',
})
cls.render_object_fr = cls.env['res.partner'].create({
'name': 'Element de Test',
'lang': 'fr_FR',
})
# some jinja templates
cls.base_inline_template_bits = [
'<p>Hello</p>',
'<p>Hello {{ object.name }}</p>',
"""<p>
{{ '<span>English Speaker</span>' if object.lang == 'en_US' else '<span>Other Speaker</span>' }}
</p>""",
"""
<p>{{ 13 + 13 }}</p>
<h1>This is a test</h1>
""",
"""<b>Test</b>{{ '' if True else '<b>Code not executed</b>' }}""",
]
cls.base_inline_template_bits_fr = [
'<p>Bonjour</p>',
'<p>Bonjour {{ object.name }}</p>',
"""<p>
{{ '<span>Narrateur Anglais</span>' if object.lang == 'en_US' else '<span>Autre Narrateur</span>' }}
</p>"""
]
# some qweb templates, their views and their xml ids
cls.base_qweb_bits = [
'<p>Hello</p>',
'<p>Hello <t t-esc="object.name"/></p>',
"""<p>
<span t-if="object.lang == 'en_US'">English Speaker</span>
<span t-else="">Other Speaker</span>
</p>"""
]
cls.base_qweb_bits_fr = [
'<p>Bonjour</p>',
'<p>Bonjour <t t-esc="object.name"/></p>',
"""<p>
<span t-if="object.lang == 'en_US'">Narrateur Anglais</span>
<span t-else="">Autre Narrateur</span>
</p>"""
]
cls.base_qweb_templates = cls.env['ir.ui.view'].create([
{'name': 'TestRender%d' % index,
'type': 'qweb',
'arch': qweb_content,
} for index, qweb_content in enumerate(cls.base_qweb_bits)
])
cls.base_qweb_templates_data = cls.env['ir.model.data'].create([
{'name': template.name, 'module': 'mail',
'model': template._name, 'res_id': template.id,
} for template in cls.base_qweb_templates
])
cls.base_qweb_templates_xmlids = [
model_data.complete_name
for model_data in cls.base_qweb_templates_data
]
# render result
cls.base_rendered = [
'<p>Hello</p>',
'<p>Hello %s</p>' % cls.render_object.name,
"""<p>
<span>English Speaker</span>
</p>"""
]
cls.base_rendered_fr = [
'<p>Bonjour</p>',
'<p>Bonjour %s</p>' % cls.render_object_fr.name,
"""<p>
<span>Autre Narrateur</span>
</p>"""
]
# link to mail template
cls.test_template = cls.env['mail.template'].create({
'name': 'Test Template',
'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 }}'
})
# some translations
cls.test_template.with_context(lang='fr_FR').subject = cls.base_qweb_bits_fr[0]
cls.test_template.with_context(lang='fr_FR').body_html = cls.base_qweb_bits_fr[1]
cls.env['ir.model.data'].create({
'name': 'test_template_xmlid',
'module': 'mail',
'model': cls.test_template._name,
'res_id': cls.test_template.id,
})
# Enable group-based template management
cls.env['ir.config_parameter'].set_param('mail.restrict.template.rendering', True)
# User without the group "mail.group_mail_template_editor"
cls.user_rendering_restricted = common.mail_new_test_user(
cls.env, login='user_rendering_restricted',
groups='base.group_user',
company_id=cls.company_admin.id,
name='Code Template Restricted User',
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')
@tagged('mail_render')
class TestMailRender(TestMailRenderCommon):
@users('employee')
def test_evaluation_context(self):
""" Test evaluation context and various ways of tweaking it. """
partner = self.env['res.partner'].browse(self.render_object.ids)
MailRenderMixin = self.env['mail.render.mixin']
custom_ctx = {'custom_ctx': 'Custom Context Value'}
add_context = {
'custom_value': 'Custom Render Value'
}
srces = [
'<b>I am {{ user.name }}</b>',
'<span>Datetime is {{ format_datetime(datetime.datetime(2021, 6, 1), dt_format="MM - d - YYY") }}</span>',
'<span>Context {{ ctx.get("custom_ctx") }}, value {{ custom_value }}</span>',
]
results = [
'<b>I am %s</b>' % self.env.user.name,
'<span>Datetime is 06 - 1 - 2021</span>',
'<span>Context Custom Context Value, value Custom Render Value</span>'
]
for src, expected in zip(srces, results):
for engine in ['inline_template']:
result = MailRenderMixin.with_context(**custom_ctx)._render_template(
src, partner._name, partner.ids,
engine=engine, add_context=add_context
)[partner.id]
self.assertEqual(expected, result)
@users('employee')
def test_prepend_preview_inline_template_to_qweb(self):
body = 'body'
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="&#34;false&#34; if 1 &gt; 2 else &#34;true&#34;"/>bar
</div>body''')
@users('employee')
def test_render_field(self):
template = self.env['mail.template'].browse(self.test_template.ids)
partner = self.env['res.partner'].browse(self.render_object.ids)
for fname, expected in zip(['subject', 'body_html'], self.base_rendered):
rendered = template._render_field(
fname,
partner.ids,
compute_lang=True
)[partner.id]
self.assertEqual(rendered, expected)
@users('employee')
def test_render_field_lang(self):
""" Test translation in french """
template = self.env['mail.template'].browse(self.test_template.ids)
partner = self.env['res.partner'].browse(self.render_object_fr.ids)
for fname, expected in zip(['subject', 'body_html'], self.base_rendered_fr):
rendered = template._render_field(
fname,
partner.ids,
compute_lang=True
)[partner.id]
self.assertEqual(rendered, expected)
@users('employee')
def test_render_template_inline_template(self):
partner = self.env['res.partner'].browse(self.render_object.ids)
for source, expected in zip(self.base_inline_template_bits, self.base_rendered):
rendered = self.env['mail.render.mixin']._render_template(
source,
partner._name,
partner.ids,
engine='inline_template',
)[partner.id]
self.assertEqual(rendered, expected)
@users('employee')
def test_render_template_inline_template_w_post_process_custom_local_links(self):
def _mock_get_base_url(recordset):
return f"http://www.render-object-{recordset._name}-{recordset.id}-{recordset.display_name}.com"
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):
# 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(
'<a href="/test/destination"><img src="/test/image"></a>',
'res.partner',
partner_ids,
engine='inline_template',
post_process=True,
)
Partner = self.env['res.partner'].with_prefetch(partner_ids)
for partner_id, render_result in render_results.items():
partner = Partner.browse(partner_id)
expected_base_url = f"http://www.render-object-{partner._name}-{partner.id}-{partner.name}.com"
self.assertEqual(render_result, f'<a href="{expected_base_url}/test/destination"><img src="{expected_base_url}/test/image"></a>')
@users('employee')
def test_render_template_qweb(self):
partner = self.env['res.partner'].browse(self.render_object.ids)
for source, expected in zip(self.base_qweb_bits, self.base_rendered):
rendered = self.env['mail.render.mixin']._render_template(
source,
partner._name,
partner.ids,
engine='qweb',
)[partner.id]
self.assertEqual(rendered, expected)
@users('employee')
def test_render_template_qweb_view(self):
partner = self.env['res.partner'].browse(self.render_object.ids)
for source, expected in zip(self.base_qweb_templates_xmlids, self.base_rendered):
rendered = self.env['mail.render.mixin']._render_template(
source,
partner._name,
partner.ids,
engine='qweb_view',
)[partner.id]
self.assertEqual(rendered, expected)
@users('employee')
def test_render_template_various(self):
""" Test static rendering """
partner = self.env['res.partner'].browse(self.render_object.ids)
MailRenderMixin = self.env['mail.render.mixin']
# static string
src = 'This is a string'
expected = 'This is a string'
for engine in ['inline_template']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# code string
src = 'This is a string with a number {{ 13+13 }}'
expected = 'This is a string with a number 26'
for engine in ['inline_template']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# block string
src = "This is a string with a block {{ 'hidden' if False else 'displayed' }}"
expected = 'This is a string with a block displayed'
for engine in ['inline_template']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# static xml
src = '<p class="text-muted"><span>This is a string</span></p>'
expected = '<p class="text-muted"><span>This is a string</span></p>'
for engine in ['inline_template', 'qweb']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result) # tde: checkme
# code xml
srces = [
'<p class="text-muted"><span>This is a string with a number {{ 13+13 }}</span></p>',
'<p class="text-muted"><span>This is a string with a number <t t-out="13+13"/></span></p>',
]
expected = '<p class="text-muted"><span>This is a string with a number 26</span></p>'
for engine, src in zip(['inline_template', 'qweb'], srces):
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, str(result))
src = """<p>
<t t-set="line_statement_variable" t-value="3" />
<span>We have <t t-out="line_statement_variable" /> cookies in stock</span>
<span>We have <t t-set="block_variable" t-value="4" /><t t-out="block_variable" /> cookies in stock</span>
</p>"""
expected = """<p>
<span>We have 3 cookies in stock</span>
<span>We have 4 cookies in stock</span>
</p>"""
for engine in ['qweb']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(result, expected)
@users('employee')
def test_replace_local_links(self):
local_links_template_bits = [
'<a href="/web/path?a=a&b=b"/>',
'<img src="/web/path?a=a&b=b"/>',
'<v:fill src="/web/path?a=a&b=b"/>',
'<v:image src="/web/path?a=a&b=b"/>',
'<div style="background-image:url(/web/path?a=a&b=b);"/>',
'<div style="background-image:url(\'/web/path?a=a&b=b\');"/>',
'<div style="background-image:url(&#34;/web/path?a=a&b=b&#34;);"/>',
'<div background="/web/path?a=a&b=b"/>',
]
base_url = self.env['mail.render.mixin'].get_base_url()
rendered_local_links = [
'<a href="%s/web/path?a=a&b=b"/>' % base_url,
'<img src="%s/web/path?a=a&b=b"/>' % base_url,
'<v:fill src="%s/web/path?a=a&b=b"/>' % base_url,
'<v:image src="%s/web/path?a=a&b=b"/>' % base_url,
'<div style="background-image:url(%s/web/path?a=a&b=b);"/>' % base_url,
'<div style="background-image:url(\'%s/web/path?a=a&b=b\');"/>' % base_url,
'<div style="background-image:url(&#34;%s/web/path?a=a&b=b&#34;);"/>' % base_url,
'<div background="%s/web/path?a=a&b=b"/>' % base_url,
]
for source, expected in zip(local_links_template_bits, rendered_local_links):
rendered = self.env['mail.render.mixin']._replace_local_links(source)
self.assertEqual(rendered, expected)
@tagged('mail_render')
class TestMailRenderSecurity(TestMailRenderCommon):
""" Test security of rendering, based on qweb finding + restricted rendering
group usage. """
@users('employee')
def test_render_inline_template_impersonate(self):
""" Test that the use of SUDO do not change the current user. """
partner = self.env['res.partner'].browse(self.render_object.ids)
src = '{{ user.name }} - {{ object.name }}'
expected = '%s - %s' % (self.env.user.name, partner.name)
result = self.env['mail.render.mixin'].sudo()._render_template_inline_template(
src, partner._name, partner.ids
)[partner.id]
self.assertIn(expected, result)
@users('user_rendering_restricted')
def test_render_inline_template_restricted(self):
"""Test if we correctly detect static template."""
res_ids = self.env['res.partner'].search([], limit=1).ids
with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'):
self.env['mail.render.mixin']._render_template_inline_template(
self.base_inline_template_bits[3],
'res.partner',
res_ids
)
src = """<h1>This is a static template</h1>"""
result = self.env['mail.render.mixin']._render_template_inline_template(
src,
'res.partner',
res_ids
)[res_ids[0]]
self.assertEqual(src, str(result))
@users('user_rendering_restricted')
def test_render_inline_template_restricted_static(self):
"""Test that we render correctly static templates (without placeholders)."""
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
MailRenderMixin = self.env['mail.render.mixin']
result = MailRenderMixin._render_template_inline_template(
self.base_inline_template_bits[0],
model,
res_ids
)[res_ids[0]]
self.assertEqual(result, self.base_inline_template_bits[0])
@users('employee')
def test_render_inline_template_unrestricted(self):
""" Test if we correctly detect static template. """
res_ids = self.env['res.partner'].search([], limit=1).ids
result = self.env['mail.render.mixin']._render_template_inline_template(
self.base_inline_template_bits[3],
'res.partner',
res_ids
)[res_ids[0]]
self.assertIn('26', result, 'Template Editor should be able to render inline_template code')
@users('user_rendering_restricted')
def test_render_template_qweb_restricted(self):
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
partner = self.env[model].browse(res_ids)
src = """<h1>This is a static template</h1>"""
result = self.env['mail.render.mixin']._render_template_qweb(src, model, res_ids)[
partner.id]
self.assertEqual(src, str(result))
@users('user_rendering_restricted')
def test_security_function_call(self):
"""Test the case when the template call a custom function.
This function should not be called when the template is not rendered.
"""
model = 'res.partner'
res_ids = self.env[model].search([], limit=1).ids
partner = self.env[model].browse(res_ids)
MailRenderMixin = self.env['mail.render.mixin']
def cust_function():
# Can not use "MagicMock" in a Jinja sand-boxed environment
# so create our own function
cust_function.call = True
return 'return value'
cust_function.call = False
src = """<h1>This is a test</h1>
<p>{{ cust_function() }}</p>"""
expected = """<h1>This is a test</h1>
<p>return value</p>"""
context = {'cust_function': cust_function}
result = self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_inline_template(
src, partner._name, partner.ids,
add_context=context
)[partner.id]
self.assertEqual(expected, result)
self.assertTrue(cust_function.call)
with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'):
MailRenderMixin._render_template_inline_template(src, model, res_ids, add_context=context)
@users('user_rendering_restricted')
def test_security_inline_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 dynamic code'):
self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[4], 'res.partner', res_ids)
@users('employee')
def test_security_inline_template_unrestricted(self):
"""Test if we correctly detect condition block (which might contains code)."""
res_ids = self.env['res.partner'].search([], limit=1).ids
result = self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[4], 'res.partner', res_ids)[res_ids[0]]
self.assertNotIn('Code not executed', result, 'The condition block did not work')
@users('user_rendering_restricted')
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)
@users('user_rendering_restricted')
def test_security_qweb_template_restricted_cached(self):
"""Test if we correctly detect condition block (which might contains code)."""
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)
# 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'):
self.env['mail.render.mixin']._render_template_qweb(
self.base_qweb_bits[1], 'res.partner', res_ids)
@users('employee')
def test_security_qweb_template_unrestricted(self):
"""Test if we correctly detect condition block (which might contains code)."""
res_ids = self.env['res.partner'].search([], limit=1).ids
result = self.env['mail.render.mixin']._render_template_qweb(self.base_qweb_bits[1], 'res.partner', res_ids)[res_ids[0]]
self.assertNotIn('Code not executed', result, 'The condition block did not work')

View file

@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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
@tagged('mail_template')
class TestMailTemplate(MailCommon):
@classmethod
def setUpClass(cls):
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.mail_template = cls.env['mail.template'].create({
'name': 'Test template',
'subject': '{{ 1 + 5 }}',
'body_html': '<t t-out="4 + 9"/>',
'lang': '{{ object.lang }}',
'auto_delete': True,
'model_id': cls.env.ref('base.model_res_partner').id,
})
@users('employee')
def test_mail_compose_message_content_from_template(self):
form = Form(self.env['mail.compose.message'])
form.template_id = self.mail_template
mail_compose_message = form.save()
self.assertEqual(mail_compose_message.subject, '6', 'We must trust mail template values')
@users('employee')
def test_mail_compose_message_content_from_template_mass_mode(self):
mail_compose_message = self.env['mail.compose.message'].create({
'composition_mode': 'mass_mail',
'model': 'res.partner',
'template_id': self.mail_template.id,
'subject': '{{ 1 + 5 }}',
})
values = mail_compose_message.get_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')
def test_mail_template_acl(self):
# Sanity check
self.assertTrue(self.user_admin.has_group('mail.group_mail_template_editor'))
self.assertTrue(self.user_admin.has_group('base.group_sanitize_override'))
self.assertFalse(self.user_employee.has_group('mail.group_mail_template_editor'))
self.assertFalse(self.user_employee.has_group('base.group_sanitize_override'))
# Group System can create / write / unlink mail template
mail_template = self.env['mail.template'].with_user(self.user_admin).create({'name': 'Test template'})
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.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.with_user(self.user_employee).email_to = 'bar@foo.com'
# Standard employee cannot create and edit templates with dynamic qweb
with self.assertRaises(AccessError):
self.env['mail.template'].with_user(self.user_employee).create({'body_html': '<p t-esc="\'foo\'"></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>'
# Standard employee can edit his own templates if not dynamic
employee_template.with_user(self.user_employee).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 }}'})
# 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>'
with self.assertRaises(AccessError):
employee_template.with_user(self.user_employee).email_to = '{{ object.partner_id.email }}'
def test_mail_template_acl_translation(self):
''' Test that a user that doenn'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.
'''
self.env.ref('base.lang_fr').sudo().active = True
employee_template = self.env['mail.template'].with_user(self.user_employee).create({
'model_id': self.env.ref('base.model_res_partner').id,
'subject': 'The subject',
'body_html': '<p>foo</p>',
})
### check qweb dynamic
# write on translation for template without dynamic code is allowed
employee_template.with_context(lang='fr_FR').body_html = 'non-qweb'
# 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').body_html = '<t t-esc="foo"/>'
employee_template.with_context(lang='fr_FR').sudo().body_html = '<t t-esc="foo"/>'
# reset the body_html to static
employee_template.body_html = False
employee_template.body_html = '<p>foo</p>'
### check qweb inline dynamic
# write on translation for template without dynamic code is allowed
employee_template.with_context(lang='fr_FR').subject = 'non-qweb'
# 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').sudo().subject = '{{ object.foo }}'
def test_server_archived_usage_protection(self):
""" Test the protection against using archived server (servers used cannot be archived) """
IrMailServer = self.env['ir.mail_server']
server = IrMailServer.create({
'name': 'Server',
'smtp_host': 'archive-test.smtp.local',
})
self.mail_template.mail_server_id = server.id
with self.assertRaises(UserError, msg='Server cannot be archived because it is used'):
server.action_archive()
self.assertTrue(server.active)
self.mail_template.mail_server_id = IrMailServer
server.action_archive() # No more usage -> can be archived
self.assertFalse(server.active)
@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 test_mail_template_reset(self):
self._load('mail', 'tests', 'test_mail_template.xml')
mail_template = self.env.ref('mail.mail_template_test').with_context(lang=self.env.user.lang)
mail_template.write({
'body_html': '<div>Hello</div>',
'name': 'Mail: Mail Template',
'subject': 'Test',
'email_from': 'admin@example.com',
'email_to': 'user@example.com',
'attachment_ids': False,
})
context = {'default_template_ids': mail_template.ids}
mail_template_reset = self.env['mail.template.reset'].with_context(context).create({})
reset_action = mail_template_reset.reset_template()
self.assertTrue(reset_action)
self.assertEqual(mail_template.body_html.strip(), Markup('<div>Hello Odoo</div>'))
self.assertEqual(mail_template.name, 'Mail: Test Mail Template')
self.assertEqual(
mail_template.email_from,
'"{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}>'
)
self.assertEqual(mail_template.email_to, '{{ object.email_formatted }}')
self.assertEqual(mail_template.attachment_ids, self.env.ref('mail.mail_template_test_attachment'))
# subject is not there in the data file template, so it should be set to False
self.assertFalse(mail_template.subject, "Subject should be set to False")
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.env['res.lang']._activate_lang('en_UK')
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({
'body_html': '<div>Hello</div>',
'name': 'Mail: Mail Template',
})
mail_template.with_context(lang='en_UK').write({
'body_html': '<div>Hello UK</div>',
'name': 'Mail: Mail Template UK',
})
context = {'default_template_ids': mail_template.ids, 'lang': 'fr_FR'}
def fake_load_file(translation_importer, filepath, lang, xmlids=None):
""" a fake load file to mimic the use case when
translations for fr_FR exist in the fr.po of the directory and
no en.po in the directory
"""
if lang == 'fr_FR': # fr_FR has translations
translation_importer.model_translations['mail.template'] = {
'body_html': {'mail.mail_template_test': {'fr_FR': '<div>Hello Odoo FR</div>'}},
'name': {'mail.mail_template_test': {'fr_FR': "Mail: Test Mail Template FR"}},
}
with patch('odoo.tools.translate.TranslationImporter.load_file', fake_load_file):
mail_template_reset = self.env['mail.template.reset'].with_context(context).create({})
reset_action = mail_template_reset.reset_template()
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='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='fr_FR').name, 'Mail: Test Mail Template FR')
@tagged('-at_install', 'post_install')
class TestConfigRestrictEditor(MailCommon):
def test_switch_icp_value(self):
# Sanity check
self.assertTrue(self.user_employee.has_group('mail.group_mail_template_editor'))
self.assertFalse(self.user_employee.has_group('base.group_system'))
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'))

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mail_template_test_attachment" model="ir.attachment">
<field name="datas">bWlncmF0aW9uIHRlc3Q=</field>
<field name="name">YourCompany2022.doc</field>
</record>
<record id="mail_template_test" model="mail.template">
<field name="name">Mail: Test Mail Template</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="email_from">"{{ object.company_id.name }}" &lt;{{ (object.company_id.email or user.email) }}&gt;</field>
<field name="email_to">{{ object.email_formatted }}</field>
<field name="body_html" type="html">
<div>Hello Odoo</div>
</field>
<field name="lang">{{ object.lang }}</field>
<field name="attachment_ids" eval="[(6, 0, [ref('mail_template_test_attachment')])]"/>
</record>
</odoo>

View file

@ -0,0 +1,291 @@
# -*- 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 import tagged, users
from odoo import tools
@tagged('mail_tools')
class TestMailTools(MailCommon):
@classmethod
def setUpClass(cls):
super(TestMailTools, cls).setUpClass()
cls._test_email = 'alfredoastaire@test.example.com'
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):
Partner = self.env['res.partner']
test_partner = Partner.browse(self.test_partner.ids)
self.assertEqual(test_partner.email, self._test_email)
sources = [
self._test_email, # test direct match
f'"Norbert Poiluchette" <{self._test_email}>', # encapsulated
'fredoastaire@test.example.com', # partial email -> should not match !
]
expected_partners = [
test_partner,
test_partner,
self.env['res.partner'],
]
for source, expected_partner in zip(sources, expected_partners):
with self.subTest(source=source):
found = Partner._mail_find_partner_from_emails([source])
self.assertEqual(found, [expected_partner])
# 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
# ------------------------------------------------------------
test_partner.sudo().write({'email': f'"Alfred Mighty Power Astaire" <{self._test_email}>'})
sources = [
self._test_email, # test direct match
f'"Norbert Poiluchette" <{self._test_email}>', # encapsulated
]
expected_partners = [
test_partner,
test_partner,
]
for source, expected_partner in zip(sources, expected_partners):
with self.subTest(source=source):
found = Partner._mail_find_partner_from_emails([source])
self.assertEqual(found, [expected_partner])
# 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_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
and encapsulated email support. """
# create partner just for the follow mechanism
linked_record = self.env['res.partner'].sudo().create({'name': 'Record for followers'})
follower_partner = self.env['res.partner'].sudo().create({
'email': self._test_email,
'name': 'Duplicated, follower of record',
})
linked_record.message_subscribe(partner_ids=follower_partner.ids)
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):
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)
# formatted email
encapsulated_test_email = f'"Robert Astaire" <{self._test_email}>'
(follower_partner + test_partner).sudo().write({'email': encapsulated_test_email})
sources = [
(self._test_email, True), # normalized
(self._test_email, False), # normalized
(encapsulated_test_email, True), # encapsulated, same
(encapsulated_test_email, False), # encapsulated, same
(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):
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,
'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
]
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):
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,
'Mail (FIXME): partial recognition of multi email through email_normalize')
# test users with same email, priority given to current user
# --------------------------------------------------------------
self.user_employee.sudo().write({'email': '"Alfred Astaire" <%s>' % self.env.user.partner_id.email_normalized})
found = self.env['res.partner']._mail_find_partner_from_emails([self.env.user.partner_id.email_formatted])
self.assertEqual(found, [self.env.user.partner_id])
def test_mail_find_partner_from_emails_multicompany(self):
""" 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
test_partner_no_company = self.test_partner.copy({'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})
records = [
None,
*Partner.create([
{'name': 'Company 2 contact', 'company_id': self.company_2.id},
{'name': 'Company 3 contact', 'company_id': self.company_3.id},
{'name': 'No restrictions', 'company_id': False},
])
]
expected_partners = [
(test_partner_no_company, "W/out reference record, prefer non-specific partner."),
(test_partner_company_2, "Prefer same company as reference record."),
(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)
@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)
)
@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')], [],
]
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)
)
@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
[], [], [],
]
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)
)

View file

@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from uuid import uuid4
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
from odoo.tests.common import Form, users
from odoo.tests import tagged
# 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')
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')
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],
})
self.assertEqual(existing.name, SAMPLES[6][1])
self.assertEqual(existing.email, SAMPLES[6][0])
self.assertEqual(existing.email_normalized, SAMPLES[6][2])
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
)
self._check_find_or_create(
SAMPLES[8][0], SAMPLES[8][1], SAMPLES[8][2],
check_partner=new6, should_create=True
)
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):
""" Test 'find_or_create' tool used in mail, notably when linking emails
found in recipients to partners when sending emails using the mail
composer. """
partners = self.env['res.partner'].create([
{
'email': 'classic.format@test.example.com',
'name': 'Classic Format',
},
{
'email': '"FindMe Format" <find.me.format@test.example.com>',
'name': 'FindMe Format',
}, {
'email': 'find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>',
'name': 'FindMe Multi',
},
])
# check data used for finding / searching
self.assertEqual(
partners.mapped('email_formatted'),
['"Classic Format" <classic.format@test.example.com>',
'"FindMe Format" <find.me.format@test.example.com>',
'"FindMe Multi" <find.me.multi.1@test.example.com,find.me.multi.2@test.example.com>']
)
# when having multi emails, first found one is taken as normalized email
self.assertEqual(
partners.mapped('email_normalized'),
['classic.format@test.example.com', 'find.me.format@test.example.com',
'find.me.multi.1@test.example.com']
)
# classic find or create: use normalized email to compare records
for email in ('CLASSIC.FORMAT@TEST.EXAMPLE.COM', '"Another Name" <classic.format@test.example.com>'):
with self.subTest(email=email):
self.assertEqual(self.env['res.partner'].find_or_create(email), partners[0])
# find on encapsulated email: comparison of normalized should work
for email in ('FIND.ME.FORMAT@TEST.EXAMPLE.COM', '"Different Format" <find.me.format@test.example.com>'):
with self.subTest(email=email):
self.assertEqual(self.env['res.partner'].find_or_create(email), partners[1])
# multi-emails -> no normalized email -> fails each time, create new partner (FIXME)
for email_input, match_partner in [
('find.me.multi.1@test.example.com', partners[2]),
('find.me.multi.2@test.example.com', self.env['res.partner']),
]:
with self.subTest(email_input=email_input):
partner = self.env['res.partner'].find_or_create(email_input)
# either matching existing, either new partner
if match_partner:
self.assertEqual(partner, match_partner)
else:
self.assertNotIn(partner, partners)
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'
# 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',
partners[0], 'classic.format@test.example.com'), # first found email matches existing
('another.email@test.example.com,classic.format@test.example.com',
self.env['res.partner'], 'another.email@test.example.com'), # first found email does not match
('find.me.multi.1@test.example.com,find.me.multi.2@test.example.com',
self.env['res.partner'], 'find.me.multi.1@test.example.com'),
]:
with self.subTest(email_input=email_input):
partner = self.env['res.partner'].find_or_create(email_input)
# either matching existing, either new partner
if match_partner:
self.assertEqual(partner, match_partner)
else:
self.assertNotIn(partner, partners)
self.assertEqual(partner.email, exp_email_partner)
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")
def test_res_partner_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')
# check at update
new_user = Users.create({
'email': 'micheline@test.example.com',
'login': 'michmich',
'name': 'Micheline Employee',
})
self.assertEqual(len(new_user.message_ids), 1, 'Should contain Contact created log message')
new_msg = new_user.message_ids
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_msg = new_user.message_ids[0]
self.assertIn('Portal Access Granted', new_msg.body)
self.assertEqual(new_msg.subtype_id, subtype_note)
# check at create
new_user = Users.create({
'email': 'micheline.2@test.example.com',
'groups_id': [(4, group_portal.id)],
'login': 'michmich.2',
'name': 'Micheline Portal',
})
self.assertEqual(len(new_user.message_ids), 2, 'Should contain Contact created + Portal access log messages')
new_msg = new_user.message_ids[0]
self.assertIn('Portal Access Granted', new_msg.body)
self.assertEqual(new_msg.subtype_id, subtype_note)
@users('admin')
def test_res_partner_merge_wizards(self):
Partner = self.env['res.partner']
p1 = Partner.create({'name': 'Customer1', 'email': 'test1@test.example.com'})
p1_msg_ids_init = p1.message_ids
p2 = Partner.create({'name': 'Customer2', 'email': 'test2@test.example.com'})
p2_msg_ids_init = p2.message_ids
p3 = Partner.create({'name': 'Other (dup email)', 'email': 'test1@test.example.com'})
# 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_msg1 = p1.message_post(
body='<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)
self.assertEqual(p1.message_ids, p1_msg_ids_init + p1_msg1)
self.assertEqual(p2.activity_ids, self.env['mail.activity'])
self.assertEqual(p2.message_follower_ids.partner_id, self.partner_admin)
self.assertEqual(p2.message_ids, p2_msg_ids_init)
MergeForm = Form(self.env['base.partner.merge.automatic.wizard'].with_context(
active_model='res.partner',
active_ids=(p1 + p2).ids
))
self.assertEqual(MergeForm.partner_ids[:], p1 + p2)
self.assertEqual(MergeForm.dst_partner_id, p2)
merge_form = MergeForm.save()
merge_form.action_merge()
# check destination and removal
self.assertFalse(p1.exists())
self.assertTrue(p2.exists())
# check mail documents have been moved
self.assertEqual(p2.activity_ids, p1_act1)
# TDE note: currently not working as soon as there is a single partner duplicated -> should be improved
# self.assertEqual(p2.message_follower_ids.partner_id, self.partner_admin + p3)
all_msg = p2_msg_ids_init + p1_msg_ids_init + p1_msg1
self.assertEqual(len(p2.message_ids), len(all_msg) + 1, 'Should have original messages + a log')
self.assertTrue(all(msg in p2.message_ids for msg in all_msg))

View file

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from psycopg2 import IntegrityError
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
from odoo.tests import RecordCapturer, tagged
from odoo.tools import mute_logger
@tagged('-at_install', 'post_install', 'mail_tools', 'res_users')
class TestUser(MailCommon):
@mute_logger('odoo.sql_db')
def test_notification_type_constraint(self):
with self.assertRaises(IntegrityError, msg='Portal user can not receive notification in Odoo'):
mail_new_test_user(
self.env,
login='user_test_constraint_2',
name='Test User 2',
email='user_test_constraint_2@test.example.com',
notification_type='inbox',
groups='base.group_portal',
)
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:
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,
},
)
# order does not seem guaranteed
self.assertEqual(len(capture.records), 2, 'Should create one user / entry')
self.assertEqual(
sorted(capture.records.mapped('name')),
sorted(('poiluchette@test.example.com', 'Jean Poilvache'))
)
self.assertEqual(
sorted(capture.records.mapped('email')),
sorted(exp_emails)
)
@tagged('-at_install', 'post_install')
class TestUserTours(HttpCaseWithUserDemo):
def test_user_modify_own_profile(self):
"""" A user should be able to modify their own profile.
Even if that user does not have access rights to write on the res.users model. """
if 'hr.employee' in self.env and not self.user_demo.employee_id:
self.env['hr.employee'].create({
'name': 'Marc Demo',
'user_id': self.user_demo.id,
})
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")

View file

@ -0,0 +1,58 @@
# -*- 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"
)

View file

@ -0,0 +1,873 @@
# -*- 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())

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged, TransactionCase
@tagged('-at_install', 'post_install')
class TestMailUninstall(TransactionCase):
def test_unlink_model(self):
model = self.env['ir.model'].create({
'name': 'Test Model',
'model': 'x_test_model',
'state': 'manual',
'is_mail_thread': True,
})
activity_type = self.env['mail.activity.type'].create({
'name': 'Test Activity Type',
'res_model': model.model,
})
record = self.env[model.model].create({})
activity = self.env['mail.activity'].create({
'activity_type_id': activity_type.id,
'res_model_id': model.id,
'res_id': record.id,
})
model.unlink()
self.assertFalse(model.exists())
self.assertFalse(activity_type.exists())
self.assertFalse(activity.exists())

View file

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
class TestUpdateNotification(TransactionCase):
def test_user_count(self):
ping_msg = self.env['publisher_warranty.contract'].with_context(active_test=False)._get_message()
user_count = self.env['res.users'].search_count([('active', '=', True)])
self.assertEqual(ping_msg.get('nbr_users'), user_count, 'Update Notification: Users count is badly computed in ping message')
share_user_count = self.env['res.users'].search_count([('active', '=', True), ('share', '=', True)])
self.assertEqual(ping_msg.get('nbr_share_users'), share_user_count, 'Update Notification: Portal Users count is badly computed in ping message')

View file

@ -0,0 +1,8 @@
# -*- 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