From 423e767856e78f330434a4371ac6d512b30b9f8a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 23 Sep 2021 15:31:28 +0100 Subject: [PATCH 1/8] Add a spamchecker method to allow or deny 3pid invites --- docs/modules/spam_checker_callbacks.md | 35 +++++++ synapse/events/spamcheck.py | 41 ++++++++ synapse/handlers/room_member.py | 11 ++ tests/rest/client/test_rooms.py | 137 ++++++++++++++++++++++++- 4 files changed, 222 insertions(+), 2 deletions(-) diff --git a/docs/modules/spam_checker_callbacks.md b/docs/modules/spam_checker_callbacks.md index 81574a015cf4..8b2fac4f1fa3 100644 --- a/docs/modules/spam_checker_callbacks.md +++ b/docs/modules/spam_checker_callbacks.md @@ -29,6 +29,41 @@ Called when processing an invitation. The module must return a `bool` indicating the inviter can invite the invitee to the given room. Both inviter and invitee are represented by their Matrix user ID (e.g. `@alice:example.com`). +### `user_may_send_threepid_invite` + +```python +async def user_may_send_3pid_invite( + inviter: str, + invitee: Dict[str, str], + room_id: str, +) -> bool +``` + +Called when processing an invitation using a third-party identifier (also called a 3PID, +e.g. an email address or a phone number). The module must return a `bool` indicating +whether the inviter can invite the invitee to the given room. + +The inviter is represented by their Matrix user ID (e.g. `@alice:example.com`), and the +invitee is represented by a dict describing the third-party identifier to send an +invitation to, with a `medium` key indicating the identifier's medium (e.g. "email") and +an `address` key indicating the identifier's address (e.g. `alice@example.com`). See +[the Matrix specification](https://matrix.org/docs/spec/appendices#pid-types) for more +information regarding third-party identifiers. + +For example, a call to this callback to send an invitation to the email address +`alice@example.com` would look like this: + +```python +await user_may_send_3pid_invite( + "@bob:example.com", # The inviter's user ID + {"medium": "email", "address": "alice@example.com"}, # The 3PID to invite + "!some_room:example.com", # The ID of the room to send the invite into +) +``` + +**Note**: If the third-party identifier is already associated with a matrix user ID, +[`user_may_invite`](#user_may_invite) will be used instead. + ### `user_may_create_room` ```python diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index 57f1d53fa863..fd2b8fb47d48 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -45,6 +45,10 @@ Awaitable[Union[bool, str]], ] USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]] +USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ + [str, Dict[str, str], str], + Awaitable[bool] +] USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]] USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]] USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]] @@ -163,6 +167,9 @@ class SpamChecker: def __init__(self): self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = [] self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = [] + self._user_may_send_3pid_invite_callbacks: List[ + USER_MAY_SEND_3PID_INVITE_CALLBACK + ] = [] self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = [] self._user_may_create_room_alias_callbacks: List[ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK @@ -182,6 +189,9 @@ def register_callbacks( self, check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None, user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None, + user_may_send_3pid_invite: Optional[ + USER_MAY_SEND_3PID_INVITE_CALLBACK + ] = None, user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None, user_may_create_room_alias: Optional[ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK @@ -200,6 +210,11 @@ def register_callbacks( if user_may_invite is not None: self._user_may_invite_callbacks.append(user_may_invite) + if user_may_send_3pid_invite is not None: + self._user_may_send_3pid_invite_callbacks.append( + user_may_send_3pid_invite, + ) + if user_may_create_room is not None: self._user_may_create_room_callbacks.append(user_may_create_room) @@ -266,6 +281,32 @@ async def user_may_invite( return True + async def user_may_send_3pid_invite( + self, inviter_userid: str, invitee_threepid: Dict[str, str], room_id: str + ) -> bool: + """Checks if a given user may invite a given threepid into the room + + If this method returns false, the threepid invite will be rejected. + + Note that if the threepid is already associated with a Matrix user ID, Synapse + will call user_may_invite with said user ID instead. + + Args: + inviter_userid: The user ID of the sender of the invitation + invitee_threepid: The threepid targeted in the invitation, as a dict including + a "medium" key indicating the threepid's medium (e.g. "email") and an + "address" key indicating the threepid's address (e.g. "alice@example.com") + room_id: The room ID + + Returns: + True if the user may send the invite, otherwise False + """ + for callback in self._user_may_send_3pid_invite_callbacks: + if await callback(inviter_userid, invitee_threepid, room_id) is False: + return False + + return True + async def user_may_create_room(self, userid: str) -> bool: """Checks if a given user may create a room diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 7bb3f0bc47d7..5d4340831ee0 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -1260,10 +1260,21 @@ async def do_3pid_invite( if invitee: # Note that update_membership with an action of "invite" can raise # a ShadowBanError, but this was done above already. + # We don't check the invite against the spamchecker(s) here (through + # user_may_invite) because we'll do it further down the line anyway (in + # update_membership_locked). _, stream_id = await self.update_membership( requester, UserID.from_string(invitee), room_id, "invite", txn_id=txn_id ) else: + # Check if the spamchecker(s) allow this invite to go through. + if not await self.spam_checker.user_may_send_3pid_invite( + inviter_userid=requester.user.to_string(), + invitee_threepid={"medium": medium, "address": address}, + room_id=room_id, + ): + raise SynapseError(403, "Cannot send threepid invite") + stream_id = await self._make_and_store_3pid_invite( requester, id_server, diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index ef847f0f5ff9..1271575f17e9 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -18,7 +18,7 @@ """Tests REST events for /rooms paths.""" import json -from typing import Iterable +from typing import Iterable, Optional, Dict from unittest.mock import Mock, call from urllib import parse as urlparse @@ -30,7 +30,7 @@ from synapse.handlers.pagination import PurgeStatus from synapse.rest import admin from synapse.rest.client import account, directory, login, profile, room, sync -from synapse.types import JsonDict, RoomAlias, UserID, create_requester +from synapse.types import JsonDict, RoomAlias, UserID, create_requester, Requester from synapse.util.stringutils import random_string from tests import unittest @@ -2315,3 +2315,136 @@ def test_bad_alias(self): """An alias which does not point to the room raises a SynapseError.""" self._set_canonical_alias({"alias": "@unknown:test"}, expected_code=400) 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, clock, homeserver): + 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(self): + # Mock a few functions to prevent the test from failing due to failing to talk to + # a remote IS. We keep the mock for _mock_make_and_store_3pid_invite around so we + # can check its call_count later on during the test. + make_invite_mock = self._mock_make_and_store_3pid_invite() + self._mock_lookup_3pid() + + # Add a mock to the spamchecker callbacks for user_may_send_3pid_invite. Make it + # allow everything for now. + return_value = True + + async def _user_may_send_3pid_invite( + inviter: str, + invitee: Dict[str, str], + room_id: str, + ) -> bool: + return return_value + + allow_mock = Mock(side_effect=_user_may_send_3pid_invite) + + self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append(allow_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.assertEquals(channel.code, 200) + + # Check that the callback was called with the right params. + expected_call_args = ( + ( + self.user_id, + {"medium": "email", "address": email_to_invite}, + self.room_id, + ), + ) + + self.assertEquals(allow_mock.call_args, expected_call_args, allow_mock.call_args) + + # Check that the call to send the invite was made. + self.assertEquals(make_invite_mock.call_count, 1) + + # Now change the return value of the callback to deny any invite and test that + # we can't send the invite. + 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.assertEquals(channel.code, 403) + + # Also check that it stopped before calling _make_and_store_3pid_invite. + self.assertEquals(make_invite_mock.call_count, 1) + + def _mock_make_and_store_3pid_invite(self) -> Mock: + """Mocks RoomMemberHandler._make_and_store_3pid_invite with a function that just + returns the integer 0. + + Returns: + The Mock object _make_and_store_3pid_invite was replaced with. + """ + async def _make_and_store_3pid_invite( + requester: Requester, + id_server: str, + medium: str, + address: str, + room_id: str, + user: UserID, + txn_id: Optional[str], + id_access_token: Optional[str] = None, + ) -> int: + return 0 + + mock = Mock(side_effect=_make_and_store_3pid_invite) + + self.hs.get_room_member_handler()._make_and_store_3pid_invite = mock + + return mock + + def _mock_lookup_3pid(self) -> Mock: + """Mocks IdentityHandler.lookup_3pid with a function that just returns None (ie + no binding for the 3PID. + + Returns: + The Mock object lookup_3pid was replaced with. + """ + async def _lookup_3pid( + id_server: str, + medium: str, + address: str, + id_access_token: Optional[str] = None, + ) -> Optional[str]: + return None + + mock = Mock(side_effect=_lookup_3pid) + + self.hs.get_identity_handler().lookup_3pid = mock + + return mock + + From d548b4ffa4dc28cab618a74e73df7108f44c3c94 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 23 Sep 2021 16:09:00 +0100 Subject: [PATCH 2/8] Changelog --- changelog.d/10894.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10894.feature diff --git a/changelog.d/10894.feature b/changelog.d/10894.feature new file mode 100644 index 000000000000..e29d9dde8ad8 --- /dev/null +++ b/changelog.d/10894.feature @@ -0,0 +1 @@ +Add a `user_may_send_3pid_invite` spam checker callback for modules. From 78c211d7029a043238e882f611547431bbb404e5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 23 Sep 2021 16:12:38 +0100 Subject: [PATCH 3/8] Make changelog more explicit --- changelog.d/10894.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/10894.feature b/changelog.d/10894.feature index e29d9dde8ad8..a4f968bed100 100644 --- a/changelog.d/10894.feature +++ b/changelog.d/10894.feature @@ -1 +1 @@ -Add a `user_may_send_3pid_invite` spam checker callback for modules. +Add a `user_may_send_3pid_invite` spam checker callback for modules to allow or deny 3PID invites. From 897ef5fe0ed3fe091fb28fb2e90eebd643e52095 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 23 Sep 2021 18:37:43 +0100 Subject: [PATCH 4/8] Lint --- synapse/events/spamcheck.py | 7 ++----- tests/rest/client/test_rooms.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index fd2b8fb47d48..7a8821ce4f60 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -46,8 +46,7 @@ ] USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]] USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ - [str, Dict[str, str], str], - Awaitable[bool] + [str, Dict[str, str], str], Awaitable[bool] ] USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]] USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]] @@ -189,9 +188,7 @@ def register_callbacks( self, check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None, user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None, - user_may_send_3pid_invite: Optional[ - USER_MAY_SEND_3PID_INVITE_CALLBACK - ] = None, + user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None, user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None, user_may_create_room_alias: Optional[ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index 1271575f17e9..c3b25232db10 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -18,7 +18,7 @@ """Tests REST events for /rooms paths.""" import json -from typing import Iterable, Optional, Dict +from typing import Dict, Iterable, Optional from unittest.mock import Mock, call from urllib import parse as urlparse @@ -30,7 +30,7 @@ from synapse.handlers.pagination import PurgeStatus from synapse.rest import admin from synapse.rest.client import account, directory, login, profile, room, sync -from synapse.types import JsonDict, RoomAlias, UserID, create_requester, Requester +from synapse.types import JsonDict, Requester, RoomAlias, UserID, create_requester from synapse.util.stringutils import random_string from tests import unittest @@ -2351,7 +2351,9 @@ async def _user_may_send_3pid_invite( allow_mock = Mock(side_effect=_user_may_send_3pid_invite) - self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append(allow_mock) + self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append( + allow_mock + ) # Send a 3PID invite into the room and check that it succeeded. email_to_invite = "teresa@example.com" @@ -2377,7 +2379,9 @@ async def _user_may_send_3pid_invite( ), ) - self.assertEquals(allow_mock.call_args, expected_call_args, allow_mock.call_args) + self.assertEquals( + allow_mock.call_args, expected_call_args, allow_mock.call_args + ) # Check that the call to send the invite was made. self.assertEquals(make_invite_mock.call_count, 1) @@ -2408,6 +2412,7 @@ def _mock_make_and_store_3pid_invite(self) -> Mock: Returns: The Mock object _make_and_store_3pid_invite was replaced with. """ + async def _make_and_store_3pid_invite( requester: Requester, id_server: str, @@ -2433,6 +2438,7 @@ def _mock_lookup_3pid(self) -> Mock: Returns: The Mock object lookup_3pid was replaced with. """ + async def _lookup_3pid( id_server: str, medium: str, @@ -2446,5 +2452,3 @@ async def _lookup_3pid( self.hs.get_identity_handler().lookup_3pid = mock return mock - - From 949bcf7928e9bd97743158efbb7e65a2e5663f84 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 27 Sep 2021 17:34:02 +0100 Subject: [PATCH 5/8] Incorporate comment --- docs/modules/spam_checker_callbacks.md | 16 ++++++++-------- synapse/events/spamcheck.py | 11 +++++------ synapse/handlers/room_member.py | 3 ++- tests/rest/client/test_rooms.py | 11 +++-------- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/docs/modules/spam_checker_callbacks.md b/docs/modules/spam_checker_callbacks.md index 33475904392e..61faf42efc49 100644 --- a/docs/modules/spam_checker_callbacks.md +++ b/docs/modules/spam_checker_callbacks.md @@ -29,12 +29,13 @@ Called when processing an invitation. The module must return a `bool` indicating the inviter can invite the invitee to the given room. Both inviter and invitee are represented by their Matrix user ID (e.g. `@alice:example.com`). -### `user_may_send_threepid_invite` +### `user_may_send_3pid_invite` ```python async def user_may_send_3pid_invite( inviter: str, - invitee: Dict[str, str], + medium: str, + address: str, room_id: str, ) -> bool ``` @@ -44,11 +45,9 @@ e.g. an email address or a phone number). The module must return a `bool` indica whether the inviter can invite the invitee to the given room. The inviter is represented by their Matrix user ID (e.g. `@alice:example.com`), and the -invitee is represented by a dict describing the third-party identifier to send an -invitation to, with a `medium` key indicating the identifier's medium (e.g. "email") and -an `address` key indicating the identifier's address (e.g. `alice@example.com`). See -[the Matrix specification](https://matrix.org/docs/spec/appendices#pid-types) for more -information regarding third-party identifiers. +invitee is represented by its medium (e.g. "email") and its address +(e.g. `alice@example.com`). See [the Matrix specification](https://matrix.org/docs/spec/appendices#pid-types) +for more information regarding third-party identifiers. For example, a call to this callback to send an invitation to the email address `alice@example.com` would look like this: @@ -56,7 +55,8 @@ For example, a call to this callback to send an invitation to the email address ```python await user_may_send_3pid_invite( "@bob:example.com", # The inviter's user ID - {"medium": "email", "address": "alice@example.com"}, # The 3PID to invite + "email", # The medium of the 3PID to invite + "alice@example.com", # The address of the 3PID to invite "!some_room:example.com", # The ID of the room to send the invite into ) ``` diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index b5313678f509..c1a374e43bd9 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -46,7 +46,7 @@ ] USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]] USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ - [str, Dict[str, str], str], Awaitable[bool] + [str, str, str, str], Awaitable[bool] ] USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]] USER_MAY_CREATE_ROOM_WITH_INVITES_CALLBACK = Callable[ @@ -293,7 +293,7 @@ async def user_may_invite( return True async def user_may_send_3pid_invite( - self, inviter_userid: str, invitee_threepid: Dict[str, str], room_id: str + self, inviter_userid: str, medium: str, address: str, room_id: str ) -> bool: """Checks if a given user may invite a given threepid into the room @@ -304,16 +304,15 @@ async def user_may_send_3pid_invite( Args: inviter_userid: The user ID of the sender of the invitation - invitee_threepid: The threepid targeted in the invitation, as a dict including - a "medium" key indicating the threepid's medium (e.g. "email") and an - "address" key indicating the threepid's address (e.g. "alice@example.com") + medium: The 3PID's medium (e.g. "email") + address: The 3PID's address (e.g. "alice@example.com") room_id: The room ID Returns: True if the user may send the invite, otherwise False """ for callback in self._user_may_send_3pid_invite_callbacks: - if await callback(inviter_userid, invitee_threepid, room_id) is False: + if await callback(inviter_userid, medium, address, room_id) is False: return False return True diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 877f2a22b746..437e46514b18 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -1270,7 +1270,8 @@ async def do_3pid_invite( # Check if the spamchecker(s) allow this invite to go through. if not await self.spam_checker.user_may_send_3pid_invite( inviter_userid=requester.user.to_string(), - invitee_threepid={"medium": medium, "address": address}, + medium=medium, + address=address, room_id=room_id, ): raise SynapseError(403, "Cannot send threepid invite") diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index 4a659dc654c6..a6b858c6f87a 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -2459,7 +2459,8 @@ def test_threepid_invite_spamcheck(self): async def _user_may_send_3pid_invite( inviter: str, - invitee: Dict[str, str], + medium: str, + address: str, room_id: str, ) -> bool: return return_value @@ -2486,13 +2487,7 @@ async def _user_may_send_3pid_invite( self.assertEquals(channel.code, 200) # Check that the callback was called with the right params. - expected_call_args = ( - ( - self.user_id, - {"medium": "email", "address": email_to_invite}, - self.room_id, - ), - ) + expected_call_args = ((self.user_id, "email", email_to_invite, self.room_id),) self.assertEquals( allow_mock.call_args, expected_call_args, allow_mock.call_args From a82e14e03e5378dca52368be3d37b450824439fc Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 27 Sep 2021 17:40:44 +0100 Subject: [PATCH 6/8] Lint --- synapse/events/spamcheck.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index c1a374e43bd9..cd14c00ab396 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -45,9 +45,7 @@ Awaitable[Union[bool, str]], ] USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]] -USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ - [str, str, str, str], Awaitable[bool] -] +USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[[str, str, str, str], Awaitable[bool]] USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]] USER_MAY_CREATE_ROOM_WITH_INVITES_CALLBACK = Callable[ [str, List[str], List[Dict[str, str]]], Awaitable[bool] From 8785f5b39b2199cee1e9282dcf45dac2a9f0338b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 28 Sep 2021 10:45:34 +0100 Subject: [PATCH 7/8] Add simplifications from review --- tests/rest/client/test_rooms.py | 78 ++++----------------------------- 1 file changed, 9 insertions(+), 69 deletions(-) diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index a6b858c6f87a..06e58b2fa975 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -2450,26 +2450,16 @@ def test_threepid_invite_spamcheck(self): # Mock a few functions to prevent the test from failing due to failing to talk to # a remote IS. We keep the mock for _mock_make_and_store_3pid_invite around so we # can check its call_count later on during the test. - make_invite_mock = self._mock_make_and_store_3pid_invite() - self._mock_lookup_3pid() + make_invite_mock = Mock(return_value=make_awaitable(0)) + self.hs.get_room_member_handler()._make_and_store_3pid_invite = make_invite_mock + self.hs.get_identity_handler().lookup_3pid = Mock( + return_value=make_awaitable(None), + ) # Add a mock to the spamchecker callbacks for user_may_send_3pid_invite. Make it # allow everything for now. - return_value = True - - async def _user_may_send_3pid_invite( - inviter: str, - medium: str, - address: str, - room_id: str, - ) -> bool: - return return_value - - allow_mock = Mock(side_effect=_user_may_send_3pid_invite) - - self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append( - allow_mock - ) + mock = Mock(return_value=make_awaitable(True)) + self.hs.get_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" @@ -2489,16 +2479,14 @@ async def _user_may_send_3pid_invite( # Check that the callback was called with the right params. expected_call_args = ((self.user_id, "email", email_to_invite, self.room_id),) - self.assertEquals( - allow_mock.call_args, expected_call_args, allow_mock.call_args - ) + self.assertEquals(mock.call_args, expected_call_args, mock.call_args) # Check that the call to send the invite was made. self.assertEquals(make_invite_mock.call_count, 1) # Now change the return value of the callback to deny any invite and test that # we can't send the invite. - return_value = False + mock.return_value = make_awaitable(False) channel = self.make_request( method="POST", path="/rooms/" + self.room_id + "/invite", @@ -2514,51 +2502,3 @@ async def _user_may_send_3pid_invite( # Also check that it stopped before calling _make_and_store_3pid_invite. self.assertEquals(make_invite_mock.call_count, 1) - - def _mock_make_and_store_3pid_invite(self) -> Mock: - """Mocks RoomMemberHandler._make_and_store_3pid_invite with a function that just - returns the integer 0. - - Returns: - The Mock object _make_and_store_3pid_invite was replaced with. - """ - - async def _make_and_store_3pid_invite( - requester: Requester, - id_server: str, - medium: str, - address: str, - room_id: str, - user: UserID, - txn_id: Optional[str], - id_access_token: Optional[str] = None, - ) -> int: - return 0 - - mock = Mock(side_effect=_make_and_store_3pid_invite) - - self.hs.get_room_member_handler()._make_and_store_3pid_invite = mock - - return mock - - def _mock_lookup_3pid(self) -> Mock: - """Mocks IdentityHandler.lookup_3pid with a function that just returns None (ie - no binding for the 3PID. - - Returns: - The Mock object lookup_3pid was replaced with. - """ - - async def _lookup_3pid( - id_server: str, - medium: str, - address: str, - id_access_token: Optional[str] = None, - ) -> Optional[str]: - return None - - mock = Mock(side_effect=_lookup_3pid) - - self.hs.get_identity_handler().lookup_3pid = mock - - return mock From 1e40ce8c96c18c25901b6e3b5622fe0b5580c3e2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 6 Oct 2021 15:33:59 +0100 Subject: [PATCH 8/8] Use mock's assertions in test --- tests/rest/client/test_rooms.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index 06e58b2fa975..b597f20a02cb 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -2477,12 +2477,10 @@ def test_threepid_invite_spamcheck(self): self.assertEquals(channel.code, 200) # Check that the callback was called with the right params. - expected_call_args = ((self.user_id, "email", email_to_invite, self.room_id),) - - self.assertEquals(mock.call_args, expected_call_args, mock.call_args) + mock.assert_called_with(self.user_id, "email", email_to_invite, self.room_id) # Check that the call to send the invite was made. - self.assertEquals(make_invite_mock.call_count, 1) + 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. @@ -2501,4 +2499,4 @@ def test_threepid_invite_spamcheck(self): self.assertEquals(channel.code, 403) # Also check that it stopped before calling _make_and_store_3pid_invite. - self.assertEquals(make_invite_mock.call_count, 1) + make_invite_mock.assert_called_once()