diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py
index c559dfda83..04442febb4 100644
--- a/tests/rest/client/test_rooms.py
+++ b/tests/rest/client/test_rooms.py
@@ -4,7 +4,7 @@
# Copyright 2019 The Matrix.org Foundation C.I.C.
# Copyright 2017 Vector Creations Ltd
# Copyright 2014-2016 OpenMarket Ltd
-# Copyright (C) 2023 New Vector, Ltd
+# Copyright (C) 2023-2024 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -25,12 +25,11 @@
import json
from http import HTTPStatus
-from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
+from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, Union
from unittest.mock import AsyncMock, Mock, call, patch
from urllib import parse as urlparse
from parameterized import param, parameterized
-from typing_extensions import Literal
from twisted.test.proto_helpers import MemoryReactor
@@ -68,6 +67,7 @@ from tests.http.server._base import make_request_with_cancellation_test
from tests.storage.test_stream import PaginationTestCase
from tests.test_utils.event_injection import create_event
from tests.unittest import override_config
+from tests.utils import default_config
PATH_PREFIX = b"/_matrix/client/api/v1"
@@ -742,7 +742,7 @@ class RoomsCreateTestCase(RoomBase):
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
self.assertTrue("room_id" in channel.json_body)
assert channel.resource_usage is not None
- self.assertEqual(33, channel.resource_usage.db_txn_count)
+ self.assertEqual(35, channel.resource_usage.db_txn_count)
def test_post_room_initial_state(self) -> None:
# POST with initial_state config key, expect new room id
@@ -755,7 +755,7 @@ class RoomsCreateTestCase(RoomBase):
self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
self.assertTrue("room_id" in channel.json_body)
assert channel.resource_usage is not None
- self.assertEqual(35, channel.resource_usage.db_txn_count)
+ self.assertEqual(37, channel.resource_usage.db_txn_count)
def test_post_room_visibility_key(self) -> None:
# POST with visibility config key, expect new room id
@@ -1337,17 +1337,13 @@ class RoomJoinTestCase(RoomBase):
"POST", f"/join/{self.room1}", access_token=self.tok2
)
self.assertEqual(channel.code, 403)
- self.assertEqual(
- channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
- )
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
channel = self.make_request(
"POST", f"/rooms/{self.room1}/join", access_token=self.tok2
)
self.assertEqual(channel.code, 403)
- self.assertEqual(
- channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
- )
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
def test_suspended_user_cannot_knock_on_room(self) -> None:
# set the user as suspended
@@ -1361,9 +1357,7 @@ class RoomJoinTestCase(RoomBase):
shorthand=False,
)
self.assertEqual(channel.code, 403)
- self.assertEqual(
- channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
- )
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
def test_suspended_user_cannot_invite_to_room(self) -> None:
# set the user as suspended
@@ -1376,9 +1370,24 @@ class RoomJoinTestCase(RoomBase):
access_token=self.tok1,
content={"user_id": self.user2},
)
- self.assertEqual(
- channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
+
+ def test_suspended_user_can_leave_room(self) -> None:
+ channel = self.make_request(
+ "POST", f"/join/{self.room1}", access_token=self.tok1
)
+ self.assertEqual(channel.code, 200)
+
+ # set the user as suspended
+ self.get_success(self.store.set_user_suspended_status(self.user1, True))
+
+ # leave room
+ channel = self.make_request(
+ "POST",
+ f"/rooms/{self.room1}/leave",
+ access_token=self.tok1,
+ )
+ self.assertEqual(channel.code, 200)
class RoomAppserviceTsParamTestCase(unittest.HomeserverTestCase):
@@ -2291,6 +2300,141 @@ class RoomMessageFilterTestCase(RoomBase):
self.assertEqual(len(chunk), 2, [event["content"] for event in chunk])
+class RoomDelayedEventTestCase(RoomBase):
+ """Tests delayed events."""
+
+ user_id = "@sid1:red"
+
+ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
+ self.room_id = self.helper.create_room_as(self.user_id)
+
+ @unittest.override_config({"max_event_delay_duration": "24h"})
+ def test_send_delayed_invalid_event(self) -> None:
+ """Test sending a delayed event with invalid content."""
+ channel = self.make_request(
+ "PUT",
+ (
+ "rooms/%s/send/m.room.message/mid1?org.matrix.msc4140.delay=2000"
+ % self.room_id
+ ).encode("ascii"),
+ {},
+ )
+ self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, channel.result)
+ self.assertNotIn("org.matrix.msc4140.errcode", channel.json_body)
+
+ def test_delayed_event_unsupported_by_default(self) -> None:
+ """Test that sending a delayed event is unsupported with the default config."""
+ channel = self.make_request(
+ "PUT",
+ (
+ "rooms/%s/send/m.room.message/mid1?org.matrix.msc4140.delay=2000"
+ % self.room_id
+ ).encode("ascii"),
+ {"body": "test", "msgtype": "m.text"},
+ )
+ self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, channel.result)
+ self.assertEqual(
+ "M_MAX_DELAY_UNSUPPORTED",
+ channel.json_body.get("org.matrix.msc4140.errcode"),
+ channel.json_body,
+ )
+
+ @unittest.override_config({"max_event_delay_duration": "1000"})
+ def test_delayed_event_exceeds_max_delay(self) -> None:
+ """Test that sending a delayed event fails if its delay is longer than allowed."""
+ channel = self.make_request(
+ "PUT",
+ (
+ "rooms/%s/send/m.room.message/mid1?org.matrix.msc4140.delay=2000"
+ % self.room_id
+ ).encode("ascii"),
+ {"body": "test", "msgtype": "m.text"},
+ )
+ self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, channel.result)
+ self.assertEqual(
+ "M_MAX_DELAY_EXCEEDED",
+ channel.json_body.get("org.matrix.msc4140.errcode"),
+ channel.json_body,
+ )
+
+ @unittest.override_config({"max_event_delay_duration": "24h"})
+ def test_delayed_event_with_negative_delay(self) -> None:
+ """Test that sending a delayed event fails if its delay is negative."""
+ channel = self.make_request(
+ "PUT",
+ (
+ "rooms/%s/send/m.room.message/mid1?org.matrix.msc4140.delay=-2000"
+ % self.room_id
+ ).encode("ascii"),
+ {"body": "test", "msgtype": "m.text"},
+ )
+ self.assertEqual(HTTPStatus.BAD_REQUEST, channel.code, channel.result)
+ self.assertEqual(
+ Codes.INVALID_PARAM, channel.json_body["errcode"], channel.json_body
+ )
+
+ @unittest.override_config({"max_event_delay_duration": "24h"})
+ def test_send_delayed_message_event(self) -> None:
+ """Test sending a valid delayed message event."""
+ channel = self.make_request(
+ "PUT",
+ (
+ "rooms/%s/send/m.room.message/mid1?org.matrix.msc4140.delay=2000"
+ % self.room_id
+ ).encode("ascii"),
+ {"body": "test", "msgtype": "m.text"},
+ )
+ self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+
+ @unittest.override_config({"max_event_delay_duration": "24h"})
+ def test_send_delayed_state_event(self) -> None:
+ """Test sending a valid delayed state event."""
+ channel = self.make_request(
+ "PUT",
+ (
+ "rooms/%s/state/m.room.topic/?org.matrix.msc4140.delay=2000"
+ % self.room_id
+ ).encode("ascii"),
+ {"topic": "This is a topic"},
+ )
+ self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+
+ @unittest.override_config(
+ {
+ "max_event_delay_duration": "24h",
+ "rc_message": {"per_second": 1, "burst_count": 2},
+ }
+ )
+ def test_add_delayed_event_ratelimit(self) -> None:
+ """Test that requests to schedule new delayed events are ratelimited by a RateLimiter,
+ which ratelimits them correctly, including by not limiting when the requester is
+ exempt from ratelimiting.
+ """
+
+ # Test that new delayed events are correctly ratelimited.
+ args = (
+ "POST",
+ (
+ "rooms/%s/send/m.room.message?org.matrix.msc4140.delay=2000"
+ % self.room_id
+ ).encode("ascii"),
+ {"body": "test", "msgtype": "m.text"},
+ )
+ channel = self.make_request(*args)
+ self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+ channel = self.make_request(*args)
+ self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result)
+
+ # Add the current user to the ratelimit overrides, allowing them no ratelimiting.
+ self.get_success(
+ self.hs.get_datastores().main.set_ratelimit_for_user(self.user_id, 0, 0)
+ )
+
+ # Test that the new delayed events aren't ratelimited anymore.
+ channel = self.make_request(*args)
+ self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
+
+
class RoomSearchTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
@@ -2457,6 +2601,11 @@ class PublicRoomsRoomTypeFilterTestCase(unittest.HomeserverTestCase):
tok=self.token,
)
+ def default_config(self) -> JsonDict:
+ config = default_config("test")
+ config["room_list_publication_rules"] = [{"action": "allow"}]
+ return config
+
def make_public_rooms_request(
self,
room_types: Optional[List[Union[str, None]]],
@@ -2794,6 +2943,68 @@ class RoomMembershipReasonTestCase(unittest.HomeserverTestCase):
self.assertEqual(event_content.get("reason"), reason, channel.result)
+class RoomForgottenTestCase(unittest.HomeserverTestCase):
+ """
+ Test forget/forgotten rooms
+ """
+
+ servlets = [
+ synapse.rest.admin.register_servlets,
+ room.register_servlets,
+ login.register_servlets,
+ ]
+
+ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
+ self.store = hs.get_datastores().main
+
+ def test_room_not_forgotten_after_unban(self) -> None:
+ """
+ Test what happens when someone is banned from a room, they forget the room, and
+ some time later are unbanned.
+
+ Currently, when they are unbanned, the room isn't forgotten anymore which may or
+ may not be expected.
+ """
+ user1_id = self.register_user("user1", "pass")
+ user1_tok = self.login(user1_id, "pass")
+ user2_id = self.register_user("user2", "pass")
+ user2_tok = self.login(user2_id, "pass")
+
+ room_id = self.helper.create_room_as(user2_id, tok=user2_tok, is_public=True)
+ self.helper.join(room_id, user1_id, tok=user1_tok)
+
+ # User1 is banned and forgets the room
+ self.helper.ban(room_id, src=user2_id, targ=user1_id, tok=user2_tok)
+ # User1 forgets the room
+ self.get_success(self.store.forget(user1_id, room_id))
+
+ # The room should show up as forgotten
+ forgotten_room_ids = self.get_success(
+ self.store.get_forgotten_rooms_for_user(user1_id)
+ )
+ self.assertIncludes(forgotten_room_ids, {room_id}, exact=True)
+
+ # Unban user1
+ self.helper.change_membership(
+ room=room_id,
+ src=user2_id,
+ targ=user1_id,
+ membership=Membership.LEAVE,
+ tok=user2_tok,
+ )
+
+ # Room is no longer forgotten because it's a new membership
+ #
+ # XXX: Is this how we actually want it to behave? It seems like ideally, the
+ # room forgotten status should only be reset when the user decides to join again
+ # (or is invited/knocks). This way the room remains forgotten for any ban/leave
+ # transitions.
+ forgotten_room_ids = self.get_success(
+ self.store.get_forgotten_rooms_for_user(user1_id)
+ )
+ self.assertIncludes(forgotten_room_ids, set(), exact=True)
+
+
class LabelsTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
@@ -3577,191 +3788,6 @@ class RoomCanonicalAliasTestCase(unittest.HomeserverTestCase):
self._set_canonical_alias({"alt_aliases": ["@unknown:test"]}, expected_code=400)
-class ThreepidInviteTestCase(unittest.HomeserverTestCase):
- servlets = [
- admin.register_servlets,
- login.register_servlets,
- room.register_servlets,
- ]
-
- def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
- self.user_id = self.register_user("thomas", "hackme")
- self.tok = self.login("thomas", "hackme")
-
- self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
-
- def test_threepid_invite_spamcheck_deprecated(self) -> None:
- """
- Test allowing/blocking threepid invites with a spam-check module.
-
- In this test, we use the deprecated API in which callbacks return a bool.
- """
- # Mock a few functions to prevent the test from failing due to failing to talk to
- # a remote IS. We keep the mock for make_and_store_3pid_invite around so we
- # can check its call_count later on during the test.
- make_invite_mock = AsyncMock(return_value=(Mock(event_id="abc"), 0))
- self.hs.get_room_member_handler()._make_and_store_3pid_invite = make_invite_mock # type: ignore[method-assign]
- self.hs.get_identity_handler().lookup_3pid = AsyncMock( # type: ignore[method-assign]
- return_value=None,
- )
-
- # Add a mock to the spamchecker callbacks for user_may_send_3pid_invite. Make it
- # allow everything for now.
- # `spec` argument is needed for this function mock to have `__qualname__`, which
- # is needed for `Measure` metrics buried in SpamChecker.
- mock = AsyncMock(return_value=True, spec=lambda *x: None)
- self.hs.get_module_api_callbacks().spam_checker._user_may_send_3pid_invite_callbacks.append(
- mock
- )
-
- # Send a 3PID invite into the room and check that it succeeded.
- email_to_invite = "teresa@example.com"
- channel = self.make_request(
- method="POST",
- path="/rooms/" + self.room_id + "/invite",
- content={
- "id_server": "example.com",
- "id_access_token": "sometoken",
- "medium": "email",
- "address": email_to_invite,
- },
- access_token=self.tok,
- )
- self.assertEqual(channel.code, 200)
-
- # Check that the callback was called with the right params.
- mock.assert_called_with(self.user_id, "email", email_to_invite, self.room_id)
-
- # Check that the call to send the invite was made.
- make_invite_mock.assert_called_once()
-
- # Now change the return value of the callback to deny any invite and test that
- # we can't send the invite.
- mock.return_value = False
- channel = self.make_request(
- method="POST",
- path="/rooms/" + self.room_id + "/invite",
- content={
- "id_server": "example.com",
- "id_access_token": "sometoken",
- "medium": "email",
- "address": email_to_invite,
- },
- access_token=self.tok,
- )
- self.assertEqual(channel.code, 403)
-
- # Also check that it stopped before calling _make_and_store_3pid_invite.
- make_invite_mock.assert_called_once()
-
- def test_threepid_invite_spamcheck(self) -> None:
- """
- Test allowing/blocking threepid invites with a spam-check module.
-
- In this test, we use the more recent API in which callbacks return a `Union[Codes, Literal["NOT_SPAM"]]`.
- """
- # Mock a few functions to prevent the test from failing due to failing to talk to
- # a remote IS. We keep the mock for make_and_store_3pid_invite around so we
- # can check its call_count later on during the test.
- make_invite_mock = AsyncMock(return_value=(Mock(event_id="abc"), 0))
- self.hs.get_room_member_handler()._make_and_store_3pid_invite = make_invite_mock # type: ignore[method-assign]
- self.hs.get_identity_handler().lookup_3pid = AsyncMock( # type: ignore[method-assign]
- return_value=None,
- )
-
- # Add a mock to the spamchecker callbacks for user_may_send_3pid_invite. Make it
- # allow everything for now.
- # `spec` argument is needed for this function mock to have `__qualname__`, which
- # is needed for `Measure` metrics buried in SpamChecker.
- mock = AsyncMock(
- return_value=synapse.module_api.NOT_SPAM,
- spec=lambda *x: None,
- )
- self.hs.get_module_api_callbacks().spam_checker._user_may_send_3pid_invite_callbacks.append(
- mock
- )
-
- # Send a 3PID invite into the room and check that it succeeded.
- email_to_invite = "teresa@example.com"
- channel = self.make_request(
- method="POST",
- path="/rooms/" + self.room_id + "/invite",
- content={
- "id_server": "example.com",
- "id_access_token": "sometoken",
- "medium": "email",
- "address": email_to_invite,
- },
- access_token=self.tok,
- )
- self.assertEqual(channel.code, 200)
-
- # Check that the callback was called with the right params.
- mock.assert_called_with(self.user_id, "email", email_to_invite, self.room_id)
-
- # Check that the call to send the invite was made.
- make_invite_mock.assert_called_once()
-
- # Now change the return value of the callback to deny any invite and test that
- # we can't send the invite. We pick an arbitrary error code to be able to check
- # that the same code has been returned
- mock.return_value = Codes.CONSENT_NOT_GIVEN
- channel = self.make_request(
- method="POST",
- path="/rooms/" + self.room_id + "/invite",
- content={
- "id_server": "example.com",
- "id_access_token": "sometoken",
- "medium": "email",
- "address": email_to_invite,
- },
- access_token=self.tok,
- )
- self.assertEqual(channel.code, 403)
- self.assertEqual(channel.json_body["errcode"], Codes.CONSENT_NOT_GIVEN)
-
- # Also check that it stopped before calling _make_and_store_3pid_invite.
- make_invite_mock.assert_called_once()
-
- # Run variant with `Tuple[Codes, dict]`.
- mock.return_value = (Codes.EXPIRED_ACCOUNT, {"field": "value"})
- channel = self.make_request(
- method="POST",
- path="/rooms/" + self.room_id + "/invite",
- content={
- "id_server": "example.com",
- "id_access_token": "sometoken",
- "medium": "email",
- "address": email_to_invite,
- },
- access_token=self.tok,
- )
- self.assertEqual(channel.code, 403)
- self.assertEqual(channel.json_body["errcode"], Codes.EXPIRED_ACCOUNT)
- self.assertEqual(channel.json_body["field"], "value")
-
- # Also check that it stopped before calling _make_and_store_3pid_invite.
- make_invite_mock.assert_called_once()
-
- def test_400_missing_param_without_id_access_token(self) -> None:
- """
- Test that a 3pid invite request returns 400 M_MISSING_PARAM
- if we do not include id_access_token.
- """
- channel = self.make_request(
- method="POST",
- path="/rooms/" + self.room_id + "/invite",
- content={
- "id_server": "example.com",
- "medium": "email",
- "address": "teresa@example.com",
- },
- access_token=self.tok,
- )
- self.assertEqual(channel.code, 400)
- self.assertEqual(channel.json_body["errcode"], "M_MISSING_PARAM")
-
-
class TimestampLookupTestCase(unittest.HomeserverTestCase):
servlets = [
admin.register_servlets,
@@ -3836,10 +3862,25 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
self.user2 = self.register_user("teresa", "hackme")
self.tok2 = self.login("teresa", "hackme")
- self.room1 = self.helper.create_room_as(room_creator=self.user1, tok=self.tok1)
+ self.admin = self.register_user("admin", "pass", True)
+ self.admin_tok = self.login("admin", "pass")
+
+ self.room1 = self.helper.create_room_as(
+ room_creator=self.user1, tok=self.tok1, room_version="11"
+ )
self.store = hs.get_datastores().main
- def test_suspended_user_cannot_send_message_to_room(self) -> None:
+ self.room2 = self.helper.create_room_as(
+ room_creator=self.user1, is_public=False, tok=self.tok1
+ )
+ self.helper.send_state(
+ self.room2,
+ EventTypes.RoomEncryption,
+ {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
+ tok=self.tok1,
+ )
+
+ def test_suspended_user_cannot_send_message_to_public_room(self) -> None:
# set the user as suspended
self.get_success(self.store.set_user_suspended_status(self.user1, True))
@@ -3849,9 +3890,25 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
access_token=self.tok1,
content={"body": "hello", "msgtype": "m.text"},
)
- self.assertEqual(
- channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
+
+ def test_suspended_user_cannot_send_message_to_encrypted_room(self) -> None:
+ channel = self.make_request(
+ "PUT",
+ f"/_synapse/admin/v1/suspend/{self.user1}",
+ {"suspend": True},
+ access_token=self.admin_tok,
+ )
+ self.assertEqual(channel.code, 200)
+ self.assertEqual(channel.json_body, {f"user_{self.user1}_suspended": True})
+
+ channel = self.make_request(
+ "PUT",
+ f"/rooms/{self.room2}/send/m.room.encrypted/1",
+ access_token=self.tok1,
+ content={},
)
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
def test_suspended_user_cannot_change_profile_data(self) -> None:
# set the user as suspended
@@ -3864,9 +3921,7 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
content={"avatar_url": "mxc://matrix.org/wefh34uihSDRGhw34"},
shorthand=False,
)
- self.assertEqual(
- channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
- )
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
channel2 = self.make_request(
"PUT",
@@ -3875,9 +3930,7 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
content={"displayname": "something offensive"},
shorthand=False,
)
- self.assertEqual(
- channel2.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
- )
+ self.assertEqual(channel2.json_body["errcode"], "M_USER_SUSPENDED")
def test_suspended_user_cannot_redact_messages_other_than_their_own(self) -> None:
# first user sends message
@@ -3911,9 +3964,7 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
content={"reason": "bogus"},
shorthand=False,
)
- self.assertEqual(
- channel.json_body["errcode"], "ORG.MATRIX.MSC3823.USER_ACCOUNT_SUSPENDED"
- )
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
# but can redact their own
channel = self.make_request(
@@ -3924,3 +3975,244 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
shorthand=False,
)
self.assertEqual(channel.code, 200)
+
+ channel = self.make_request(
+ "PUT",
+ f"/_matrix/client/v3/rooms/{self.room1}/send/m.room.redaction/3456346",
+ access_token=self.tok1,
+ content={"reason": "bogus", "redacts": event_id},
+ shorthand=False,
+ )
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
+
+ channel = self.make_request(
+ "PUT",
+ f"/_matrix/client/v3/rooms/{self.room1}/send/m.room.redaction/3456346",
+ access_token=self.tok1,
+ content={"reason": "bogus", "redacts": event_id2},
+ shorthand=False,
+ )
+ self.assertEqual(channel.code, 200)
+
+ def test_suspended_user_cannot_ban_others(self) -> None:
+ # user to ban joins room user1 created
+ self.make_request("POST", f"/rooms/{self.room1}/join", access_token=self.tok2)
+
+ # suspend user1
+ self.get_success(self.store.set_user_suspended_status(self.user1, True))
+
+ # user1 tries to ban other user while suspended
+ channel = self.make_request(
+ "POST",
+ f"/_matrix/client/v3/rooms/{self.room1}/ban",
+ access_token=self.tok1,
+ content={"reason": "spite", "user_id": self.user2},
+ shorthand=False,
+ )
+ self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
+
+ # un-suspend user1
+ self.get_success(self.store.set_user_suspended_status(self.user1, False))
+
+ # ban now goes through
+ channel = self.make_request(
+ "POST",
+ f"/_matrix/client/v3/rooms/{self.room1}/ban",
+ access_token=self.tok1,
+ content={"reason": "spite", "user_id": self.user2},
+ shorthand=False,
+ )
+ self.assertEqual(channel.code, 200)
+
+
+class RoomParticipantTestCase(unittest.HomeserverTestCase):
+ servlets = [
+ login.register_servlets,
+ room.register_servlets,
+ profile.register_servlets,
+ admin.register_servlets,
+ ]
+
+ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
+ self.user1 = self.register_user("thomas", "hackme")
+ self.tok1 = self.login("thomas", "hackme")
+
+ self.user2 = self.register_user("teresa", "hackme")
+ self.tok2 = self.login("teresa", "hackme")
+
+ self.room1 = self.helper.create_room_as(
+ room_creator=self.user1,
+ tok=self.tok1,
+ # Allow user2 to send state events into the room.
+ extra_content={
+ "power_level_content_override": {
+ "state_default": 0,
+ },
+ },
+ )
+ self.store = hs.get_datastores().main
+
+ @parameterized.expand(
+ [
+ # Should record participation.
+ param(
+ is_state=False,
+ event_type="m.room.message",
+ event_content={
+ "msgtype": "m.text",
+ "body": "I am engaging in this room",
+ },
+ record_participation=True,
+ ),
+ param(
+ is_state=False,
+ event_type="m.room.encrypted",
+ event_content={
+ "algorithm": "m.megolm.v1.aes-sha2",
+ "ciphertext": "AwgAEnACgAkLmt6qF84IK++J7UDH2Za1YVchHyprqTqsg...",
+ "device_id": "RJYKSTBOIE",
+ "sender_key": "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
+ "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
+ },
+ record_participation=True,
+ ),
+ # Should not record participation.
+ param(
+ is_state=False,
+ event_type="m.sticker",
+ event_content={
+ "body": "My great sticker",
+ "info": {},
+ "url": "mxc://unused/mxcurl",
+ },
+ record_participation=False,
+ ),
+ # An invalid **state event** with type `m.room.message`
+ param(
+ is_state=True,
+ event_type="m.room.message",
+ event_content={
+ "msgtype": "m.text",
+ "body": "I am engaging in this room",
+ },
+ record_participation=False,
+ ),
+ # An invalid **state event** with type `m.room.encrypted`
+ # Note: this may become valid in the future with encrypted state, though we
+ # still may not want to consider it grounds for marking a user as participating.
+ param(
+ is_state=True,
+ event_type="m.room.encrypted",
+ event_content={
+ "algorithm": "m.megolm.v1.aes-sha2",
+ "ciphertext": "AwgAEnACgAkLmt6qF84IK++J7UDH2Za1YVchHyprqTqsg...",
+ "device_id": "RJYKSTBOIE",
+ "sender_key": "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
+ "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
+ },
+ record_participation=False,
+ ),
+ ]
+ )
+ def test_sending_message_records_participation(
+ self,
+ is_state: bool,
+ event_type: str,
+ event_content: JsonDict,
+ record_participation: bool,
+ ) -> None:
+ """
+ Test that sending an various events into a room causes the user to
+ appropriately marked or not marked as a participant in that room.
+ """
+ self.helper.join(self.room1, self.user2, tok=self.tok2)
+
+ # user has not sent any messages, so should not be a participant
+ participant = self.get_success(
+ self.store.get_room_participation(self.user2, self.room1)
+ )
+ self.assertFalse(participant)
+
+ # send an event into the room
+ if is_state:
+ # send a state event
+ self.helper.send_state(
+ self.room1,
+ event_type,
+ body=event_content,
+ tok=self.tok2,
+ )
+ else:
+ # send a non-state event
+ self.helper.send_event(
+ self.room1,
+ event_type,
+ content=event_content,
+ tok=self.tok2,
+ )
+
+ # check whether the user has been marked as a participant
+ participant = self.get_success(
+ self.store.get_room_participation(self.user2, self.room1)
+ )
+ self.assertEqual(participant, record_participation)
+
+ @parameterized.expand(
+ [
+ param(
+ event_type="m.room.message",
+ event_content={
+ "msgtype": "m.text",
+ "body": "I am engaging in this room",
+ },
+ ),
+ param(
+ event_type="m.room.encrypted",
+ event_content={
+ "algorithm": "m.megolm.v1.aes-sha2",
+ "ciphertext": "AwgAEnACgAkLmt6qF84IK++J7UDH2Za1YVchHyprqTqsg...",
+ "device_id": "RJYKSTBOIE",
+ "sender_key": "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
+ "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
+ },
+ ),
+ ]
+ )
+ def test_sending_event_and_leaving_does_not_record_participation(
+ self,
+ event_type: str,
+ event_content: JsonDict,
+ ) -> None:
+ """
+ Test that sending an event into a room that should mark a user as a
+ participant, but then leaving the room, results in the user no longer
+ be marked as a participant in that room.
+ """
+ self.helper.join(self.room1, self.user2, tok=self.tok2)
+
+ # user has not sent any messages, so should not be a participant
+ participant = self.get_success(
+ self.store.get_room_participation(self.user2, self.room1)
+ )
+ self.assertFalse(participant)
+
+ # sending a message should now mark user as participant
+ self.helper.send_event(
+ self.room1,
+ event_type,
+ content=event_content,
+ tok=self.tok2,
+ )
+ participant = self.get_success(
+ self.store.get_room_participation(self.user2, self.room1)
+ )
+ self.assertTrue(participant)
+
+ # leave the room
+ self.helper.leave(self.room1, self.user2, tok=self.tok2)
+
+ # user should no longer be considered a participant
+ participant = self.get_success(
+ self.store.get_room_participation(self.user2, self.room1)
+ )
+ self.assertFalse(participant)
|