19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -0,0 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_avatar_acl
from . import test_discuss_attachment_controller
from . import test_discuss_binary_controller
from . import test_discuss_channel_invite
from . import test_discuss_action
from . import test_discuss_channel
from . import test_discuss_channel_access
from . import test_discuss_channel_as_guest
from . import test_discuss_channel_member
from . import test_discuss_mail_presence
from . import test_discuss_mention_suggestions
from . import test_discuss_message_update_controller
from . import test_discuss_reaction_controller
from . import test_discuss_res_role
from . import test_discuss_sub_channels
from . import test_discuss_thread_controller
from . import test_guest
from . import test_message_controller
from . import test_guest_feature
from . import test_toggle_upload
from . import test_load_messages
from . import test_rtc
from . import test_ui

View file

@ -0,0 +1,204 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, Command
from odoo.tests import HttpCase
from odoo.tests.common import tagged
@tagged("post_install", "-at_install")
class TestAvatarAcl(HttpCase):
def get_avatar_url(self, record, add_token=False):
access_token = ""
if add_token:
access_token = f"&access_token={record._get_avatar_128_access_token()}"
return f"/web/image?field=avatar_128&id={record.id}&model={record._name}&unique={fields.Datetime.to_string(record.write_date)}{access_token}"
def test_partner_open_guest_avatar(self):
self.env["res.users"].create(
{
"email": "testuser@testuser.com",
"group_ids": [Command.set([self.ref("base.group_user")])],
"name": "Test User",
"login": "testuser",
"password": "testuser",
}
)
self.authenticate("testuser", "testuser")
guest = self.env["mail.guest"].create({"name": "Guest"})
res = self.url_open(url=self.get_avatar_url(guest))
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=Guest.svg")
def test_partner_open_guest_avatar_with_channel(self):
testuser = self.env["res.users"].create(
{
"email": "testuser@testuser.com",
"group_ids": [Command.set([self.ref("base.group_user")])],
"name": "Test User",
"login": "testuser",
"password": "testuser",
}
)
self.authenticate("testuser", "testuser")
guest = self.env["mail.guest"].create({"name": "Guest"})
channel = self.env["discuss.channel"].create(
{
"group_public_id": None,
"name": "Test channel",
}
)
channel._add_members(guests=guest, users=testuser)
res = self.url_open(url=self.get_avatar_url(guest))
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=Guest.svg")
def test_guest_open_partner_avatar(self):
self.authenticate(None, None)
guest = self.env["mail.guest"].create({"name": "Guest"})
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
testuser = self.env["res.users"].create(
{
"email": "testuser@testuser.com",
"group_ids": [Command.set([self.ref("base.group_user")])],
"name": "Test User",
"login": "testuser",
"password": "testuser",
}
)
partner = self.env["res.users"].browse(testuser.id).partner_id
res = self.url_open(url=self.get_avatar_url(partner))
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
def test_guest_open_partner_avatar_with_channel(self):
self.authenticate(None, None)
guest = self.env["mail.guest"].create({"name": "Guest"})
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
testuser = self.env["res.users"].create(
{
"email": "testuser@testuser.com",
"group_ids": [Command.set([self.ref("base.group_user")])],
"name": "Test User",
"login": "testuser",
"password": "testuser",
}
)
channel = self.env["discuss.channel"].create(
{
"group_public_id": None,
"name": "Test channel",
}
)
channel._add_members(guests=guest, users=testuser)
res = self.url_open(url=self.get_avatar_url(testuser.partner_id))
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
res = self.url_open(url=self.get_avatar_url(testuser.partner_id, add_token=True))
self.assertEqual(res.headers["Content-Disposition"], f'inline; filename="{testuser.partner_id.name}.svg"')
def test_partner_open_partner_avatar(self):
testuser = self.env["res.users"].create(
{
"email": "testuser@testuser.com",
"group_ids": [Command.set([self.ref("base.group_user")])],
"name": "Test User",
"login": "testuser",
"password": "testuser",
}
)
self.authenticate("testuser", "testuser")
testuser2 = self.env["res.users"].create(
{
"email": "testuser2@testuser.com",
"group_ids": [Command.set([self.ref("base.group_user")])],
"name": "Test User 2",
"login": "testuser 2",
"password": "testuser 2",
}
)
channel = self.env["discuss.channel"].create(
{
"group_public_id": None,
"name": "Test channel",
}
)
channel._add_members(users=testuser | testuser2)
res = self.url_open(url=self.get_avatar_url(testuser2.partner_id))
self.assertEqual(res.headers["Content-Disposition"], f'inline; filename="{testuser2.partner_id.name}.svg"')
def test_guest_open_guest_avatar(self):
self.authenticate(None, None)
guest = self.env["mail.guest"].create({"name": "Guest"})
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
guest2 = self.env["mail.guest"].create({"name": "Guest 2"})
res = self.url_open(url=self.get_avatar_url(guest2))
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
def test_guest_open_guest_avatar_with_channel(self):
self.authenticate(None, None)
guest = self.env["mail.guest"].create({"name": "Guest"})
self.opener.cookies[guest._cookie_name] = guest._format_auth_cookie()
guest2 = self.env["mail.guest"].create({"name": "Guest 2"})
channel = self.env["discuss.channel"].create(
{
"group_public_id": None,
"name": "Test channel",
}
)
channel._add_members(guests=guest | guest2)
res = self.url_open(url=self.get_avatar_url(guest2))
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
res = self.url_open(url=self.get_avatar_url(guest2, add_token=True))
self.assertEqual(res.headers["Content-Disposition"], 'inline; filename="Guest 2.svg"')
def test_portal_open_partner_avatar(self):
self.env["res.users"].create(
{
"email": "testuser@testuser.com",
"group_ids": [Command.set([self.ref("base.group_portal")])],
"name": "Test User",
"login": "testuser",
"password": "testuser",
}
)
self.authenticate("testuser", "testuser")
testuser2 = self.env["res.users"].create(
{
"email": "testuser2@testuser.com",
"group_ids": [Command.set([self.ref("base.group_user")])],
"name": "Test User 2",
"login": "testuser 2",
"password": "testuser 2",
}
)
partner2 = self.env["res.users"].browse(testuser2.id).partner_id
res = self.url_open(url=self.get_avatar_url(partner2))
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
def test_portal_open_partner_avatar_with_channel(self):
testuser = self.env["res.users"].create(
{
"email": "testuser@testuser.com",
"group_ids": [Command.set([self.ref("base.group_portal")])],
"name": "Test User",
"login": "testuser",
"password": "testuser",
}
)
self.authenticate("testuser", "testuser")
testuser2 = self.env["res.users"].create(
{
"email": "testuser2@testuser.com",
"group_ids": [Command.set([self.ref("base.group_user")])],
"name": "Test User 2",
"login": "testuser 2",
"password": "testuser 2",
}
)
channel = self.env["discuss.channel"].create(
{
"group_public_id": None,
"name": "Test channel",
}
)
channel._add_members(users=testuser | testuser2)
res = self.url_open(url=self.get_avatar_url(testuser2.partner_id))
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
res = self.url_open(url=self.get_avatar_url(testuser2.partner_id, add_token=True))
self.assertEqual(res.headers["Content-Disposition"], f'inline; filename="{testuser2.partner_id.name}.svg"')

View file

@ -1,99 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
try:
import websocket as ws
except ImportError:
ws = None
from odoo.tests import tagged, new_test_user
from odoo.addons.bus.tests.common import WebsocketCase
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.bus.models.bus import channel_with_db, json_dump
@tagged("post_install", "-at_install")
class TestBusPresence(WebsocketCase, MailCommon):
def _receive_presence(self, sender, recipient):
self._reset_bus()
sent_from_user = isinstance(sender, self.env.registry["res.users"])
receive_to_user = isinstance(recipient, self.env.registry["res.users"])
if receive_to_user:
session = self.authenticate(recipient.login, recipient.login)
auth_cookie = f"session_id={session.sid};"
else:
self.authenticate(None, None)
auth_cookie = f"{recipient._cookie_name}={recipient._format_auth_cookie()};"
websocket = self.websocket_connect(cookie=auth_cookie)
sender_bus_target = sender.partner_id if sent_from_user else sender
self.subscribe(
websocket,
[f"odoo-presence-{sender_bus_target._name}_{sender_bus_target.id}"],
self.env["bus.bus"]._bus_last_id(),
)
self.env["bus.presence"].create(
{"user_id" if sent_from_user else "guest_id": sender.id, "status": "online"}
)
self.trigger_notification_dispatching([(sender_bus_target, "presence")])
notifications = json.loads(websocket.recv())
self._close_websockets()
bus_record = self.env["bus.bus"].search([("id", "=", int(notifications[0]["id"]))])
self.assertEqual(
bus_record.channel,
json_dump(channel_with_db(self.env.cr.dbname, (sender_bus_target, "presence"))),
)
self.assertEqual(notifications[0]["message"]["type"], "bus.bus/im_status_updated")
self.assertEqual(notifications[0]["message"]["payload"]["im_status"], "online")
self.assertEqual(notifications[0]["message"]["payload"]["presence_status"], "online")
self.assertEqual(
notifications[0]["message"]["payload"]["partner_id" if sent_from_user else "guest_id"],
sender_bus_target.id,
)
def test_receive_presences_as_guest(self):
guest = self.env["mail.guest"].create({"name": "Guest"})
bob = new_test_user(self.env, login="bob_user", groups="base.group_user")
# Guest should not receive users's presence: no common channel.
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
self._receive_presence(sender=bob, recipient=guest)
channel = self.env["discuss.channel"].channel_create(group_id=None, name="General")
channel.add_members(guest_ids=[guest.id], partner_ids=[bob.partner_id.id])
# Now that they share a channel, guest should receive users's presence.
self._receive_presence(sender=bob, recipient=guest)
other_guest = self.env["mail.guest"].create({"name": "OtherGuest"})
# Guest should not receive guest's presence: no common channel.
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
self._receive_presence(sender=other_guest, recipient=guest)
channel.add_members(guest_ids=[other_guest.id])
# Now that they share a channel, guest should receive guest's presence.
self._receive_presence(sender=other_guest, recipient=guest)
def test_receive_presences_as_portal(self):
portal = new_test_user(self.env, login="portal_user", groups="base.group_portal")
bob = new_test_user(self.env, login="bob_user", groups="base.group_user")
# Portal should not receive users's presence: no common channel.
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
self._receive_presence(sender=bob, recipient=portal)
channel = self.env["discuss.channel"].channel_create(group_id=None, name="General")
channel.add_members(partner_ids=[portal.partner_id.id, bob.partner_id.id])
# Now that they share a channel, portal should receive users's presence.
self._receive_presence(sender=bob, recipient=portal)
guest = self.env["mail.guest"].create({"name": "Guest"})
# Portal should not receive guest's presence: no common channel.
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
self._receive_presence(sender=guest, recipient=portal)
channel.add_members(guest_ids=[guest.id])
# Now that they share a channel, portal should receive guest's presence.
self._receive_presence(sender=guest, recipient=portal)
def test_receive_presences_as_internal(self):
internal = new_test_user(self.env, login="internal_user", groups="base.group_user")
guest = self.env["mail.guest"].create({"name": "Guest"})
# Internal can access guest's presence regardless of their channels.
self._receive_presence(sender=guest, recipient=internal)
# Internal can access users's presence regardless of their channels.
bob = new_test_user(self.env, login="bob_user", groups="base.group_user")
self._receive_presence(sender=bob, recipient=internal)

View file

@ -0,0 +1,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import HttpCase, tagged
from odoo.addons.mail.tests.common import MailCommon
@tagged("post_install", "-at_install", "discuss_action")
class TestDiscussAction(HttpCase, MailCommon):
def test_go_back_to_thread_from_breadcrumbs(self):
self.start_tour(
"/odoo/discuss?active_id=mail.box_inbox",
"discuss_go_back_to_thread_from_breadcrumbs.js",
login="admin",
)
def test_join_call_with_client_action(self):
inviting_user = self.env['res.users'].sudo().create({'name': "Inviting User", 'login': 'inviting'})
invited_user = self.env['res.users'].sudo().create({'name': "Invited User", 'login': 'invited'})
channel = self.env['discuss.channel'].with_user(inviting_user)._get_or_create_chat(partners_to=invited_user.partner_id.ids)
channel_member = channel.sudo().channel_member_ids.filtered(
lambda channel_member: channel_member.partner_id == inviting_user.partner_id)
self._reset_bus()
channel_member._rtc_join_call()
self.start_tour(
f"/odoo/{channel.id}/action-mail.action_discuss?call=accept",
"discuss_channel_call_action.js",
login=invited_user.login,
)

View file

@ -0,0 +1,87 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
from odoo.addons.mail.tests.common_controllers import MailControllerAttachmentCommon
from odoo.tools.misc import file_open
@odoo.tests.tagged("-at_install", "post_install", "mail_controller")
class TestDiscussAttachmentController(MailControllerAttachmentCommon):
def test_attachment_allowed_upload_public_channel(self):
"""Test access to upload an attachment on an allowed upload public channel"""
channel = self.env["discuss.channel"].create(
{"group_public_id": None, "name": "public channel"}
)
channel._add_members(guests=self.guest)
channel = channel.with_context(guest=self.guest)
self._execute_subtests_upload(
channel,
(
(self.guest, True),
(self.user_admin, True),
(self.user_employee, True),
(self.user_portal, True),
(self.user_public, True),
),
)
def test_attachment_delete_linked_to_public_channel(self):
"""Test access to delete an attachment associated with a public channel
whether or not limited `ownership_token` is sent"""
channel = self.env["discuss.channel"].create(
{"group_public_id": None, "name": "public channel"}
)
self._execute_subtests_delete(self.all_users, token=True, allowed=True, thread=channel)
self._execute_subtests_delete(
(self.user_admin, self.user_employee),
token=False,
allowed=True,
thread=channel,
)
self._execute_subtests_delete(
(self.guest, self.user_portal, self.user_public),
token=False,
allowed=False,
thread=channel,
)
def test_attachment_delete_linked_to_private_channel(self):
"""Test access to delete an attachment associated with a private channel
whether or not limited `ownership_token` is sent"""
channel = self.env["discuss.channel"].create(
{"name": "Private Channel", "channel_type": "group"}
)
self._execute_subtests_delete(self.all_users, token=True, allowed=True, thread=channel)
self._execute_subtests_delete(self.user_admin, token=False, allowed=True, thread=channel)
self._execute_subtests_delete(
(self.guest, self.user_employee, self.user_portal, self.user_public),
token=False,
allowed=False,
thread=channel,
)
def test_first_page_access_of_mail_attachment_pdf(self):
"""Test accessing the first page of a PDF that is encrypted(test_AES.pdf) or has invalid encoding(test_unicode.pdf)."""
attachments = []
for pdf in (
'mail/tests/discuss/files/test_AES.pdf',
'mail/tests/discuss/files/test_unicode.pdf',
):
with file_open(pdf, "rb") as file:
attachments.append({
'name': pdf,
'raw': file.read(),
'mimetype': 'application/pdf',
})
attachments = self.env['ir.attachment'].create(attachments)
self.authenticate("admin", "admin")
for attachment in attachments:
ownership_token = attachment._get_ownership_token()
url = f'/mail/attachment/pdf_first_page/{attachment.id}?access_token={ownership_token}'
response = self.url_open(url)
# Depending on the environment, the response status_code may vary:
# - 200 if PyPDF2 and PyCryptodome are installed (PDF successfully parsed)
# - 415 if those libs are missing (PDF cannot be processed)
self.assertIn(response.status_code, [415, 200])

View file

@ -0,0 +1,390 @@
from odoo.addons.mail.tests.common_controllers import MailControllerBinaryCommon
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestDiscussBinaryController(MailControllerBinaryCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.private_channel = cls.env["discuss.channel"].create(
{"name": "Private Channel", "channel_type": "group"}
)
cls.public_channel = cls.env["discuss.channel"]._create_channel(
name="Public Channel", group_id=None
)
cls.users = (
cls.user_public + cls.user_portal + cls.user_employee + cls.user_admin
)
def test_open_guest_avatar(self):
"""Test access to open the avatar of a guest.
There is no common channel or any interaction from the guest."""
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_01_guest_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: group
- target joins the channel: True
- other users join the channel: True
- target sends a message: False"""
self.private_channel._add_members(users=self.users, guests=self.guest | self.guest_2)
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_01_partner_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: partner
- channel type: group
- target joins the channel: True
- other users join the channel: True
- target sends a message: False"""
self.private_channel._add_members(
users=self.users | self.user_employee_nopartner, guests=self.guest
)
self._execute_subtests(
self.user_employee_nopartner.partner_id,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_02_guest_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: group
- target joins the channel: True
- other users join the channel: True
- target sends a message: True"""
self.private_channel._add_members(users=self.users, guests=self.guest | self.guest_2)
self._post_message(self.private_channel, self.guest_2)
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_02_partner_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: partner
- channel type: group
- target joins the channel: True
- other users join the channel: True
- target sends a message: True"""
self.private_channel._add_members(
users=self.users | self.user_employee_nopartner, guests=self.guest
)
self._post_message(self.private_channel, self.user_employee_nopartner)
self._execute_subtests(
self.user_employee_nopartner.partner_id,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_03_guest_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: group
- target joins the channel: True
- other users join the channel: True
- target sends a message: False
- target leaves the channel: True"""
self.private_channel._add_members(users=self.users, guests=self.guest | self.guest_2)
self.env["discuss.channel.member"].search(
[("guest_id", "=", self.guest_2.id), ("channel_id", "=", self.private_channel.id)]
).unlink()
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_03_partner_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: partner
- channel type: group
- target joins the channel: True
- other users join the channel: True
- target sends a message: False
- target leaves the channel: True"""
self.private_channel._add_members(
users=self.users | self.user_employee_nopartner, guests=self.guest
)
self.env["discuss.channel.member"].search(
[
("partner_id", "=", self.user_employee_nopartner.partner_id.id),
("channel_id", "=", self.private_channel.id),
]
).unlink()
self._execute_subtests(
self.user_employee_nopartner.partner_id,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_04_guest_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: group
- target joins the channel: True
- other users join the channel: True
- target sends a message: True
- target leaves the channel: True"""
self.private_channel._add_members(users=self.users, guests=self.guest | self.guest_2)
self._post_message(self.private_channel, self.guest_2)
self.env["discuss.channel.member"].search(
[("guest_id", "=", self.guest_2.id), ("channel_id", "=", self.private_channel.id)]
).unlink()
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_04_partner_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: partner
- channel type: group
- target joins the channel: True
- other users join the channel: True
- target sends a message: True
- target leaves the channel: True"""
self.private_channel._add_members(
users=self.users | self.user_employee_nopartner, guests=self.guest
)
self._post_message(self.private_channel, self.user_employee_nopartner)
self.env["discuss.channel.member"].search(
[
("partner_id", "=", self.user_employee_nopartner.partner_id.id),
("channel_id", "=", self.private_channel.id),
]
).unlink()
self._execute_subtests(
self.user_employee_nopartner.partner_id,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_05_guest_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: group
- target joins the channel: False
- other users join the channel: False
- target sends a message: True"""
self.private_channel.with_user(self.user_public).with_context(
guest=self.guest_2
).sudo().message_post(body="Test", subtype_xmlid="mail.mt_comment", message_type="comment")
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_05_partner_avatar_private_channel(self):
"""Test access to open the avatar:
- target type: partner
- channel type: group
- target joins the channel: False
- other users join the channel: False
- target sends a message: True"""
self.private_channel.message_post(
body="Test",
subtype_xmlid="mail.mt_comment",
message_type="comment",
author_id=self.user_employee_nopartner.partner_id.id,
)
self._execute_subtests(
self.user_employee_nopartner.partner_id,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_01_guest_avatar_public_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: public
- target joins the channel: False
- other users join the channel: False
- target sends a message: True"""
self.public_channel.with_user(self.user_public).with_context(
guest=self.guest_2
).sudo().message_post(body="Test", subtype_xmlid="mail.mt_comment", message_type="comment")
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_01_partner_avatar_public_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: public
- target joins the channel: False
- other users join the channel: False
- target sends a message: True"""
self._post_message(self.public_channel, self.user_employee_nopartner)
self._execute_subtests(
self.user_employee_nopartner.partner_id,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_02_guest_avatar_public_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: public
- target joins the channel: True
- other users join the channel: False
- target sends a message: False
- target leaves the channel: True"""
target_member = self.public_channel._add_members(guests=self.guest_2)
target_member.unlink()
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_02_partner_avatar_public_channel(self):
"""Test access to open the avatar:
- target type: partner
- channel type: public
- target joins the channel: True
- other users join the channel: False
- target sends a message: False
- target leaves the channel: True"""
target_member = self.public_channel._add_members(users=self.user_employee_nopartner)
target_member.unlink()
self._execute_subtests(
self.user_employee_nopartner.partner_id,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_03_guest_avatar_public_channel(self):
"""Test access to open the avatar:
- target type: guest
- channel type: public
- target joins the channel: True
- other users join the channel: False
- target sends a message: True
- target leaves the channel: True"""
target_member = self.public_channel._add_members(guests=self.guest_2)
self._post_message(self.public_channel, self.guest_2)
target_member.unlink()
self._execute_subtests(
self.guest_2,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_03_partner_avatar_public_channel(self):
"""Test access to open the avatar:
- target type: partner
- channel type: public
- target joins the channel: True
- other users join the channel: False
- target sends a message: True
- target leaves the channel: True"""
target_member = self.public_channel._add_members(users=self.user_employee_nopartner)
self._post_message(self.public_channel, self.user_employee_nopartner)
target_member.unlink()
self._execute_subtests(
self.user_employee_nopartner.partner_id,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, True),
(self.user_admin, True),
),
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,572 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from psycopg2.errors import UniqueViolation
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common import MailCommon
from odoo.exceptions import AccessError, UserError
from odoo.tests.common import tagged
from odoo.tools import mute_logger
@tagged("post_install", "-at_install")
class TestDiscussChannelAccess(MailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._channel_type_channel_access_cases = [
("public", "no_group", "member", "read", True),
("public", "no_group", "member", "write", False),
("public", "no_group", "member", "unlink", False),
("public", "no_group", "outside", "create", False),
("public", "no_group", "outside", "read", True),
("public", "no_group", "outside", "write", False),
("public", "no_group", "outside", "unlink", False),
("public", "group_matching", "member", "read", True),
("public", "group_matching", "member", "write", False),
("public", "group_matching", "member", "unlink", False),
("public", "group_matching", "outside", "create", False),
("public", "group_matching", "outside", "read", True),
("public", "group_matching", "outside", "write", False),
("public", "group_matching", "outside", "unlink", False),
("public", "group_failing", "member", "read", False),
("public", "group_failing", "member", "write", False),
("public", "group_failing", "member", "unlink", False),
("public", "group_failing", "outside", "create", False),
("public", "group_failing", "outside", "read", False),
("public", "group_failing", "outside", "write", False),
("public", "group_failing", "outside", "unlink", False),
("portal", "no_group", "member", "read", True),
("portal", "no_group", "member", "write", False),
("portal", "no_group", "member", "unlink", False),
("portal", "no_group", "outside", "create", False),
("portal", "no_group", "outside", "read", True),
("portal", "no_group", "outside", "write", False),
("portal", "no_group", "outside", "unlink", False),
("portal", "group_matching", "member", "read", True),
("portal", "group_matching", "member", "write", False),
("portal", "group_matching", "member", "unlink", False),
("portal", "group_matching", "outside", "create", False),
("portal", "group_matching", "outside", "read", True),
("portal", "group_matching", "outside", "write", False),
("portal", "group_matching", "outside", "unlink", False),
("portal", "group_failing", "member", "read", False),
("portal", "group_failing", "member", "write", False),
("portal", "group_failing", "member", "unlink", False),
("portal", "group_failing", "outside", "create", False),
("portal", "group_failing", "outside", "read", False),
("portal", "group_failing", "outside", "write", False),
("portal", "group_failing", "outside", "unlink", False),
("user", "no_group", "member", "read", True),
("user", "no_group", "member", "write", True),
("user", "no_group", "member", "unlink", False),
("user", "no_group", "outside", "create", True),
("user", "no_group", "outside", "read", True),
("user", "no_group", "outside", "write", True),
("user", "no_group", "outside", "unlink", False),
("user", "group_matching", "member", "read", True),
("user", "group_matching", "member", "write", True),
("user", "group_matching", "member", "unlink", False),
("user", "group_matching", "outside", "create", True),
("user", "group_matching", "outside", "read", True),
("user", "group_matching", "outside", "write", True),
("user", "group_matching", "outside", "unlink", False),
("user", "group_failing", "member", "read", False),
("user", "group_failing", "member", "write", False),
("user", "group_failing", "member", "unlink", False),
("user", "group_failing", "outside", "create", False),
("user", "group_failing", "outside", "read", False),
("user", "group_failing", "outside", "write", False),
("user", "group_failing", "outside", "unlink", False),
]
cls._channel_type_channel_member_access_cases = [
("public", "no_group", "member", "self", "create", False),
("public", "no_group", "member", "self", "read", True),
("public", "no_group", "member", "self", "write", True),
("public", "no_group", "member", "self", "unlink", True),
("public", "no_group", "member", "other", "create", False),
("public", "no_group", "member", "other", "read", True),
("public", "no_group", "member", "other", "write", False),
("public", "no_group", "member", "other", "unlink", False),
("public", "no_group", "outside", "self", "create", True),
("public", "no_group", "outside", "other", "create", False),
("public", "no_group", "outside", "other", "read", True),
("public", "no_group", "outside", "other", "write", False),
("public", "no_group", "outside", "other", "unlink", False),
("public", "group_matching", "member", "self", "create", False),
("public", "group_matching", "member", "self", "read", True),
("public", "group_matching", "member", "self", "write", True),
("public", "group_matching", "member", "self", "unlink", True),
("public", "group_matching", "member", "other", "create", False),
("public", "group_matching", "member", "other", "read", True),
("public", "group_matching", "member", "other", "write", False),
("public", "group_matching", "member", "other", "unlink", False),
("public", "group_matching", "outside", "self", "create", True),
("public", "group_matching", "outside", "other", "create", False),
("public", "group_matching", "outside", "other", "read", True),
("public", "group_matching", "outside", "other", "write", False),
("public", "group_matching", "outside", "other", "unlink", False),
("public", "group_failing", "member", "self", "create", False),
("public", "group_failing", "member", "self", "read", False),
("public", "group_failing", "member", "self", "write", False),
("public", "group_failing", "member", "self", "unlink", False),
("public", "group_failing", "member", "other", "create", False),
("public", "group_failing", "member", "other", "read", False),
("public", "group_failing", "member", "other", "write", False),
("public", "group_failing", "member", "other", "unlink", False),
("public", "group_failing", "outside", "self", "create", False),
("public", "group_failing", "outside", "other", "create", False),
("public", "group_failing", "outside", "other", "read", False),
("public", "group_failing", "outside", "other", "write", False),
("public", "group_failing", "outside", "other", "unlink", False),
("portal", "no_group", "member", "self", "create", False),
("portal", "no_group", "member", "self", "read", True),
("portal", "no_group", "member", "self", "write", True),
("portal", "no_group", "member", "self", "unlink", True),
("portal", "no_group", "member", "other", "create", False),
("portal", "no_group", "member", "other", "read", True),
("portal", "no_group", "member", "other", "write", False),
("portal", "no_group", "member", "other", "unlink", False),
("portal", "no_group", "outside", "self", "create", True),
("portal", "no_group", "outside", "other", "create", False),
("portal", "no_group", "outside", "other", "read", True),
("portal", "no_group", "outside", "other", "write", False),
("portal", "no_group", "outside", "other", "unlink", False),
("portal", "group_matching", "member", "self", "create", False),
("portal", "group_matching", "member", "self", "read", True),
("portal", "group_matching", "member", "self", "write", True),
("portal", "group_matching", "member", "self", "unlink", True),
("portal", "group_matching", "member", "other", "create", False),
("portal", "group_matching", "member", "other", "read", True),
("portal", "group_matching", "member", "other", "write", False),
("portal", "group_matching", "member", "other", "unlink", False),
("portal", "group_matching", "outside", "self", "create", True),
("portal", "group_matching", "outside", "other", "create", False),
("portal", "group_matching", "outside", "other", "read", True),
("portal", "group_matching", "outside", "other", "write", False),
("portal", "group_matching", "outside", "other", "unlink", False),
("portal", "group_failing", "member", "self", "create", False),
("portal", "group_failing", "member", "self", "read", False),
("portal", "group_failing", "member", "self", "write", False),
("portal", "group_failing", "member", "self", "unlink", False),
("portal", "group_failing", "member", "other", "create", False),
("portal", "group_failing", "member", "other", "read", False),
("portal", "group_failing", "member", "other", "write", False),
("portal", "group_failing", "member", "other", "unlink", False),
("portal", "group_failing", "outside", "self", "create", False),
("portal", "group_failing", "outside", "other", "create", False),
("portal", "group_failing", "outside", "other", "read", False),
("portal", "group_failing", "outside", "other", "write", False),
("portal", "group_failing", "outside", "other", "unlink", False),
("user", "no_group", "member", "self", "create", False),
("user", "no_group", "member", "self", "read", True),
("user", "no_group", "member", "self", "write", True),
("user", "no_group", "member", "self", "unlink", True),
("user", "no_group", "member", "other", "create", True),
("user", "no_group", "member", "other", "read", True),
("user", "no_group", "member", "other", "write", False),
("user", "no_group", "member", "other", "unlink", False),
("user", "no_group", "outside", "self", "create", True),
("user", "no_group", "outside", "other", "create", True),
("user", "no_group", "outside", "other", "read", True),
("user", "no_group", "outside", "other", "write", False),
("user", "no_group", "outside", "other", "unlink", False),
("user", "group_matching", "member", "self", "create", False),
("user", "group_matching", "member", "self", "read", True),
("user", "group_matching", "member", "self", "write", True),
("user", "group_matching", "member", "self", "unlink", True),
("user", "group_matching", "member", "other", "create", True),
("user", "group_matching", "member", "other", "read", True),
("user", "group_matching", "member", "other", "write", False),
("user", "group_matching", "member", "other", "unlink", False),
("user", "group_matching", "outside", "self", "create", True),
("user", "group_matching", "outside", "other", "create", True),
("user", "group_matching", "outside", "other", "read", True),
("user", "group_matching", "outside", "other", "write", False),
("user", "group_matching", "outside", "other", "unlink", False),
("user", "group_failing", "member", "self", "create", False),
("user", "group_failing", "member", "self", "read", False),
("user", "group_failing", "member", "self", "write", False),
("user", "group_failing", "member", "self", "unlink", False),
("user", "group_failing", "member", "other", "create", False),
("user", "group_failing", "member", "other", "read", False),
("user", "group_failing", "member", "other", "write", False),
("user", "group_failing", "member", "other", "unlink", False),
("user", "group_failing", "outside", "self", "create", False),
("user", "group_failing", "outside", "other", "create", False),
("user", "group_failing", "outside", "other", "read", False),
("user", "group_failing", "outside", "other", "write", False),
("user", "group_failing", "outside", "other", "unlink", False),
]
cls._group_type_channel_access_cases = [
("public", "group", "member", "read", True),
("public", "group", "member", "write", False),
("public", "group", "member", "unlink", False),
("public", "group", "outside", "create", False),
("public", "group", "outside", "read", False),
("public", "group", "outside", "write", False),
("public", "group", "outside", "unlink", False),
("portal", "group", "member", "read", True),
("portal", "group", "member", "write", False),
("portal", "group", "member", "unlink", False),
("portal", "group", "outside", "create", False),
("portal", "group", "outside", "read", False),
("portal", "group", "outside", "write", False),
("portal", "group", "outside", "unlink", False),
("user", "group", "member", "read", True),
("user", "group", "member", "write", True),
("user", "group", "member", "unlink", False),
("user", "group", "outside", "create", True),
("user", "group", "outside", "read", False),
("user", "group", "outside", "write", False),
("user", "group", "outside", "unlink", False),
]
cls._group_type_channel_member_access_cases = [
("public", "group", "member", "self", "create", False),
("public", "group", "member", "self", "read", True),
("public", "group", "member", "self", "write", True),
("public", "group", "member", "self", "unlink", True),
("public", "group", "member", "other", "create", False),
("public", "group", "member", "other", "read", True),
("public", "group", "member", "other", "write", False),
("public", "group", "member", "other", "unlink", False),
("public", "group", "outside", "self", "create", False),
("public", "group", "outside", "other", "create", False),
("public", "group", "outside", "other", "read", False),
("public", "group", "outside", "other", "write", False),
("public", "group", "outside", "other", "unlink", False),
("portal", "group", "member", "self", "create", False),
("portal", "group", "member", "self", "read", True),
("portal", "group", "member", "self", "write", True),
("portal", "group", "member", "self", "unlink", True),
("portal", "group", "member", "other", "create", False),
("portal", "group", "member", "other", "read", True),
("portal", "group", "member", "other", "write", False),
("portal", "group", "member", "other", "unlink", False),
("portal", "group", "outside", "self", "create", False),
("portal", "group", "outside", "other", "create", False),
("portal", "group", "outside", "other", "read", False),
("portal", "group", "outside", "other", "write", False),
("portal", "group", "outside", "other", "unlink", False),
("user", "group", "member", "self", "create", False),
("user", "group", "member", "self", "read", True),
("user", "group", "member", "self", "write", True),
("user", "group", "member", "self", "unlink", True),
("user", "group", "member", "other", "create", True),
("user", "group", "member", "other", "read", True),
("user", "group", "member", "other", "write", False),
("user", "group", "member", "other", "unlink", False),
("user", "group", "outside", "self", "create", False),
("user", "group", "outside", "other", "create", False),
("user", "group", "outside", "other", "read", False),
("user", "group", "outside", "other", "write", False),
("user", "group", "outside", "other", "unlink", False),
]
cls.secret_group = cls.env["res.groups"].create({"name": "Secret User Group"})
cls.env["ir.model.data"].create(
{
"name": "secret_group",
"module": "mail",
"model": cls.secret_group._name,
"res_id": cls.secret_group.id,
}
)
cls.guest = cls.env["mail.guest"].create({"name": "A Guest"}).sudo(False)
cls.users = {
"public": mail_new_test_user(
cls.env,
login="public1",
name="A Public User",
groups="base.group_public,mail.secret_group",
),
"portal": mail_new_test_user(
cls.env,
login="portal1",
name="A Portal User",
groups="base.group_portal,mail.secret_group",
),
"user": mail_new_test_user(
cls.env,
login="user1",
name="An Internal User",
groups="base.group_user,mail.secret_group",
),
}
cls.other_user = mail_new_test_user(
cls.env,
login="other1",
name="Another User 1",
groups="base.group_user,mail.secret_group",
)
cls.other_user_2 = mail_new_test_user(
cls.env,
login="other2",
name="Another User 2",
groups="base.group_user,mail.secret_group",
)
def _test_discuss_channel_access(self, cases, for_sub_channel):
"""
Executes a list of operations on channels in various setups and checks whether the outcomes
match the expected results.
:param cases: A list of test cases, where each tuple contains:
- user_key (``"portal"`` | ``"public"`` | ``"user"``): The user performing the operation.
- group_key (``"chat"`` | ``"group"`` | ``"no_group"`` | ``"group_matching"`` |
``"group_failing"``): The group specification to use. ``chat`` and ``group`` define the
channel type, while the others configure group setups for the channels.
- membership (``"member"`` | ``"outside"``): Whether the user is a member of the channel.
- operation (``"create"`` | ``"read"`` | ``"write"`` | ``"unlink"``): The action being tested.
- expected_result (bool): Whether the action is expected to be allowed (``True``) or denied
(``False``).
:type cases: List[Tuple[str, str, str, str, bool]]
:param for_sub_channel: Whether the operation is being tested on a sub-channel. In this case, the
``cases`` parameter is used to configure the parent channel.
"""
for user_key, channel_key, membership, operation, result in cases:
if result:
try:
self._execute_action_channel(
user_key, channel_key, membership, operation, result, for_sub_channel
)
except Exception as e: # noqa: BLE001 - re-raising, just with a more contextual message
raise AssertionError(
f"{user_key, channel_key, membership, operation} should not raise"
) from e
else:
try:
with self.assertRaises(AccessError), mute_logger("odoo.sql_db"), mute_logger(
"odoo.addons.base.models.ir_model"
), mute_logger("odoo.addons.base.models.ir_rule"), mute_logger(
"odoo.models.unlink"
):
self._execute_action_channel(
user_key, channel_key, membership, operation, result, for_sub_channel
)
except AssertionError as e:
raise AssertionError(
f"{user_key, channel_key, membership, operation} should raise"
) from e
def test_01_discuss_channel_access(self):
cases = [
*self._channel_type_channel_access_cases,
*self._group_type_channel_access_cases,
("public", "chat", "outside", "create", False),
("public", "chat", "outside", "read", False),
("public", "chat", "outside", "write", False),
("public", "chat", "outside", "unlink", False),
("portal", "chat", "member", "read", True),
("portal", "chat", "member", "write", False),
("portal", "chat", "member", "unlink", False),
("portal", "chat", "outside", "create", False),
("portal", "chat", "outside", "read", False),
("portal", "chat", "outside", "write", False),
("portal", "chat", "outside", "unlink", False),
("user", "chat", "member", "read", True),
("user", "chat", "member", "write", True),
("user", "chat", "member", "unlink", False),
("user", "chat", "outside", "create", True),
("user", "chat", "outside", "read", False),
("user", "chat", "outside", "write", False),
("user", "chat", "outside", "unlink", False),
]
self._test_discuss_channel_access(cases, for_sub_channel=False)
def test_02_discuss_sub_channel_access(self):
cases = [
*self._channel_type_channel_access_cases,
*self._group_type_channel_access_cases,
]
self._test_discuss_channel_access(cases, for_sub_channel=True)
def _test_discuss_channel_member_access(self, cases, for_sub_channel):
"""
Executes a list of operations on channel members in various setups and checks whether the
outcomes match the expected results.
:param cases: A list of test cases, where each tuple contains:
- user_key (``"portal"`` | ``"public"`` | ``"user"``):
The user performing the operation.
- group_key (``"chat"`` | ``"group"`` | ``"no_group"`` | ``"group_matching"`` |
``"group_failing"``):
The group specification to use. ``chat`` and ``group`` define the channel type, while the
others configure group setups for the channels.
- membership (``"member"`` | ``"outside"``):
Whether the user is a member of the channel.
- target (``"self"`` | ``"other"``):
Whether the operation is executed on the self-member or another member.
- operation (``"create"`` | ``"read"`` | ``"write"`` | ``"unlink"``):
The action being tested.
- expected_result (bool):
Whether the action is expected to be allowed (``True``) or denied (``False``).
:type cases: List[Tuple[str, str, str, str, str, bool]]
:param for_sub_channel: Whether the operation is being tested on a sub-channel. In this case, the
``cases`` parameter is used to configure the parent channel's member.
"""
for user_key, channel_key, membership, target, operation, result in cases:
channel_id = self._get_channel_id(user_key, channel_key, membership, for_sub_channel)
if result:
try:
self._execute_action_member(channel_id, user_key, target, operation, result)
except Exception as e: # noqa: BLE001 - re-raising, just with a more contextual message
raise AssertionError(
f"{user_key, channel_key, membership, target, operation} should not raise"
) from e
else:
try:
with self.assertRaises(AccessError), mute_logger("odoo.sql_db"), mute_logger(
"odoo.addons.base.models.ir_model"
), mute_logger("odoo.addons.base.models.ir_rule"), mute_logger(
"odoo.models.unlink"
):
try:
self._execute_action_member(
channel_id, user_key, target, operation, result
)
except (UniqueViolation, UserError) as e:
raise AccessError("expected errors as access error") from e
except AssertionError as e:
raise AssertionError(
f"{user_key, channel_key, membership, target, operation} should raise access error"
) from e
def test_10_discuss_channel_member_access(self):
cases = [
*self._channel_type_channel_member_access_cases,
*self._group_type_channel_member_access_cases,
("public", "chat", "outside", "self", "create", False),
("public", "chat", "outside", "other", "create", False),
("public", "chat", "outside", "other", "read", False),
("public", "chat", "outside", "other", "write", False),
("public", "chat", "outside", "other", "unlink", False),
("portal", "chat", "member", "self", "create", False),
("portal", "chat", "member", "self", "read", True),
("portal", "chat", "member", "self", "write", True),
("portal", "chat", "member", "self", "unlink", True),
("portal", "chat", "member", "other", "create", False),
("portal", "chat", "member", "other", "read", True),
("portal", "chat", "member", "other", "write", False),
("portal", "chat", "member", "other", "unlink", False),
("portal", "chat", "outside", "self", "create", False),
("portal", "chat", "outside", "other", "create", False),
("portal", "chat", "outside", "other", "read", False),
("portal", "chat", "outside", "other", "write", False),
("portal", "chat", "outside", "other", "unlink", False),
("user", "chat", "member", "self", "create", False),
("user", "chat", "member", "self", "read", True),
("user", "chat", "member", "self", "write", True),
("user", "chat", "member", "self", "unlink", True),
("user", "chat", "member", "other", "create", False),
("user", "chat", "member", "other", "read", True),
("user", "chat", "member", "other", "write", False),
("user", "chat", "member", "other", "unlink", False),
("user", "chat", "outside", "self", "create", False),
("user", "chat", "outside", "other", "create", False),
("user", "chat", "outside", "other", "read", False),
("user", "chat", "outside", "other", "write", False),
("user", "chat", "outside", "other", "unlink", False),
]
self._test_discuss_channel_member_access(cases, for_sub_channel=False)
def test_11_discuss_sub_channel_member_access(self):
cases = [
*self._channel_type_channel_member_access_cases,
*self._group_type_channel_member_access_cases,
]
self._test_discuss_channel_member_access(cases, for_sub_channel=True)
def _get_channel_id(self, user_key, channel_key, membership, sub_channel):
user = self.env["res.users"] if user_key == "public" else self.users[user_key]
partner = user.partner_id
guest = self.guest if user_key == "public" else self.env["mail.guest"]
partners = self.other_user.partner_id
if membership == "member":
partners += partner
DiscussChannel = self.env["discuss.channel"].with_user(self.other_user)
if channel_key == "group":
channel = DiscussChannel._create_group(partners.ids)
if membership == "member":
channel._add_members(users=user, guests=guest)
elif channel_key == "chat":
channel = DiscussChannel._get_or_create_chat(partners.ids)
else:
channel = DiscussChannel._create_channel("Channel", group_id=None)
if membership == "member":
channel._add_members(users=user, guests=guest)
if channel_key == "no_group":
channel.group_public_id = None
elif channel_key == "group_matching":
channel.group_public_id = self.secret_group
elif channel_key == "group_failing":
channel.group_public_id = self.env.ref("base.group_system")
if sub_channel:
channel.sudo()._create_sub_channel()
channel = channel.sub_channel_ids[0]
if membership == "member":
channel.sudo()._add_members(users=user, guests=guest)
return channel.id
def _execute_action_channel(self, user_key, channel_key, membership, operation, result, for_sub_channel):
current_user = self.users[user_key]
guest = self.guest if user_key == "public" else self.env["mail.guest"]
ChannelAsUser = self.env["discuss.channel"].with_user(current_user).with_context(guest=guest)
if operation == "create":
group_public_id = None
if channel_key == "group_matching":
group_public_id = self.secret_group.id
elif channel_key == "group_failing":
group_public_id = self.env.ref("base.group_system").id
data = {
"name": "Test Channel",
"channel_type": channel_key if channel_key in ("group", "chat") else "channel",
"group_public_id": group_public_id,
}
ChannelAsUser.create(data)
else:
channel = ChannelAsUser.browse(
self._get_channel_id(user_key, channel_key, membership, for_sub_channel)
)
self.assertEqual(len(channel), 1, "should find the channel")
if operation == "read":
self.assertEqual(len(ChannelAsUser.search([("id", "=", channel.id)])), 1 if result else 0)
channel.read(["name"])
elif operation == "write":
channel.write({"name": "new name"})
elif operation == "unlink":
channel.unlink()
def _execute_action_member(self, channel_id, user_key, target, operation, result):
current_user = self.users[user_key]
partner = self.env["res.partner"] if user_key == "public" else current_user.partner_id
guest = self.guest if user_key == "public" else self.env["mail.guest"]
ChannelMemberAsUser = self.env["discuss.channel.member"].with_user(current_user).with_context(guest=guest)
if operation == "create":
create_data = {"channel_id": channel_id}
if target == "self":
if guest:
create_data["guest_id"] = guest.id
else:
create_data["partner_id"] = partner.id
else:
create_data["partner_id"] = self.other_user_2.partner_id.id
ChannelMemberAsUser.create(create_data)
else:
domain = [("channel_id", "=", channel_id)]
if target == "self":
if guest:
domain.append(("guest_id", "=", guest.id))
else:
domain.append(("partner_id", "=", partner.id))
else:
domain.append(("partner_id", "=", self.other_user.partner_id.id))
member = ChannelMemberAsUser.sudo().search(domain).sudo(False)
self.assertEqual(len(member), 1, "should find the target member")
if operation == "read":
self.assertEqual(len(ChannelMemberAsUser.search(domain)), 1 if result else 0)
member.read(["custom_channel_name"])
elif operation == "write":
member.write({"custom_channel_name": "new name"})
elif operation == "unlink":
member.unlink()

View file

@ -0,0 +1,112 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.tests.common import tagged
from odoo.addons.base.tests.common import HttpCaseWithUserPortal, HttpCaseWithUserDemo
@tagged("post_install", "-at_install", "is_tour")
class TestMailPublicPage(HttpCaseWithUserPortal, HttpCaseWithUserDemo):
"""Checks that the invite page redirects to the channel and that all
modules load correctly on the welcome and channel page when authenticated as various users"""
def setUp(self):
super().setUp()
portal_user = mail_new_test_user(
self.env,
name='Portal Bowser',
login='portal_bowser',
email='portal_bowser@example.com',
groups='base.group_portal',
)
internal_user = mail_new_test_user(
self.env,
name='Internal Luigi',
login='internal_luigi',
email='internal_luigi@example.com',
groups='base.group_user',
)
guest = self.env['mail.guest'].create({'name': 'Guest Mario'})
self.channel = self.env['discuss.channel']._create_channel(group_id=None, name='Test channel')
self.channel._add_members(users=portal_user)
self.channel._add_members(users=internal_user)
self.channel._add_members(guests=guest)
internal_member = self.channel.channel_member_ids.filtered(lambda m: internal_user.partner_id == m.partner_id)
internal_member._rtc_join_call()
self.group = self.env['discuss.channel']._create_group(partners_to=(internal_user + portal_user).partner_id.ids, name="Test group")
self.group._add_members(guests=guest)
self.tour = "discuss_channel_public_tour.js"
def _open_channel_page_as_user(self, login):
self.start_tour(self.channel.invitation_url, self.tour, login=login)
# Update the body to a unique value to ensure the second run does not confuse the 2 messages.
self.channel._get_last_messages().body = "a-very-unique-body-in-channel"
# Second run of the tour as the first call has side effects, like creating user settings or adding members to
# the channel, so we need to run it again to test different parts of the code.
self.start_tour(self.channel.invitation_url, self.tour, login=login)
def _open_group_page_as_user(self, login):
self.start_tour(self.group.invitation_url, self.tour, login=login)
# Update the body to a unique value to ensure the second run does not confuse the 2 messages.
self.channel._get_last_messages().body = "a-very-unique-body-in-group"
# Second run of the tour as the first call has side effects, like creating user settings or adding members to
# the channel, so we need to run it again to test different parts of the code.
self.start_tour(self.group.invitation_url, self.tour, login=login)
def test_discuss_channel_public_page_as_admin(self):
self._open_channel_page_as_user('admin')
def test_mail_group_public_page_as_admin(self):
self._open_group_page_as_user('admin')
def test_discuss_channel_public_page_as_guest(self):
self.start_tour(self.channel.invitation_url, "discuss_channel_as_guest_tour.js")
guest = self.env['mail.guest'].search([('channel_ids', 'in', self.channel.id)], limit=1, order='id desc')
self.start_tour(self.channel.invitation_url, self.tour, cookies={guest._cookie_name: guest._format_auth_cookie()})
def test_discuss_channel_public_page_call_public(self):
self.channel.default_display_mode = 'video_full_screen'
self.start_tour(self.channel.invitation_url, "discuss_channel_call_public_tour.js")
def test_mail_group_public_page_as_guest(self):
self.start_tour(self.group.invitation_url, "discuss_channel_as_guest_tour.js")
guest = self.env['mail.guest'].search([('channel_ids', 'in', self.channel.id)], limit=1, order='id desc')
self.start_tour(self.group.invitation_url, self.tour, cookies={guest._cookie_name: guest._format_auth_cookie()})
def test_discuss_channel_public_page_as_internal(self):
self._open_channel_page_as_user('demo')
def test_mail_group_public_page_as_internal(self):
self._open_group_page_as_user('demo')
def test_discuss_channel_public_page_as_portal(self):
self._open_channel_page_as_user('portal')
def test_mail_group_public_page_as_portal(self):
self._open_group_page_as_user('portal')
def test_chat_from_token_as_guest(self):
self.env['ir.config_parameter'].set_param('mail.chat_from_token', True)
self.url_open('/chat/xyz')
channel = self.env['discuss.channel'].search([('uuid', '=', 'xyz')])
self.assertEqual(len(channel), 1)
def test_channel_invitation_from_token(self):
public_channel = self.env["discuss.channel"]._create_channel(name="Public Channel", group_id=None)
internal_channel = self.env["discuss.channel"]._create_channel(name="Internal Channel", group_id=self.env.ref("base.group_user").id)
public_response = self.url_open(public_channel.invitation_url)
self.assertEqual(public_response.status_code, 200)
internal_response = self.url_open(internal_channel.invitation_url)
self.assertEqual(internal_response.status_code, 404)
def test_sidebar_in_public_page(self):
guest = self.env['mail.guest'].create({'name': 'Guest'})
channel_1 = self.env["discuss.channel"]._create_channel(name="Channel 1", group_id=None)
channel_2 = self.env["discuss.channel"]._create_channel(name="Channel 2", group_id=None)
channel_1._add_members(guests=guest)
channel_2._add_members(guests=guest)
self.start_tour(f"/discuss/channel/{channel_1.id}", "sidebar_in_public_page_tour", cookies={guest._cookie_name: guest._format_auth_cookie()})

View file

@ -0,0 +1,199 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import html
from itertools import product
from odoo.addons.mail.tests.common import MailCommon
from odoo.exceptions import UserError
from odoo.tests import HttpCase, new_test_user, tagged, users
from odoo.tools.misc import hash_sign
@tagged("-at_install", "post_install")
class TestDiscussChannelInvite(HttpCase, MailCommon):
def test_01_invite_by_email_flow(self):
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
john = new_test_user(self.env, "john", groups="base.group_user", email="john@test.com")
group_chat = (
self.env["discuss.channel"].with_user(bob)._create_group(partners_to=bob.partner_id.ids)
)
with self.mock_mail_gateway():
self.start_tour(
f"/odoo/discuss?active_id={group_chat.id}", "discuss.invite_by_email", login="bob"
)
self.assertIn(john.partner_id, group_chat.channel_member_ids.partner_id)
self.assertNoMail(self.env["res.partner"], "john@test.com")
self.assertMailMail(
self.env["res.partner"],
status=None,
email_to_all=["unknown_email@test.com"],
author=bob.partner_id,
email_values={
"subject": f"{bob.name} has invited you to a channel",
},
)
mail = self.env["mail.mail"].search(
[("model", "=", "discuss.channel"), ("res_id", "=", group_chat.id)]
)
body_html = html.fromstring(mail.body_html)
join_link = body_html.xpath('//a[normalize-space(text())="Join Channel"]')
self.assertTrue(join_link)
self.assertEqual(
join_link[0].get("href"),
f"{self.env['ir.config_parameter'].get_base_url()}{group_chat.invitation_url}?email_token={hash_sign(self.env, 'mail.invite_email', 'unknown_email@test.com')}",
)
def test_02_invite_by_email_excludes_member_emails(self):
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
group_chat = (
self.env["discuss.channel"].with_user(bob)._create_group(partners_to=bob.partner_id.ids)
)
alfred_guest = self.env["mail.guest"].create({"email": "alfred@test.com", "name": "Alfred"})
group_chat._add_members(guests=alfred_guest)
with self.mock_mail_gateway():
group_chat.invite_by_email(["alfred@test.com", "bob@test.com", "other@test.com"])
self.assertMailMail(
self.env["res.partner"],
status=None,
email_to_all=["other@test.com"],
author=bob.partner_id,
)
self.assertNoMail(self.env["res.partner"], "bob@test.com")
self.assertNoMail(self.env["res.partner"], "alfred@test.com")
def test_03_only_invite_by_email_on_allowed_channel_types(self):
bob = new_test_user(self.env, "bob", groups="base.group_user")
john = new_test_user(self.env, "john", groups="base.group_user")
chat = (
self.env["discuss.channel"]
.with_user(bob)
._get_or_create_chat(partners_to=john.partner_id.ids)
)
group_chat = (
self.env["discuss.channel"]
.with_user(bob)
._create_group(partners_to=john.partner_id.ids)
)
public_channel = self.env["discuss.channel"].create(
{"name": "public community", "group_public_id": False}
)
private_channel = self.env["discuss.channel"].create(
{
"name": "user restricted channel",
"channel_type": "channel",
"group_public_id": self.env.ref("base.group_user").id,
}
)
for channel in [chat, private_channel]:
with self.assertRaises(UserError) as exc:
channel.invite_by_email(["some@email.com"])
self.assertEqual(
exc.exception.args[0],
f"Inviting by email is not allowed for this channel type ({channel.channel_type}).",
)
with self.mock_mail_gateway():
for channel in [group_chat, public_channel]:
channel.invite_by_email(["some@email.com"])
self.assertMailMail(
self.env["res.partner"],
status=None,
email_to_all=["some@email.com"],
email_values={"model": "discuss.channel", "res_id": channel.id},
)
def test_04_guest_email_updated_when_invited_from_email(self):
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
group_chat = (
self.env["discuss.channel"].with_user(bob)._create_group(partners_to=bob.partner_id.ids)
)
# Guest email is filled at create
self.url_open(
f"{group_chat.invitation_url}?email_token={hash_sign(self.env, 'mail.invite_email', 'alfred@test.com')}"
)
self.assertEqual(group_chat.channel_member_ids.guest_id.email, "alfred@test.com")
self.assertEqual(group_chat.channel_member_ids.guest_id.name, "alfred@test.com")
# Guest email is updated if empty when invited from email
guest = self.env["mail.guest"].create({"name": "Alice"})
self.assertFalse(guest.email)
self.url_open(
f"{group_chat.invitation_url}?email_token={hash_sign(self.env, 'mail.invite_email', 'alice@test.com')}",
cookies={
guest._cookie_name: f"{guest.id}{guest._cookie_separator}{guest.access_token}",
},
)
self.assertEqual(guest.email, "alice@test.com")
self.assertEqual(guest.name, "Alice")
# Guest email is not overwriten if already filled
guest = self.env["mail.guest"].create({"name": "John", "email": "john@test.com"})
self.url_open(
f"{group_chat.invitation_url}?email_token={hash_sign(self.env, 'mail.invite_email', 'john_other_email@test.com')}",
cookies={
guest._cookie_name: f"{guest.id}{guest._cookie_separator}{guest.access_token}",
},
)
self.assertEqual(guest.email, "john@test.com")
self.assertEqual(guest.name, "John")
def test_05_search_for_channel_invite_selectable_email(self):
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
john = new_test_user(self.env, "john", groups="base.group_user", email="john@test.com")
alfred_guest = self.env["mail.guest"].create({"email": "alfred@test.com", "name": "Alfred"})
chat = (
self.env["discuss.channel"]
.with_user(bob)
._get_or_create_chat(partners_to=john.partner_id.ids)
)
group_chat = (
self.env["discuss.channel"]
.with_user(bob)
._create_group(partners_to=john.partner_id.ids)
)
group_chat._add_members(guests=alfred_guest)
public_channel = self.env["discuss.channel"].create(
{"name": "public community", "group_public_id": False},
)
public_channel._add_members(guests=alfred_guest)
private_channel = self.env["discuss.channel"].create(
{
"name": "user restricted channel",
"channel_type": "channel",
"group_public_id": self.env.ref("base.group_user").id,
},
)
cases = [
*product(
[chat, private_channel, group_chat, public_channel],
["foo@bar"],
[False],
),
# Channel types that do not allow inviting by email, not selectable.
*product(
[chat, private_channel],
["bob@odoo.com", "alfred@odoo.com", "jane@odoo.com"],
[False],
),
# Channel types that allow inviting by email, valid email, selectable.
*product(
[group_chat, public_channel],
["bob@odoo.com", "alfred@odoo.com", "jane@odoo.com"],
[True],
),
]
for channel, search_term, is_selectable in cases:
with self.subTest(
f"channel={channel.channel_type}_{channel.display_name}, search_term={search_term}, is_selectable={is_selectable}"
):
result = self.env["res.partner"].search_for_channel_invite(
search_term, channel_id=channel.id
)
if is_selectable:
self.assertEqual(result["selectable_email"], search_term)
continue
self.assertFalse(result["selectable_email"])
@users("employee")
def test_06_invite_by_email_posts_user_notification(self):
group_chat = self.env["discuss.channel"]._create_group(partners_to=self.user_employee.partner_id.ids)
with self.mock_mail_gateway():
group_chat.invite_by_email(["alfred@test.com"])
last_message = group_chat._get_last_messages()
self.assertEqual(last_message.message_type, "user_notification")

View file

@ -0,0 +1,274 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mail.tests.common import MailCommon
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests.common import new_test_user, tagged
@tagged("post_install", "-at_install")
class TestDiscussChannelMember(MailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.secret_group = cls.env['res.groups'].create({
'name': 'Secret User Group',
})
cls.env['ir.model.data'].create({
'name': 'secret_group',
'module': 'mail',
'model': cls.secret_group._name,
'res_id': cls.secret_group.id,
})
cls.user_1 = new_test_user(
cls.env, login="user_1", name="User 1", groups="base.group_user,mail.secret_group"
)
cls.user_2 = new_test_user(
cls.env, login="user_2", name="User 2", groups="base.group_user,mail.secret_group"
)
cls.user_3 = new_test_user(
cls.env, login="user_3", name="User 3", groups="base.group_user,mail.secret_group"
)
cls.user_portal = new_test_user(
cls.env, login="user_portal", name="User Portal", groups="base.group_portal"
)
cls.user_public = new_test_user(
cls.env, login="user_public", name="User Public", groups="base.group_public"
)
cls.group = cls.env['discuss.channel'].create({
'name': 'Group',
'channel_type': 'group',
})
cls.group_restricted_channel = cls.env['discuss.channel'].create({
'name': 'Group restricted channel',
'channel_type': 'channel',
'group_public_id': cls.secret_group.id,
})
cls.public_channel = cls.env['discuss.channel']._create_channel(group_id=None, name='Public channel of user 1')
(cls.group | cls.group_restricted_channel | cls.public_channel).channel_member_ids.unlink()
# ------------------------------------------------------------
# GROUP
# ------------------------------------------------------------
def test_group_01(self):
"""Test access on group."""
res = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
self.assertFalse(res)
# User 1 can join group with SUDO
self.group.with_user(self.user_1).sudo()._add_members(users=self.user_1)
res = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
self.assertEqual(res.partner_id, self.user_1.partner_id)
# User 2 can not join group
with self.assertRaises(AccessError):
self.group.with_user(self.user_2)._add_members(users=self.user_2)
# User 2 can not create a `discuss.channel.member` to join the group
with self.assertRaises(AccessError):
self.env['discuss.channel.member'].with_user(self.user_2).create({
'partner_id': self.user_2.partner_id.id,
'channel_id': self.group.id,
})
# User 2 can not write on `discuss.channel.member` to join the group
channel_member = self.env['discuss.channel.member'].with_user(self.user_2).search([('is_self', '=', True)])[0]
with self.assertRaises(AccessError):
channel_member.channel_id = self.group.id
with self.assertRaises(AccessError):
channel_member.write({'channel_id': self.group.id})
# Even with SUDO, channel_id of channel.member should not be changed.
with self.assertRaises(AccessError):
channel_member.sudo().channel_id = self.group.id
# User 2 can not write on the `partner_id` of `discuss.channel.member`
# of an other partner to join a group
channel_member_1 = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_1.partner_id.id)])
with self.assertRaises(AccessError):
channel_member_1.with_user(self.user_2).partner_id = self.user_2.partner_id
self.assertEqual(channel_member_1.partner_id, self.user_1.partner_id)
# Even with SUDO, partner_id of channel.member should not be changed.
with self.assertRaises(AccessError):
channel_member_1.with_user(self.user_2).sudo().partner_id = self.user_2.partner_id
def test_group_members(self):
"""Test invitation in group part 1 (invite using crud methods)."""
self.group.with_user(self.user_1).sudo()._add_members(users=self.user_1)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
self.assertEqual(len(channel_members), 1)
# User 2 is not in the group, they can not invite user 3
with self.assertRaises(AccessError):
self.env['discuss.channel.member'].with_user(self.user_2).create({
'partner_id': self.user_portal.partner_id.id,
'channel_id': self.group.id,
})
# User 1 is in the group, they can invite other users
self.env['discuss.channel.member'].with_user(self.user_1).create({
'partner_id': self.user_portal.partner_id.id,
'channel_id': self.group.id,
})
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
# But User 3 can not write on the `discuss.channel.member` of other user
channel_member_1 = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_1.partner_id.id)])
channel_member_3 = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id), ('partner_id', '=', self.user_portal.partner_id.id)])
channel_member_3.with_user(self.user_portal).custom_channel_name = 'Test'
with self.assertRaises(AccessError):
channel_member_1.with_user(self.user_2).custom_channel_name = 'Blabla'
self.assertNotEqual(channel_member_1.custom_channel_name, 'Blabla')
def test_group_invite(self):
"""Test invitation in group part 2 (use `invite` action)."""
self.group.with_user(self.user_1).sudo()._add_members(users=self.user_1)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
# User 2 is not in the group, they can not invite user_portal
with self.assertRaises(AccessError):
self.group.with_user(self.user_2)._add_members(users=self.user_portal)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
# User 1 is in the group, they can invite user_portal
self.group.with_user(self.user_1)._add_members(users=self.user_portal)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
def test_group_leave(self):
"""Test kick/leave channel."""
self.group.with_user(self.user_1).sudo()._add_members(users=self.user_1)
self.group.with_user(self.user_portal).sudo()._add_members(users=self.user_portal)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group.id)])
self.assertEqual(len(channel_members), 2)
# User 2 is not in the group, they can not kick user 1
with self.assertRaises(AccessError):
channel_members.with_user(self.user_2).unlink()
# User 3 is in the group, but not admin/owner, they can not kick user 1
with self.assertRaises(AccessError):
channel_members.with_user(self.user_portal).unlink()
def test_group_subchannel_join(self):
"""Test join subchannel."""
self.group.add_members((self.user_1 | self.user_2).partner_id.ids)
group_subchannel = self.group.with_user(self.user_1)._create_sub_channel()
group_subchannel.with_user(self.user_2).add_members(self.user_2.partner_id.id)
self.assertEqual(group_subchannel.channel_member_ids.partner_id, (self.user_1 | self.user_2).partner_id)
# ------------------------------------------------------------
# GROUP BASED CHANNELS
# ------------------------------------------------------------
def test_group_restricted_channel(self):
"""Test basics on group channel."""
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
self.assertFalse(channel_members)
# user 1 is in the channel, they can join the channel
self.group_restricted_channel.with_user(self.user_1)._add_members(users=self.user_1)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
# user 3 is not in the channel, they can not join
with self.assertRaises(AccessError):
self.group_restricted_channel.with_user(self.user_portal)._add_members(users=self.user_portal)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
with self.assertRaises(AccessError):
channel_members.with_user(self.user_portal).partner_id = self.user_portal.partner_id
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
self.group_restricted_channel.with_user(self.user_1)._add_members(users=self.user_portal)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_portal.partner_id)
# but user 2 is in the channel and can be invited by user 1
self.group_restricted_channel.with_user(self.user_1)._add_members(users=self.user_2)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.group_restricted_channel.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id | self.user_portal.partner_id)
# ------------------------------------------------------------
# PUBLIC CHANNELS
# ------------------------------------------------------------
def test_public_channel(self):
""" Test access on public channels """
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.public_channel.id)])
self.assertFalse(channel_members)
self.public_channel.with_user(self.user_1)._add_members(users=self.user_1)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.public_channel.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id)
self.public_channel.with_user(self.user_2)._add_members(users=self.user_2)
channel_members = self.env['discuss.channel.member'].search([('channel_id', '=', self.public_channel.id)])
self.assertEqual(channel_members.mapped('partner_id'), self.user_1.partner_id | self.user_2.partner_id)
self.public_channel.with_user(self.user_portal)._add_members(users=self.user_portal)
with self.assertRaises(ValidationError): # public cannot join without having a guest
self.public_channel.with_user(self.user_public)._add_members(users=self.user_public)
def test_channel_member_invite_with_guest(self):
guest = self.env['mail.guest'].create({'name': 'Guest'})
partner = self.env['res.partner'].create({
'name': 'ToInvite',
'active': True,
'type': 'contact',
'user_ids': self.user_1,
})
self.public_channel._add_members(guests=guest)
data = self.env["res.partner"].search_for_channel_invite(
partner.name, channel_id=self.public_channel.id
)["store_data"]
self.assertEqual(len(data["res.partner"]), 1)
self.assertEqual(data["res.partner"][0]["id"], partner.id)
# ------------------------------------------------------------
# UNREAD COUNTER TESTS
# ------------------------------------------------------------
def test_unread_counter_with_message_post(self):
channel_as_user_1 = self.env['discuss.channel'].with_user(self.user_1)._create_channel(group_id=None, name='Public channel')
channel_as_user_1.with_user(self.user_1)._add_members(users=self.user_1)
channel_as_user_1.with_user(self.user_1)._add_members(users=self.user_2)
channel_1_rel_user_2 = self.env['discuss.channel.member'].search([
('channel_id', '=', channel_as_user_1.id),
('partner_id', '=', self.user_2.partner_id.id)
])
self.assertEqual(channel_1_rel_user_2.message_unread_counter, 0, "should not have unread message initially as notification type is ignored")
channel_as_user_1.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
channel_1_rel_user_2 = self.env['discuss.channel.member'].search([
('channel_id', '=', channel_as_user_1.id),
('partner_id', '=', self.user_2.partner_id.id)
])
self.assertEqual(channel_1_rel_user_2.message_unread_counter, 1, "should have 1 unread message after someone else posted a message")
def test_unread_counter_with_message_post_multi_channel(self):
channel_1_as_user_1 = self.env['discuss.channel'].with_user(self.user_1)._create_channel(group_id=None, name='wololo channel')
channel_2_as_user_2 = self.env['discuss.channel'].with_user(self.user_2)._create_channel(group_id=None, name='walala channel')
channel_1_as_user_1._add_members(users=self.user_2)
channel_2_as_user_2._add_members(users=self.user_1)
channel_2_as_user_2._add_members(users=self.user_3)
channel_1_as_user_1.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
channel_1_as_user_1.message_post(body='Test 2', message_type='comment', subtype_xmlid='mail.mt_comment')
channel_2_as_user_2.message_post(body='Test', message_type='comment', subtype_xmlid='mail.mt_comment')
members = self.env['discuss.channel.member'].search([('channel_id', 'in', (channel_1_as_user_1 + channel_2_as_user_2).ids)], order="id")
self.assertEqual(members.mapped('message_unread_counter'), [
0, # channel 1 user 1: posted last message
0, # channel 2 user 2: posted last message
2, # channel 1 user 2: received 2 messages (from message post)
1, # channel 2 user 1: received 1 message (from message post)
1, # channel 2 user 3: received 1 message (from message post)
])

View file

@ -0,0 +1,76 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
try:
import websocket as ws
except ImportError:
ws = None
from itertools import product
from odoo.tests import tagged, new_test_user
from odoo.addons.bus.tests.common import WebsocketCase
from odoo.addons.mail.tests.common import MailCommon, freeze_all_time
from odoo.addons.bus.models.bus import channel_with_db, json_dump
@tagged("post_install", "-at_install")
class TestMailPresence(WebsocketCase, MailCommon):
def _receive_presence(self, requested_by, target, has_token=False):
self.env["mail.presence"].search([]).unlink()
target_user = isinstance(target, self.env.registry["res.users"])
if isinstance(requested_by, self.env.registry["res.users"]):
session = self.authenticate(requested_by.login, requested_by.login)
auth_cookie = f"session_id={session.sid};"
else:
self.authenticate(None, None)
auth_cookie = f"{requested_by._cookie_name}={requested_by._format_auth_cookie()};"
websocket = self.websocket_connect(cookie=auth_cookie)
target_channel = target.partner_id if target_user else target
channel_parts = ["odoo-presence", f"{target_channel._name}_{target_channel.id}"]
if has_token:
channel_parts.append(target_channel._get_im_status_access_token())
self.subscribe(websocket, ["-".join(channel_parts)], self.env["bus.bus"]._bus_last_id())
self.env["mail.presence"]._update_presence(target)
self.trigger_notification_dispatching([(target_channel, "presence")])
notifications = json.loads(websocket.recv())
self._close_websockets()
bus_record = self.env["bus.bus"].search([("id", "=", int(notifications[0]["id"]))])
self.assertEqual(
bus_record.channel,
json_dump(channel_with_db(self.env.cr.dbname, (target_channel, "presence"))),
)
self.assertEqual(notifications[0]["message"]["type"], "bus.bus/im_status_updated")
self.assertEqual(notifications[0]["message"]["payload"]["im_status"], "online")
self.assertEqual(notifications[0]["message"]["payload"]["presence_status"], "online")
self.assertEqual(
notifications[0]["message"]["payload"]["partner_id" if target_user else "guest_id"],
target_channel.id,
)
@freeze_all_time()
def test_presence_access(self):
internal = new_test_user(self.env, login="internal_user", groups="base.group_user")
other_internal = new_test_user(
self.env, login="other_internal_user", groups="base.group_user"
)
portal = new_test_user(self.env, login="portal_user", groups="base.group_portal")
other_portal = new_test_user(
self.env, login="other_portal_user", groups="base.group_portal"
)
guest = self.env["mail.guest"].create({"name": "Guest"})
other_guest = self.env["mail.guest"].create({"name": "Other Guest"})
for requested_by, target, has_token, allowed in [
*product([internal], [guest, other_internal, portal], [True, False], [True]),
*product([guest, portal], [internal, other_guest, other_portal], [False], [False]),
*product([guest, portal], [internal, other_guest, other_portal], [True], [True]),
]:
with self.subTest(
f"test presence access, requested_by={requested_by.name}, target={target.name}, has_token={has_token}, allowed={allowed}"
):
if allowed:
self._receive_presence(requested_by, target, has_token=has_token)
else:
with self.assertRaises(ws._exceptions.WebSocketTimeoutException):
self._receive_presence(requested_by, target, has_token=has_token)

View file

@ -0,0 +1,30 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests.common import HttpCase, new_test_user, tagged
@tagged("post_install", "-at_install")
class TestDiscussMentionSuggestions(HttpCase):
def test_mention_suggestions_group_restricted_channel(self):
user_admin = self.env.ref("base.user_admin")
user_group = self.env.ref("base.group_user")
rd_group = self.env["res.groups"].create({"name": "R&D Group"})
new_test_user(self.env, login="dev", name="Dev User", group_ids=[user_group.id, rd_group.id])
# have a user that is not channel member and not in group -> should not be suggested as mention
new_test_user(self.env, login="sales", name="Sales User", groups="base.group_user")
consultant_user = new_test_user(self.env, login="consultant", name="Consultant User", groups="base.group_user")
rd_channel = self.env['discuss.channel'].with_user(user_admin).create({
"name": "R&D Channel",
"channel_type": "channel",
"group_public_id": rd_group.id,
"channel_member_ids": [
Command.create({"partner_id": consultant_user.partner_id.id}),
Command.create({"partner_id": user_admin.partner_id.id}),
],
})
self.start_tour(
f"/odoo/discuss?active_id=discuss.channel_{rd_channel.id}",
"discuss_mention_suggestions_group_restricted_channel.js",
login="admin",
)

View file

@ -0,0 +1,50 @@
from odoo.addons.mail.tests.common_controllers import MailControllerUpdateCommon
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestDiscussMessageUpdateController(MailControllerUpdateCommon):
def test_message_update_guest_as_owner(self):
"""Test only admin user and message author can update the message content in a channel."""
channel = self.env["discuss.channel"].create(
{"group_public_id": None, "name": "public channel"}
)
channel._add_members(guests=self.guest)
# sudo: discuss.channel: posting a message as guest in a test is acceptable
message = (
channel.with_user(self.user_public)
.with_context(guest=self.guest)
.sudo()
.message_post(body=self.message_body, message_type="comment")
)
self._execute_subtests(
message,
(
(self.guest, True),
(self.user_admin, True),
(self.user_employee, False),
(self.user_portal, False),
(self.user_public, False),
),
)
def test_message_update_public_channel(self):
"""Test only admin user can update the message content of other authors in a channel."""
channel = self.env["discuss.channel"].create(
{"group_public_id": None, "name": "public channel"}
)
message = channel.message_post(
body=self.message_body,
message_type="comment",
)
self._execute_subtests(
message,
(
(self.guest, False),
(self.user_admin, True),
(self.user_employee, False),
(self.user_portal, False),
(self.user_public, False),
),
)

View file

@ -0,0 +1,56 @@
from odoo.addons.mail.tests.common_controllers import MailControllerReactionCommon
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestMessageReactionController(MailControllerReactionCommon):
def test_message_reaction_public_channel(self):
"""Test access of message reaction for a public channel."""
channel = self.env["discuss.channel"].create(
{"group_public_id": None, "name": "public channel"}
)
message = channel.message_post(body="public message")
self._execute_subtests(
message,
(
(self.user_public, False),
(self.guest, True),
(self.user_portal, True),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_message_reaction_channel_as_member(self):
"""Test access of message reaction for a channel as member."""
channel = self.env["discuss.channel"]._create_group(
partners_to=(self.user_portal + self.user_employee).partner_id.ids
)
channel._add_members(guests=self.guest)
message = channel.message_post(body="invite message")
self._execute_subtests(
message,
(
(self.user_public, False),
(self.guest, True),
(self.user_portal, True),
(self.user_employee, True),
(self.user_admin, True),
),
)
def test_message_reaction_channel_as_non_member(self):
"""Test access of message reaction for a channel as non-member."""
channel = self.env["discuss.channel"]._create_group(partners_to=[])
message = channel.message_post(body="private message")
self._execute_subtests(
message,
(
(self.user_public, False),
(self.guest, False),
(self.user_portal, False),
(self.user_employee, False),
(self.user_admin, True),
),
)

View file

@ -0,0 +1,58 @@
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.test_res_role import TestResRole
from odoo.tests.common import tagged
@tagged("-at_install", "post_install")
class TestDiscussResRole(TestResRole):
def test_only_mention_by_role_when_channel_is_accessible(self):
self.authenticate("admin", "admin")
role = self.env["res.role"].create({"name": "rd-Discuss"})
for idx, test_case in enumerate([
# channel_type, channel_grp, user_grp, is_member, mentionned
("channel", None, "base.group_user", False, True),
("channel", None, "base.group_user", True, True),
("channel", "base.group_system", "base.group_user", False, False),
("channel", "base.group_system", "base.group_system", True, True),
("group", None, "base.group_user", False, False),
("group", None, "base.group_user", True, True),
]):
channel_type, channel_grp, user_grp, is_member, mentionned = test_case
with self.subTest(
channel_type=channel_type,
channel_grp=channel_grp,
user_grp=user_grp,
is_member=is_member,
notified=mentionned,
):
channel = self.env["discuss.channel"].create(
{
"name": f"channel_{channel_grp}_{user_grp}_{mentionned}",
"channel_type": channel_type,
"group_public_id": self.env.ref(channel_grp).id if channel_grp else None,
}
)
user = mail_new_test_user(
self.env, login=f"user_{user_grp}_{idx}", role_ids=role.ids, groups=user_grp
)
if is_member:
channel.add_members(partner_ids=user.partner_id.ids)
data = self.make_jsonrpc_request(
"/mail/message/post",
{
"thread_model": "discuss.channel",
"thread_id": channel.id,
"post_data": {
"body": "irrelevant",
"message_type": "comment",
"role_ids": role.ids,
"subtype_xmlid": "mail.mt_note",
},
},
)
formatted_partner = user.partner_id.id
message = next(filter(lambda m: m["id"] == data["message_id"], data["store_data"]["mail.message"]))
if mentionned:
self.assertIn(formatted_partner, message["partner_ids"])
else:
self.assertNotIn(formatted_partner, message["partner_ids"])

View file

@ -0,0 +1,231 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from datetime import datetime, timedelta
from freezegun import freeze_time
from unittest.mock import patch
from odoo.tests.common import HttpCase, new_test_user, tagged
from odoo.exceptions import UserError, ValidationError
@tagged("post_install", "-at_install")
class TestDiscussSubChannels(HttpCase):
def test_01_gc_unpin_outdated_sub_channels(self):
bob = new_test_user(self.env, "bob_user", groups="base.group_user")
parent = self.env["discuss.channel"].create({"name": "General"})
parent._create_sub_channel()
sub_channel = parent.sub_channel_ids[0]
sub_channel._add_members(users=self.env.user)
sub_channel.channel_pin(pinned=True)
self_member = sub_channel.channel_member_ids.filtered(lambda m: m.is_self)
self.assertTrue(self_member.is_pinned)
# Last interrest of the member is older than 2 days, no activity on the
# channel: should be unpinned.
two_days_later_dt = datetime.now() + timedelta(days=3)
with freeze_time(two_days_later_dt) as frozen_time:
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
self.assertFalse(self_member.is_pinned)
unpin_dt = self_member.unpin_dt
# The member isn't unpinned again when GC re-runs.
frozen_time.tick(delta=timedelta(days=1))
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
self.assertEqual(self_member.unpin_dt, unpin_dt)
sub_channel.channel_pin(pinned=True)
with freeze_time(two_days_later_dt) as frozen_time:
# Last interrest older than 2 days, activity on the channel: should be kept.
message = sub_channel.with_user(bob).message_post(body="Hey!", message_type="comment")
self_member._mark_as_read(message.id)
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
self.assertTrue(self_member.is_pinned)
# Unread messages: should be kept regardless of last interrest.
message = sub_channel.with_user(bob).message_post(body="Another message!", message_type="comment")
frozen_time.tick(delta=timedelta(days=3))
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
self.assertTrue(self_member.is_pinned)
self_member._mark_as_read(message.id)
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
self.assertFalse(self_member.is_pinned)
# Ensure regular channels are not impacted.
channel = self.env["discuss.channel"].create({"name": "General"})
channel.channel_pin(pinned=True)
with freeze_time(two_days_later_dt):
self.env["discuss.channel.member"]._gc_unpin_outdated_sub_channels()
self.assertTrue(channel.channel_member_ids.filtered("is_self").is_pinned)
def test_02_sub_channel_members_sync_with_parent(self):
parent = self.env["discuss.channel"].create({"name": "General"})
parent.action_unfollow()
self.assertFalse(any(m.is_self for m in parent.channel_member_ids))
parent._create_sub_channel()
sub_channel = parent.sub_channel_ids[0]
# Member created for sub channel (_create_sub_channel): should also be
# created for the parent channel.
self.assertTrue(any(m.is_self for m in parent.channel_member_ids))
self.assertTrue(any(m.is_self for m in sub_channel.channel_member_ids))
# Member removed from parent channel: should also be removed from the sub
# channel.
parent.action_unfollow()
self.assertFalse(any(m.is_self for m in parent.channel_member_ids))
self.assertFalse(any(m.is_self for m in sub_channel.channel_member_ids))
# Member created for sub channel (add_members): should also be created
# for parent.
sub_channel._add_members(users=self.env.user)
self.assertTrue(any(m.is_self for m in parent.channel_member_ids))
self.assertTrue(any(m.is_self for m in sub_channel.channel_member_ids))
def test_03_cannot_create_recursive_sub_channel(self):
parent = self.env["discuss.channel"].create({"name": "General"})
parent._create_sub_channel()
sub_channel = parent.sub_channel_ids[0]
with self.assertRaises(ValidationError):
sub_channel._create_sub_channel()
def test_04_sub_channel_panel_search(self):
bob_user = new_test_user(self.env, "bob_user", groups="base.group_user")
self.authenticate("bob_user", "bob_user")
channel = self.env["discuss.channel"]._create_channel(name="General", group_id=None)
channel._add_members(users=bob_user)
for i in range(100):
channel._create_sub_channel(name=f"Sub Channel {i}")
self.start_tour(
f"/odoo/discuss?active_id=discuss.channel_{channel.id}",
"test_discuss_sub_channel_search",
login="bob_user",
)
def test_05_cannot_upate_first_message_nor_parent_channel(self):
parent = self.env["discuss.channel"].create({"name": "General"})
parent.message_post(body="Hello there!")
parent._create_sub_channel(from_message_id=parent.message_ids[0].id)
sub_channel = parent.sub_channel_ids[0]
random_channel = self.env["discuss.channel"].create({"name": "Random"})
parent.message_post(body="Random message")
with self.assertRaises(UserError, msg="Cannot change initial message nor parent channel of: Hello there!."):
sub_channel.parent_channel_id = random_channel
with self.assertRaises(UserError, msg="Cannot change initial message nor parent channel of: Hello there!."):
sub_channel.from_message_id = parent.message_ids[0]
def test_06_initial_message_must_belong_to_parent_channel(self):
parent = self.env["discuss.channel"].create({"name": "General"})
random_channel = self.env["discuss.channel"].create({"name": "Random"})
random_channel.message_post(body="Hello world!")
with self.assertRaises(
ValidationError,
msg="Cannot create Hello world!: initial message should belong to parent channel.",
):
parent._create_sub_channel(from_message_id=random_channel.message_ids[0].id)
def test_07_unlink_sub_channel(self):
bob_user = new_test_user(self.env, "bob_user", groups="base.group_user")
baz_user = new_test_user(self.env, "baz_user", groups="base.group_user")
parent_1 = self.env["discuss.channel"].with_user(bob_user).create({"name": "Parent 1"})
parent_1_baz_member = parent_1._add_members(users=baz_user)
parent_1_sub_channel_1 = parent_1._create_sub_channel(name="Parent 1 Sub 1")
parent_1_sub_channel_1._add_members(users=baz_user)
parent_1_sub_channel_2 = parent_1._create_sub_channel(name="Parent 1 Sub 2")
parent_1_sub_channel_2._add_members(users=baz_user)
parent_2 = self.env["discuss.channel"].with_user(baz_user).create({"name": "Parent 2"})
parent_2_bob_member = parent_2._add_members(users=bob_user)
parent_2_sub_channel = parent_2._create_sub_channel(name="Parent 2 Sub")
parent_2_sub_channel._add_members(users=bob_user)
parent_3 = self.env["discuss.channel"].with_user(bob_user).create({"name": "Parent 3"})
guest = self.env["mail.guest"].create({"name": "Guest"})
parent_3_guest_member = parent_3._add_members(guests=guest)
parent_3_sub_channel = parent_3._create_sub_channel(name="Parent 3 Sub")
parent_3_sub_channel._add_members(guests=guest)
members_to_unlink = parent_1_baz_member + parent_2_bob_member + parent_3_guest_member
members_to_unlink.sudo().unlink()
self.assertNotIn(
baz_user.partner_id,
parent_1.channel_member_ids.partner_id
| parent_1.sub_channel_ids.channel_member_ids.partner_id,
)
self.assertNotIn(
bob_user.partner_id,
parent_2.channel_member_ids.partner_id
| parent_2.sub_channel_ids.channel_member_ids.partner_id,
)
self.assertNotIn(
guest,
parent_3.channel_member_ids.guest_id
| parent_3.sub_channel_ids.channel_member_ids.guest_id,
)
self.assertIn(bob_user.partner_id, parent_1_sub_channel_1.channel_member_ids.partner_id)
self.assertIn(bob_user.partner_id, parent_1_sub_channel_2.channel_member_ids.partner_id)
self.assertIn(baz_user.partner_id, parent_2_sub_channel.channel_member_ids.partner_id)
self.assertIn(bob_user.partner_id, parent_3_sub_channel.channel_member_ids.partner_id)
def test_08_group_public_id_synced_with_parent(self):
parent = self.env["discuss.channel"].create({"name": "General"})
parent._create_sub_channel()
sub_channel = parent.sub_channel_ids[0]
self.assertEqual(parent.group_public_id, self.env.ref("base.group_user"))
self.assertEqual(sub_channel.group_public_id, parent.group_public_id)
parent.group_public_id = self.env.ref("base.group_system")
self.assertEqual(parent.group_public_id, self.env.ref("base.group_system"))
self.assertEqual(sub_channel.group_public_id, parent.group_public_id)
parent.group_public_id = None
self.assertEqual(parent.group_public_id, self.env["res.groups"])
self.assertEqual(sub_channel.group_public_id, parent.group_public_id)
def test_09_cannot_change_group_public_id_of_sub_channel(self):
parent = self.env["discuss.channel"].create({"name": "General"})
parent._create_sub_channel()
sub_channel = parent.sub_channel_ids[0]
with self.assertRaises(UserError):
sub_channel.group_public_id = self.env.ref("base.group_system")
def test_10_sub_channel_message_author_member(self):
bob_user = new_test_user(self.env, "bob_user", groups="base.group_user")
parent = self.env["discuss.channel"].create({
"name": "General",
"channel_member_ids": [Command.create({"partner_id": bob_user.partner_id.id})],
})
message = parent.with_user(bob_user).message_post(body="Hello there!")
sub_channel = parent._create_sub_channel(from_message_id=message.id)
self.assertIn(bob_user.partner_id, sub_channel.channel_member_ids.partner_id)
self.assertEqual(len(sub_channel.channel_member_ids), 2)
def test_11_sub_channel_fallback_name_on_empty_message(self):
parent = self.env["discuss.channel"].create({"name": "General"})
message = parent.message_post(body="Hello there!", message_type="comment")
parent._message_update_content(message, body="")
sub_channel = parent._create_sub_channel(from_message_id=message.id)
self.assertEqual(sub_channel.name, "This message has been removed")
def test_12_unlink_children_members_only_once(self):
parent = self.env["discuss.channel"].create({"name": "General"})
child = parent._create_sub_channel()
og_unlink = self.env.registry["discuss.channel.member"].unlink
unlinked_member_ids = []
expected_unlinked_member_ids = sorted((parent.self_member_id | child.self_member_id).ids)
def _patched_unlink(records):
unlinked_member_ids.extend(records.ids)
og_unlink(records)
with patch.object(self.env.registry["discuss.channel.member"], "unlink", _patched_unlink):
(parent | child).channel_member_ids.unlink()
self.assertEqual(expected_unlinked_member_ids, sorted(unlinked_member_ids))
def test_13_mentioned_user_becomes_sub_channel_member(self):
alice_user = new_test_user(self.env, "alice_user", groups="base.group_user")
bob_user = new_test_user(self.env, "bob_user", groups="base.group_user")
parent = self.env["discuss.channel"].create({
"name": "General",
"channel_member_ids": [
Command.create({"partner_id": alice_user.partner_id.id}),
Command.create({"partner_id": bob_user.partner_id.id}),
],
})
message = parent.with_user(bob_user).message_post(body="Hello there!")
sub_channel = parent._create_sub_channel(from_message_id=message.id)
self.assertNotIn(alice_user.partner_id, sub_channel.channel_member_ids.partner_id)
sub_channel.with_user(bob_user).message_post(
body="Check this out @Alice",
partner_ids=[alice_user.partner_id.id],
)
self.assertIn(alice_user.partner_id, sub_channel.channel_member_ids.partner_id)

View file

@ -0,0 +1,104 @@
from odoo.addons.mail.tests.common_controllers import MailControllerThreadCommon, MessagePostSubTestData
from odoo.tests import tagged
@tagged("-at_install", "post_install", "mail_controller")
class TestDiscussThreadController(MailControllerThreadCommon):
def test_internal_channel_message_post_access(self):
"""Test access of message_post on internal channel."""
channel = self.env["discuss.channel"].create({"name": "Internal Channel"})
def test_access(user, allowed):
return MessagePostSubTestData(user, allowed)
self._execute_message_post_subtests(
channel,
(
test_access(self.user_public, False),
test_access(self.guest, False),
test_access(self.user_portal, False),
test_access(self.user_employee, True),
test_access(self.user_admin, True),
),
)
def test_public_channel_message_post_access(self):
"""Test access of message_post on public channel."""
channel = self.env["discuss.channel"].create(
{"name": "Public Channel", "group_public_id": None}
)
def test_access(user, allowed, exp_author=None):
return MessagePostSubTestData(user, allowed, exp_author=exp_author)
self._execute_message_post_subtests(
channel,
(
test_access(self.user_public, True),
test_access(self.guest, True),
test_access(self.user_portal, True),
test_access(self.user_employee, True),
test_access(self.user_admin, True),
),
)
def test_public_channel_message_post_partner_ids(self):
"""Test partner_ids of message_post on public channel.
Non-internal users cannot use mentions without mention_token."""
channel = self.env["discuss.channel"].create(
{"name": "Public Channel", "group_public_id": None}
)
channel._add_members(users=self.user_employee_nopartner)
partners = (
self.user_portal + self.user_employee + self.user_employee_nopartner + self.user_admin
).partner_id
def test_partners(user, allowed, exp_partners):
return MessagePostSubTestData(
user, allowed, partners=partners, exp_partners=exp_partners
)
self._execute_message_post_subtests(
channel,
(
test_partners(self.user_public, True, self.env["res.partner"]),
test_partners(self.guest, True, self.env["res.partner"]),
test_partners(self.user_portal, True, self.env["res.partner"]),
test_partners(self.user_employee, True, partners),
test_partners(self.user_employee_nopartner, True, partners),
test_partners(self.user_admin, True, partners),
),
)
def test_public_channel_message_post_partner_emails(self):
"""Test partner_emails of message_post on public channel can only be
used by users of base.group_partner_manager."""
channel = self.env["discuss.channel"].create(
{"name": "Public Channel", "group_public_id": None}
)
no_emails = []
existing_emails = [self.user_employee.email]
partner_emails = [self.user_employee.email, "test@example.com"]
def test_emails(user, allowed, exp_emails, exp_author=None):
return MessagePostSubTestData(
user,
allowed,
partner_emails=partner_emails,
exp_author=exp_author,
exp_emails=exp_emails,
)
self._execute_message_post_subtests(
channel,
(
test_emails(self.user_public, True, no_emails),
test_emails(self.guest, True, no_emails),
test_emails(self.user_portal, True, no_emails),
# restricted because not base.group_partner_manager: find existing only
test_emails(self.user_employee_nopartner, True, existing_emails),
test_emails(self.user_employee, True, partner_emails),
test_emails(self.user_admin, True, partner_emails),
),
)

View file

@ -0,0 +1,47 @@
from odoo import fields
from odoo.addons.mail.tests.common import MailCase
from odoo.tests.common import tagged
@tagged("post_install", "-at_install")
class TestGuest(MailCase):
def test_updating_guest_name_linked_to_multiple_channels(self):
"""This test ensures that when a guest is linked to multiple channels,
the guest's name is updated correctly and the appropriate bus notifications are sent.
"""
guest = self.env['mail.guest'].create({'name': 'Guest'})
channel_1 = self.env["discuss.channel"]._create_channel(name="Channel 1", group_id=None)
channel_2 = self.env["discuss.channel"]._create_channel(name="Channel 2", group_id=None)
channel_1._add_members(guests=guest)
channel_2._add_members(guests=guest)
def get_guest_bus_params():
guest_write_date = fields.Datetime.to_string(guest.write_date)
message = {
"type": "mail.record/insert",
"payload": {
"mail.guest": [
{
"avatar_128_access_token": guest._get_avatar_128_access_token(),
"id": guest.id,
"name": "Guest Name Updated",
"write_date": guest_write_date,
},
],
},
}
return (
[
(self.cr.dbname, "discuss.channel", channel_1.id),
(self.cr.dbname, "discuss.channel", channel_2.id),
(self.cr.dbname, "mail.guest", guest.id),
],
[message, message, message],
)
self._reset_bus()
with self.assertBus(get_params=get_guest_bus_params):
guest._update_name("Guest Name Updated")
self.assertEqual(guest.name, "Guest Name Updated")

View file

@ -0,0 +1,61 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo.tests import tagged
from odoo.addons.bus.tests.common import WebsocketCase
from odoo.addons.mail.tests.common import MailCommon
@tagged("post_install", "-at_install")
class TestGuestFeature(WebsocketCase, MailCommon):
def test_mark_as_read_as_guest(self):
guest = self.env["mail.guest"].create({"name": "Guest"})
partner = self.env["res.partner"].create({"name": "John"})
channel = self.env["discuss.channel"]._create_channel(
group_id=None, name="General"
)
channel._add_members(guests=guest, partners=partner)
channel.message_post(
body="Hello World!", message_type="comment", subtype_xmlid="mail.mt_comment"
)
guest_member = channel.channel_member_ids.filtered(
lambda m: m.guest_id == guest
)
self.assertEqual(guest_member.seen_message_id, self.env["mail.message"])
self.make_jsonrpc_request(
"/discuss/channel/mark_as_read",
{
"channel_id": channel.id,
"last_message_id": channel.message_ids[0].id,
},
cookies={guest._cookie_name: guest._format_auth_cookie()}
)
self.assertEqual(guest_member.seen_message_id, channel.message_ids[0])
def test_subscribe_to_guest_channel(self):
self._reset_bus()
guest = self.env["mail.guest"].create({"name": "Guest"})
guest_websocket = self.websocket_connect()
self.subscribe(guest_websocket, [f"mail.guest_{guest._format_auth_cookie()}"], guest.id)
guest._bus_send("lambda", {"foo": "bar"})
self.trigger_notification_dispatching([guest])
notifications = json.loads(guest_websocket.recv())
self.assertEqual(1, len(notifications))
self.assertEqual(notifications[0]["message"]["type"], "lambda")
self.assertEqual(notifications[0]["message"]["payload"], {"foo": "bar"})
def test_subscribe_to_discuss_channel(self):
guest = self.env["mail.guest"].create({"name": "Guest"})
channel = self.env["discuss.channel"]._create_channel(
group_id=None, name="General"
)
channel._add_members(guests=guest)
self._reset_bus()
guest_websocket = self.websocket_connect()
self.subscribe(guest_websocket, [f"mail.guest_{guest._format_auth_cookie()}"], guest.id)
channel._bus_send("lambda", {"foo": "bar"})
self.trigger_notification_dispatching([channel])
notifications = json.loads(guest_websocket.recv())
self.assertEqual(1, len(notifications))
self.assertEqual(notifications[0]["message"]["type"], "lambda")
self.assertEqual(notifications[0]["message"]["payload"], {"foo": "bar"})

View file

@ -0,0 +1,26 @@
import odoo.tests
from odoo import Command
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
@odoo.tests.tagged('post_install', '-at_install')
class TestLoadMessages(HttpCaseWithUserDemo):
def test_01_mail_message_load_order_tour(self):
partner_admin = self.env.ref('base.partner_admin')
channel_id = self.env["discuss.channel"].create({
"name": "MyTestChannel",
"channel_member_ids": [Command.create({"partner_id": partner_admin.id})],
})
self.env["mail.message"].create([{
"body": str(n),
"model": "discuss.channel",
"pinned_at": odoo.fields.Datetime.now() if n == 1 else None,
"res_id": channel_id.id,
"author_id": partner_admin.id,
"message_type": "comment",
} for n in range(1, 61)])
channel_id.channel_member_ids.filtered(
lambda m: m.partner_id == partner_admin
)._mark_as_read(channel_id.message_ids[0].id)
self.start_tour("/odoo/action-mail.action_discuss", "mail_message_load_order_tour", login="admin")

View file

@ -0,0 +1,351 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import odoo
from odoo.tests import tagged, users
from odoo.tools import mute_logger
from odoo.addons.base.tests.common import HttpCase, HttpCaseWithUserDemo
from odoo.addons.mail.tests.common import MailCommon, mail_new_test_user
from odoo.http import STATIC_CACHE_LONG
from odoo import Command, fields
@odoo.tests.tagged("-at_install", "post_install", "mail_controller")
class TestMessageController(HttpCaseWithUserDemo):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.channel = cls.env["discuss.channel"].create(
{
"group_public_id": None,
"name": "Test channel",
}
)
cls.public_user = cls.env.ref("base.public_user")
cls.attachments = (
cls.env["ir.attachment"]
.with_user(cls.public_user)
.sudo()
.create(
[
{
"name": "File 1",
"res_id": 0,
"res_model": "mail.compose.message",
},
{
"name": "File 2",
"res_id": 0,
"res_model": "mail.compose.message",
},
]
)
)
cls.guest = cls.env["mail.guest"].create({"name": "Guest"})
cls.channel._add_members(guests=cls.guest)
@mute_logger("odoo.addons.http_routing.models.ir_http", "odoo.http")
def test_channel_message_attachments(self):
self.authenticate(None, None)
self.opener.cookies[self.guest._cookie_name] = self.guest._format_auth_cookie()
# test message post: token error
res1 = self.url_open(
url="/mail/message/post",
data=json.dumps(
{
"params": {
"thread_model": "discuss.channel",
"thread_id": self.channel.id,
"post_data": {
"body": "test",
"attachment_ids": [self.attachments[0].id],
"attachment_tokens": ["wrong token"],
},
},
},
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res1.status_code, 200)
self.assertIn(
"One or more attachments do not exist, or you do not have the rights to access them.",
res1.text,
"guest should not be allowed to add attachment without token when posting message",
)
# test message post: token ok
res2 = self.url_open(
url="/mail/message/post",
data=json.dumps(
{
"params": {
"thread_model": "discuss.channel",
"thread_id": self.channel.id,
"post_data": {
"body": "test",
"attachment_ids": [self.attachments[0].id],
"attachment_tokens": [self.attachments[0]._get_ownership_token()],
"message_type": "comment",
},
},
},
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res2.status_code, 200)
data1 = res2.json()["result"]
self.assertEqual(
data1["store_data"]["ir.attachment"],
[
{
"checksum": False,
"create_date": fields.Datetime.to_string(self.attachments[0].create_date),
"file_size": 0,
"has_thumbnail": False,
"id": self.attachments[0].id,
"mimetype": "application/octet-stream",
"name": "File 1",
"ownership_token": self.attachments[0]._get_ownership_token(),
"raw_access_token": self.attachments[0]._get_raw_access_token(),
"res_name": "Test channel",
"res_model": self.attachments[0].res_model,
"thread": {"id": self.channel.id, "model": "discuss.channel"},
"thumbnail_access_token": self.attachments[0]._get_thumbnail_token(),
"voice_ids": [],
'type': 'binary',
'url': False,
},
],
"guest should be allowed to add attachment with token when posting message",
)
# test message update: token error
res3 = self.url_open(
url="/mail/message/update_content",
data=json.dumps(
{
"params": {
"message_id": data1["message_id"],
"update_data": {
"body": "test",
"attachment_ids": [self.attachments[1].id],
"attachment_tokens": ["wrong token"],
},
},
},
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res3.status_code, 200)
self.assertIn(
"One or more attachments do not exist, or you do not have the rights to access them.",
res3.text,
"guest should not be allowed to add attachment without token when updating message",
)
# test message update: token ok
res4 = self.url_open(
url="/mail/message/update_content",
data=json.dumps(
{
"params": {
"message_id": data1["message_id"],
"update_data": {
"body": "test",
"attachment_ids": [self.attachments[1].id],
"attachment_tokens": [self.attachments[1]._get_ownership_token()],
},
},
},
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res4.status_code, 200)
data2 = res4.json()["result"]
self.assertEqual(
data2["ir.attachment"],
[
{
"checksum": False,
"create_date": fields.Datetime.to_string(self.attachments[0].create_date),
"file_size": 0,
"has_thumbnail": False,
"id": self.attachments[0].id,
"mimetype": "application/octet-stream",
"name": "File 1",
"ownership_token": self.attachments[0]._get_ownership_token(),
"raw_access_token": self.attachments[0]._get_raw_access_token(),
"res_name": "Test channel",
"res_model": self.attachments[0].res_model,
"thread": {"id": self.channel.id, "model": "discuss.channel"},
"thumbnail_access_token": self.attachments[0]._get_thumbnail_token(),
"voice_ids": [],
'type': 'binary',
'url': False,
},
{
"checksum": False,
"create_date": fields.Datetime.to_string(self.attachments[1].create_date),
"file_size": 0,
"has_thumbnail": False,
"id": self.attachments[1].id,
"mimetype": "application/octet-stream",
"name": "File 2",
"ownership_token": self.attachments[1]._get_ownership_token(),
"raw_access_token": self.attachments[1]._get_raw_access_token(),
"res_name": "Test channel",
"res_model": self.attachments[1].res_model,
"thread": {"id": self.channel.id, "model": "discuss.channel"},
"thumbnail_access_token": self.attachments[1]._get_thumbnail_token(),
"voice_ids": [],
'type': 'binary',
'url': False,
},
],
"guest should be allowed to add attachment with token when updating message",
)
@mute_logger("odoo.addons.http_routing.models.ir_http", "odoo.http")
def test_mail_partner_from_email_unauthenticated(self):
self.authenticate(None, None)
self.opener.cookies[self.guest._cookie_name] = self.guest._format_auth_cookie()
res1 = self.url_open(
url="/mail/partner/from_email",
data=json.dumps(
{
"params": {
"thread_model": "discuss.channel",
"thread_id": self.channel.id,
"emails": ["john@test.be"],
},
}
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res1.status_code, 200)
self.assertEqual(
0,
self.env["res.partner"].search_count([('email', '=', "john@test.be")]),
"guest should not be allowed to create a partner from an email",
)
self.authenticate(None, None)
self.opener.cookies[self.guest._cookie_name] = self.guest._format_auth_cookie()
res1 = self.url_open(
url="/mail/partner/from_email",
data=json.dumps(
{
"params": {
"thread_model": "discuss.channel",
"thread_id": self.channel.id,
"emails": ["john@test.be"],
},
}
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res1.status_code, 200)
self.assertEqual(
0,
self.env["res.partner"].search_count([('email', '=', "john@test.be")]),
"guest should not be allowed to create a partner from an email",
)
res2 = self.url_open(
url="/mail/message/post",
data=json.dumps(
{
"params": {
"thread_model": "discuss.channel",
"thread_id": self.channel.id,
"post_data": {
"body": "test",
"partner_emails": ["john@test.be"],
},
},
},
),
headers={"Content-Type": "application/json"},
)
self.assertEqual(res2.status_code, 200)
self.assertEqual(
0,
self.env["res.partner"].search_count([('email', '=', "john@test.be")]),
"guest should not be allowed to create a partner from an email from message_post",
)
def test_mail_cache_control_header(self):
testuser = self.env['res.users'].create({
'email': 'testuser@testuser.com',
'group_ids': [Command.set([self.ref('base.group_portal')])],
'name': 'Test User',
'login': 'testuser',
'password': 'testuser',
})
test_user = self.authenticate("testuser", "testuser")
partner = self.env["res.users"].browse(test_user.uid).partner_id
self.channel._add_members(users=testuser)
res = self.url_open(
url=f"/web/image/?field=avatar_128&id={self.channel.id}&model=discuss.channel&unique={self.channel.avatar_cache_key}"
)
self.assertIn(f"max-age={STATIC_CACHE_LONG}", res.headers["Cache-Control"])
res = self.url_open(
url=f"/web/image/?field=avatar_128&id={self.channel.id}&model=discuss.channel"
)
self.assertIn("no-cache", res.headers["Cache-Control"])
res = self.url_open(
url=f"/web/image?field=avatar_128&id={partner.id}&model=res.partner&unique={fields.Datetime.to_string(partner.write_date)}"
)
self.assertIn(f"max-age={STATIC_CACHE_LONG}", res.headers["Cache-Control"])
res = self.url_open(
url=f"/web/image?field=avatar_128&id={partner.id}&model=res.partner"
)
self.assertIn("no-cache", res.headers["Cache-Control"])
res = self.url_open(
url=f"/web/image?field=avatar_128&id={self.guest.id}&model=mail.guest&unique={fields.Datetime.to_string(partner.write_date)}"
)
self.assertIn(f"max-age={STATIC_CACHE_LONG}", res.headers["Cache-Control"])
res = self.url_open(
url=f"/web/image?field=avatar_128&id={self.guest.id}&model=mail.guest"
)
self.assertIn("no-cache", res.headers["Cache-Control"])
@tagged("mail_message")
class TestMessageLinks(MailCommon, HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_employee_1 = mail_new_test_user(cls.env, login='tao1', groups='base.group_user', name='Tao Lee')
cls.public_channel = cls.env['discuss.channel']._create_channel(name='Public Channel1', group_id=None)
cls.private_group = cls.env['discuss.channel']._create_group(partners_to=cls.user_employee_1.partner_id.ids, name="Group")
@users('employee')
def test_message_link_by_employee(self):
channel_message = self.public_channel.message_post(body='Public Channel Message', message_type='comment')
private_message_id = self.private_group.with_user(self.user_employee_1).message_post(
body='Private Message',
message_type='comment',
).id
self.authenticate('employee', 'employee')
with self.subTest(channel_message=channel_message):
expected_url = self.base_url() + f'/odoo/action-mail.action_discuss?active_id={channel_message.res_id}&highlight_message_id={channel_message.id}'
res = self.url_open(f'/mail/message/{channel_message.id}')
self.assertEqual(res.url, expected_url)
with self.subTest(private_message_id=private_message_id):
res = self.url_open(f'/mail/message/{private_message_id}')
self.assertEqual(res.status_code, 404)
@users('employee')
def test_message_link_by_public(self):
message = self.public_channel.message_post(
body='Public Channel Message',
message_type='comment',
subtype_xmlid='mail.mt_comment'
)
res = self.url_open(f'/mail/message/{message.id}')
self.assertEqual(res.status_code, 200)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from requests.exceptions import HTTPError
from odoo import Command, http
from odoo.tests.common import tagged, HttpCase
from odoo.tools import file_open, mute_logger
@tagged("post_install", "-at_install")
class TestToggleUpload(HttpCase):
def test_upload_allowed(self):
self.authenticate(None, None)
channel = self.env["discuss.channel"].create({"name": "General", "group_public_id": None})
guest = self.env["mail.guest"].create({"name": "Guest"})
channel.write({"channel_member_ids": [Command.create({"guest_id": guest.id})]})
with file_open("addons/web/__init__.py") as file:
response = self.url_open(
"/mail/attachment/upload",
{
"csrf_token": http.Request.csrf_token(self),
"thread_id": channel.id,
"thread_model": "discuss.channel",
},
files={"ufile": file},
cookies={guest._cookie_name: guest._format_auth_cookie()}
)
self.assertEqual(response.status_code, 200)

View file

@ -0,0 +1,48 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo import Command
from odoo.addons.base.tests.common import HttpCaseWithUserDemo, new_test_user
@odoo.tests.tagged('post_install', '-at_install')
class TestUi(HttpCaseWithUserDemo):
def test_01_mail_tour(self):
self.start_tour("/odoo", 'discuss_channel_tour', login="admin")
def test_02_mail_create_channel_no_mail_tour(self):
self.env['res.users'].create({
'email': '', # User should be able to create a channel even if no email is defined
'group_ids': [Command.set([self.ref('base.group_user')])],
'name': 'Test User',
'login': 'testuser',
'password': 'testuser',
})
self.start_tour("/odoo", 'discuss_channel_tour', login='testuser')
# basic rendering test of the configuration menu in Discuss
def test_03_mail_discuss_configuration_tour(self):
self.start_tour("/odoo", "discuss_configuration_tour", login="admin")
def test_04_meeting_view_tour(self):
bob = new_test_user(self.env, "bob", groups="base.group_user", email="bob@test.com")
john = new_test_user(self.env, "john", groups="base.group_user", email="john@test.com")
group_chat = (
self.env["discuss.channel"]
.with_user(bob)
._create_group(
partners_to=john.partner_id.ids, default_display_mode="video_full_screen"
)
)
self.authenticate("bob", "bob")
self.make_jsonrpc_request("/mail/rtc/channel/join_call", {"channel_id": group_chat.id})
self.start_tour(
f"/odoo/discuss?active_id=discuss.channel_{group_chat.id}&fullscreen=1",
"discuss.meeting_view_tour",
login="john",
)
self.start_tour(group_chat.invitation_url, "discuss.meeting_view_public_tour", login="john")
def test_05_can_create_channel_tour(self):
self.start_tour("odoo/discuss", "can_create_channel_from_form_view", login="demo")