1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291 |
- # Copyright 2015-2021 The Matrix.org Foundation C.I.C.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- from typing import Dict, Iterable, List, Optional
- from unittest.mock import AsyncMock, Mock
- from parameterized import parameterized
- from twisted.internet import defer
- from twisted.test.proto_helpers import MemoryReactor
- import synapse.rest.admin
- import synapse.storage
- from synapse.api.constants import EduTypes, EventTypes
- from synapse.appservice import (
- ApplicationService,
- TransactionOneTimeKeysCount,
- TransactionUnusedFallbackKeys,
- )
- from synapse.handlers.appservice import ApplicationServicesHandler
- from synapse.rest.client import login, receipts, register, room, sendtodevice
- from synapse.server import HomeServer
- from synapse.types import (
- JsonDict,
- MultiWriterStreamToken,
- RoomStreamToken,
- StreamKeyType,
- )
- from synapse.util import Clock
- from synapse.util.stringutils import random_string
- from tests import unittest
- from tests.test_utils import event_injection
- from tests.unittest import override_config
- from tests.utils import MockClock
- class AppServiceHandlerTestCase(unittest.TestCase):
- """Tests the ApplicationServicesHandler."""
- def setUp(self) -> None:
- self.mock_store = Mock()
- self.mock_as_api = AsyncMock()
- self.mock_scheduler = Mock()
- hs = Mock()
- hs.get_datastores.return_value = Mock(main=self.mock_store)
- self.mock_store.get_appservice_last_pos = AsyncMock(return_value=None)
- self.mock_store.set_appservice_last_pos = AsyncMock(return_value=None)
- self.mock_store.set_appservice_stream_type_pos = AsyncMock(return_value=None)
- hs.get_application_service_api.return_value = self.mock_as_api
- hs.get_application_service_scheduler.return_value = self.mock_scheduler
- hs.get_clock.return_value = MockClock()
- self.handler = ApplicationServicesHandler(hs)
- self.event_source = hs.get_event_sources()
- def test_notify_interested_services(self) -> None:
- interested_service = self._mkservice(is_interested_in_event=True)
- services = [
- self._mkservice(is_interested_in_event=False),
- interested_service,
- self._mkservice(is_interested_in_event=False),
- ]
- self.mock_as_api.query_user.return_value = True
- self.mock_store.get_app_services.return_value = services
- self.mock_store.get_user_by_id = AsyncMock(return_value=[])
- event = Mock(
- sender="@someone:anywhere", type="m.room.message", room_id="!foo:bar"
- )
- self.mock_store.get_all_new_event_ids_stream = AsyncMock(
- side_effect=[
- (0, {}),
- (1, {event.event_id: 0}),
- ]
- )
- self.mock_store.get_events_as_list = AsyncMock(
- side_effect=[
- [],
- [event],
- ]
- )
- self.handler.notify_interested_services(RoomStreamToken(stream=1))
- self.mock_scheduler.enqueue_for_appservice.assert_called_once_with(
- interested_service, events=[event]
- )
- def test_query_user_exists_unknown_user(self) -> None:
- user_id = "@someone:anywhere"
- services = [self._mkservice(is_interested_in_event=True)]
- services[0].is_interested_in_user.return_value = True
- self.mock_store.get_app_services.return_value = services
- self.mock_store.get_user_by_id = AsyncMock(return_value=None)
- event = Mock(sender=user_id, type="m.room.message", room_id="!foo:bar")
- self.mock_as_api.query_user.return_value = True
- self.mock_store.get_all_new_event_ids_stream = AsyncMock(
- side_effect=[
- (0, {event.event_id: 0}),
- ]
- )
- self.mock_store.get_events_as_list = AsyncMock(side_effect=[[event]])
- self.handler.notify_interested_services(RoomStreamToken(stream=0))
- self.mock_as_api.query_user.assert_called_once_with(services[0], user_id)
- def test_query_user_exists_known_user(self) -> None:
- user_id = "@someone:anywhere"
- services = [self._mkservice(is_interested_in_event=True)]
- services[0].is_interested_in_user.return_value = True
- self.mock_store.get_app_services.return_value = services
- self.mock_store.get_user_by_id = AsyncMock(return_value={"name": user_id})
- event = Mock(sender=user_id, type="m.room.message", room_id="!foo:bar")
- self.mock_as_api.query_user.return_value = True
- self.mock_store.get_all_new_event_ids_stream = AsyncMock(
- side_effect=[
- (0, [event], {event.event_id: 0}),
- ]
- )
- self.handler.notify_interested_services(RoomStreamToken(stream=0))
- self.assertFalse(
- self.mock_as_api.query_user.called,
- "query_user called when it shouldn't have been.",
- )
- def test_query_room_alias_exists(self) -> None:
- room_alias_str = "#foo:bar"
- room_alias = Mock()
- room_alias.to_string.return_value = room_alias_str
- room_id = "!alpha:bet"
- servers = ["aperture"]
- interested_service = self._mkservice_alias(is_room_alias_in_namespace=True)
- services = [
- self._mkservice_alias(is_room_alias_in_namespace=False),
- interested_service,
- self._mkservice_alias(is_room_alias_in_namespace=False),
- ]
- self.mock_as_api.query_alias = AsyncMock(return_value=True)
- self.mock_store.get_app_services.return_value = services
- self.mock_store.get_association_from_room_alias = AsyncMock(
- return_value=Mock(room_id=room_id, servers=servers)
- )
- result = self.successResultOf(
- defer.ensureDeferred(self.handler.query_room_alias_exists(room_alias))
- )
- assert result is not None
- self.mock_as_api.query_alias.assert_called_once_with(
- interested_service, room_alias_str
- )
- self.assertEqual(result.room_id, room_id)
- self.assertEqual(result.servers, servers)
- def test_get_3pe_protocols_no_appservices(self) -> None:
- self.mock_store.get_app_services.return_value = []
- response = self.successResultOf(
- defer.ensureDeferred(self.handler.get_3pe_protocols("my-protocol"))
- )
- self.mock_as_api.get_3pe_protocol.assert_not_called()
- self.assertEqual(response, {})
- def test_get_3pe_protocols_no_protocols(self) -> None:
- service = self._mkservice(False, [])
- self.mock_store.get_app_services.return_value = [service]
- response = self.successResultOf(
- defer.ensureDeferred(self.handler.get_3pe_protocols())
- )
- self.mock_as_api.get_3pe_protocol.assert_not_called()
- self.assertEqual(response, {})
- def test_get_3pe_protocols_protocol_no_response(self) -> None:
- service = self._mkservice(False, ["my-protocol"])
- self.mock_store.get_app_services.return_value = [service]
- self.mock_as_api.get_3pe_protocol.return_value = None
- response = self.successResultOf(
- defer.ensureDeferred(self.handler.get_3pe_protocols())
- )
- self.mock_as_api.get_3pe_protocol.assert_called_once_with(
- service, "my-protocol"
- )
- self.assertEqual(response, {})
- def test_get_3pe_protocols_select_one_protocol(self) -> None:
- service = self._mkservice(False, ["my-protocol"])
- self.mock_store.get_app_services.return_value = [service]
- self.mock_as_api.get_3pe_protocol.return_value = {
- "x-protocol-data": 42,
- "instances": [],
- }
- response = self.successResultOf(
- defer.ensureDeferred(self.handler.get_3pe_protocols("my-protocol"))
- )
- self.mock_as_api.get_3pe_protocol.assert_called_once_with(
- service, "my-protocol"
- )
- self.assertEqual(
- response, {"my-protocol": {"x-protocol-data": 42, "instances": []}}
- )
- def test_get_3pe_protocols_one_protocol(self) -> None:
- service = self._mkservice(False, ["my-protocol"])
- self.mock_store.get_app_services.return_value = [service]
- self.mock_as_api.get_3pe_protocol.return_value = {
- "x-protocol-data": 42,
- "instances": [],
- }
- response = self.successResultOf(
- defer.ensureDeferred(self.handler.get_3pe_protocols())
- )
- self.mock_as_api.get_3pe_protocol.assert_called_once_with(
- service, "my-protocol"
- )
- self.assertEqual(
- response, {"my-protocol": {"x-protocol-data": 42, "instances": []}}
- )
- def test_get_3pe_protocols_multiple_protocol(self) -> None:
- service_one = self._mkservice(False, ["my-protocol"])
- service_two = self._mkservice(False, ["other-protocol"])
- self.mock_store.get_app_services.return_value = [service_one, service_two]
- self.mock_as_api.get_3pe_protocol.return_value = {
- "x-protocol-data": 42,
- "instances": [],
- }
- response = self.successResultOf(
- defer.ensureDeferred(self.handler.get_3pe_protocols())
- )
- self.mock_as_api.get_3pe_protocol.assert_called()
- self.assertEqual(
- response,
- {
- "my-protocol": {"x-protocol-data": 42, "instances": []},
- "other-protocol": {"x-protocol-data": 42, "instances": []},
- },
- )
- def test_get_3pe_protocols_multiple_info(self) -> None:
- service_one = self._mkservice(False, ["my-protocol"])
- service_two = self._mkservice(False, ["my-protocol"])
- async def get_3pe_protocol(
- service: ApplicationService, protocol: str
- ) -> Optional[JsonDict]:
- if service == service_one:
- return {
- "x-protocol-data": 42,
- "instances": [{"desc": "Alice's service"}],
- }
- if service == service_two:
- return {
- "x-protocol-data": 36,
- "x-not-used": 45,
- "instances": [{"desc": "Bob's service"}],
- }
- raise Exception("Unexpected service")
- self.mock_store.get_app_services.return_value = [service_one, service_two]
- self.mock_as_api.get_3pe_protocol = get_3pe_protocol
- response = self.successResultOf(
- defer.ensureDeferred(self.handler.get_3pe_protocols())
- )
- # It's expected that the second service's data doesn't appear in the response
- self.assertEqual(
- response,
- {
- "my-protocol": {
- "x-protocol-data": 42,
- "instances": [
- {
- "desc": "Alice's service",
- },
- {"desc": "Bob's service"},
- ],
- },
- },
- )
- def test_notify_interested_services_ephemeral(self) -> None:
- """
- Test sending ephemeral events to the appservice handler are scheduled
- to be pushed out to interested appservices, and that the stream ID is
- updated accordingly.
- """
- interested_service = self._mkservice(is_interested_in_event=True)
- services = [interested_service]
- self.mock_store.get_app_services.return_value = services
- self.mock_store.get_type_stream_id_for_appservice = AsyncMock(return_value=579)
- event = Mock(event_id="event_1")
- self.event_source.sources.receipt.get_new_events_as = AsyncMock(
- return_value=([event], None)
- )
- self.handler.notify_interested_services_ephemeral(
- StreamKeyType.RECEIPT,
- MultiWriterStreamToken(stream=580),
- ["@fakerecipient:example.com"],
- )
- self.mock_scheduler.enqueue_for_appservice.assert_called_once_with(
- interested_service, ephemeral=[event]
- )
- self.mock_store.set_appservice_stream_type_pos.assert_called_once_with(
- interested_service,
- "read_receipt",
- 580,
- )
- def test_notify_interested_services_ephemeral_out_of_order(self) -> None:
- """
- Test sending out of order ephemeral events to the appservice handler
- are ignored.
- """
- interested_service = self._mkservice(is_interested_in_event=True)
- services = [interested_service]
- self.mock_store.get_app_services.return_value = services
- self.mock_store.get_type_stream_id_for_appservice = AsyncMock(return_value=580)
- event = Mock(event_id="event_1")
- self.event_source.sources.receipt.get_new_events_as = AsyncMock(
- return_value=([event], None)
- )
- self.handler.notify_interested_services_ephemeral(
- StreamKeyType.RECEIPT,
- MultiWriterStreamToken(stream=580),
- ["@fakerecipient:example.com"],
- )
- # This method will be called, but with an empty list of events
- self.mock_scheduler.enqueue_for_appservice.assert_called_once_with(
- interested_service, ephemeral=[]
- )
- def _mkservice(
- self, is_interested_in_event: bool, protocols: Optional[Iterable] = None
- ) -> Mock:
- """
- Create a new mock representing an ApplicationService.
- Args:
- is_interested_in_event: Whether this application service will be considered
- interested in all events.
- protocols: The third-party protocols that this application service claims to
- support.
- Returns:
- A mock representing the ApplicationService.
- """
- service = Mock()
- service.is_interested_in_event = AsyncMock(return_value=is_interested_in_event)
- service.token = "mock_service_token"
- service.url = "mock_service_url"
- service.protocols = protocols
- return service
- def _mkservice_alias(self, is_room_alias_in_namespace: bool) -> Mock:
- """
- Create a new mock representing an ApplicationService that is or is not interested
- any given room aliase.
- Args:
- is_room_alias_in_namespace: If true, the application service will be interested
- in all room aliases that are queried against it. If false, the application
- service will not be interested in any room aliases.
- Returns:
- A mock representing the ApplicationService.
- """
- service = Mock()
- service.is_room_alias_in_namespace.return_value = is_room_alias_in_namespace
- service.token = "mock_service_token"
- service.url = "mock_service_url"
- return service
- class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
- """
- Tests that the ApplicationServicesHandler sends events to application
- services correctly.
- """
- servlets = [
- synapse.rest.admin.register_servlets_for_client_rest_resource,
- login.register_servlets,
- room.register_servlets,
- sendtodevice.register_servlets,
- receipts.register_servlets,
- ]
- def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
- self.hs = hs
- # Mock the ApplicationServiceScheduler's _TransactionController's send method so that
- # we can track any outgoing ephemeral events
- self.send_mock = AsyncMock()
- hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock # type: ignore[method-assign]
- # Mock out application services, and allow defining our own in tests
- self._services: List[ApplicationService] = []
- self.hs.get_datastores().main.get_app_services = Mock( # type: ignore[method-assign]
- return_value=self._services
- )
- # A user on the homeserver.
- self.local_user_device_id = "local_device"
- self.local_user = self.register_user("local_user", "password")
- self.local_user_token = self.login(
- "local_user", "password", self.local_user_device_id
- )
- # A user on the homeserver which lies within an appservice's exclusive user namespace.
- self.exclusive_as_user_device_id = "exclusive_as_device"
- self.exclusive_as_user = self.register_user("exclusive_as_user", "password")
- self.exclusive_as_user_token = self.login(
- "exclusive_as_user", "password", self.exclusive_as_user_device_id
- )
- self.exclusive_as_user_2_device_id = "exclusive_as_device_2"
- self.exclusive_as_user_2 = self.register_user("exclusive_as_user_2", "password")
- self.exclusive_as_user_2_token = self.login(
- "exclusive_as_user_2", "password", self.exclusive_as_user_2_device_id
- )
- self.exclusive_as_user_3_device_id = "exclusive_as_device_3"
- self.exclusive_as_user_3 = self.register_user("exclusive_as_user_3", "password")
- self.exclusive_as_user_3_token = self.login(
- "exclusive_as_user_3", "password", self.exclusive_as_user_3_device_id
- )
- def _notify_interested_services(self) -> None:
- # This is normally set in `notify_interested_services` but we need to call the
- # internal async version so the reactor gets pushed to completion.
- self.hs.get_application_service_handler().current_max += 1
- self.get_success(
- self.hs.get_application_service_handler()._notify_interested_services(
- RoomStreamToken(
- stream=self.hs.get_application_service_handler().current_max
- )
- )
- )
- @parameterized.expand(
- [
- ("@local_as_user:test", True),
- # Defining remote users in an application service user namespace regex is a
- # footgun since the appservice might assume that it'll receive all events
- # sent by that remote user, but it will only receive events in rooms that
- # are shared with a local user. So we just remove this footgun possibility
- # entirely and we won't notify the application service based on remote
- # users.
- ("@remote_as_user:remote", False),
- ]
- )
- def test_match_interesting_room_members(
- self, interesting_user: str, should_notify: bool
- ) -> None:
- """
- Test to make sure that a interesting user (local or remote) in the room is
- notified as expected when someone else in the room sends a message.
- """
- # Register an application service that's interested in the `interesting_user`
- interested_appservice = self._register_application_service(
- namespaces={
- ApplicationService.NS_USERS: [
- {
- "regex": interesting_user,
- "exclusive": False,
- },
- ],
- },
- )
- # Create a room
- alice = self.register_user("alice", "pass")
- alice_access_token = self.login("alice", "pass")
- room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token)
- # Join the interesting user to the room
- self.get_success(
- event_injection.inject_member_event(
- self.hs, room_id, interesting_user, "join"
- )
- )
- # Kick the appservice into checking this membership event to get the event out
- # of the way
- self._notify_interested_services()
- # We don't care about the interesting user join event (this test is making sure
- # the next thing works)
- self.send_mock.reset_mock()
- # Send a message from an uninteresting user
- self.helper.send_event(
- room_id,
- type=EventTypes.Message,
- content={
- "msgtype": "m.text",
- "body": "message from uninteresting user",
- },
- tok=alice_access_token,
- )
- # Kick the appservice into checking this new event
- self._notify_interested_services()
- if should_notify:
- self.send_mock.assert_called_once()
- (
- service,
- events,
- _ephemeral,
- _to_device_messages,
- _otks,
- _fbks,
- _device_list_summary,
- ) = self.send_mock.call_args[0]
- # Even though the message came from an uninteresting user, it should still
- # notify us because the interesting user is joined to the room where the
- # message was sent.
- self.assertEqual(service, interested_appservice)
- self.assertEqual(events[0]["type"], "m.room.message")
- self.assertEqual(events[0]["sender"], alice)
- else:
- self.send_mock.assert_not_called()
- def test_application_services_receive_events_sent_by_interesting_local_user(
- self,
- ) -> None:
- """
- Test to make sure that a messages sent from a local user can be interesting and
- picked up by the appservice.
- """
- # Register an application service that's interested in all local users
- interested_appservice = self._register_application_service(
- namespaces={
- ApplicationService.NS_USERS: [
- {
- "regex": ".*",
- "exclusive": False,
- },
- ],
- },
- )
- # Create a room
- alice = self.register_user("alice", "pass")
- alice_access_token = self.login("alice", "pass")
- room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token)
- # We don't care about interesting events before this (this test is making sure
- # the next thing works)
- self.send_mock.reset_mock()
- # Send a message from the interesting local user
- self.helper.send_event(
- room_id,
- type=EventTypes.Message,
- content={
- "msgtype": "m.text",
- "body": "message from interesting local user",
- },
- tok=alice_access_token,
- )
- # Kick the appservice into checking this new event
- self._notify_interested_services()
- self.send_mock.assert_called_once()
- (
- service,
- events,
- _ephemeral,
- _to_device_messages,
- _otks,
- _fbks,
- _device_list_summary,
- ) = self.send_mock.call_args[0]
- # Events sent from an interesting local user should also be picked up as
- # interesting to the appservice.
- self.assertEqual(service, interested_appservice)
- self.assertEqual(events[0]["type"], "m.room.message")
- self.assertEqual(events[0]["sender"], alice)
- def test_sending_read_receipt_batches_to_application_services(self) -> None:
- """Tests that a large batch of read receipts are sent correctly to
- interested application services.
- """
- # Register an application service that's interested in a certain user
- # and room prefix
- interested_appservice = self._register_application_service(
- namespaces={
- ApplicationService.NS_USERS: [
- {
- "regex": "@exclusive_as_user:.+",
- "exclusive": True,
- }
- ],
- ApplicationService.NS_ROOMS: [
- {
- "regex": "!fakeroom_.*",
- "exclusive": True,
- }
- ],
- },
- )
- # Now, pretend that we receive a large burst of read receipts (300 total) that
- # all come in at once.
- for i in range(300):
- self.get_success(
- # Insert a fake read receipt into the database
- self.hs.get_datastores().main.insert_receipt(
- # We have to use unique room ID + user ID combinations here, as the db query
- # is an upsert.
- room_id=f"!fakeroom_{i}:test",
- receipt_type="m.read",
- user_id=self.local_user,
- event_ids=[f"$eventid_{i}"],
- thread_id=None,
- data={},
- )
- )
- # Now notify the appservice handler that 300 read receipts have all arrived
- # at once. What will it do!
- # note: stream tokens start at 2
- for stream_token in range(2, 303):
- self.get_success(
- self.hs.get_application_service_handler()._notify_interested_services_ephemeral(
- services=[interested_appservice],
- stream_key=StreamKeyType.RECEIPT,
- new_token=MultiWriterStreamToken(stream=stream_token),
- users=[self.exclusive_as_user],
- )
- )
- # Using our txn send mock, we can see what the AS received. After iterating over every
- # transaction, we'd like to see all 300 read receipts accounted for.
- # No more, no less.
- all_ephemeral_events = []
- for call in self.send_mock.call_args_list:
- ephemeral_events = call[0][2]
- all_ephemeral_events += ephemeral_events
- # Ensure that no duplicate events were sent
- self.assertEqual(len(all_ephemeral_events), 300)
- # Check that the ephemeral event is a read receipt with the expected structure
- latest_read_receipt = all_ephemeral_events[-1]
- self.assertEqual(latest_read_receipt["type"], EduTypes.RECEIPT)
- event_id = list(latest_read_receipt["content"].keys())[0]
- self.assertEqual(
- latest_read_receipt["content"][event_id]["m.read"], {self.local_user: {}}
- )
- @unittest.override_config(
- {"experimental_features": {"msc2409_to_device_messages_enabled": True}}
- )
- def test_application_services_receive_local_to_device(self) -> None:
- """
- Test that when a user sends a to-device message to another user
- that is an application service's user namespace, the
- application service will receive it.
- """
- interested_appservice = self._register_application_service(
- namespaces={
- ApplicationService.NS_USERS: [
- {
- "regex": "@exclusive_as_user:.+",
- "exclusive": True,
- }
- ],
- },
- )
- # Have local_user send a to-device message to exclusive_as_user
- message_content = {"some_key": "some really interesting value"}
- chan = self.make_request(
- "PUT",
- "/_matrix/client/r0/sendToDevice/m.room_key_request/3",
- content={
- "messages": {
- self.exclusive_as_user: {
- self.exclusive_as_user_device_id: message_content
- }
- }
- },
- access_token=self.local_user_token,
- )
- self.assertEqual(chan.code, 200, chan.result)
- # Have exclusive_as_user send a to-device message to local_user
- chan = self.make_request(
- "PUT",
- "/_matrix/client/r0/sendToDevice/m.room_key_request/4",
- content={
- "messages": {
- self.local_user: {self.local_user_device_id: message_content}
- }
- },
- access_token=self.exclusive_as_user_token,
- )
- self.assertEqual(chan.code, 200, chan.result)
- # Check if our application service - that is interested in exclusive_as_user - received
- # the to-device message as part of an AS transaction.
- # Only the local_user -> exclusive_as_user to-device message should have been forwarded to the AS.
- #
- # The uninterested application service should not have been notified at all.
- self.send_mock.assert_called_once()
- (
- service,
- _events,
- _ephemeral,
- to_device_messages,
- _otks,
- _fbks,
- _device_list_summary,
- ) = self.send_mock.call_args[0]
- # Assert that this was the same to-device message that local_user sent
- self.assertEqual(service, interested_appservice)
- self.assertEqual(to_device_messages[0]["type"], "m.room_key_request")
- self.assertEqual(to_device_messages[0]["sender"], self.local_user)
- # Additional fields 'to_user_id' and 'to_device_id' specifically for
- # to-device messages via the AS API
- self.assertEqual(to_device_messages[0]["to_user_id"], self.exclusive_as_user)
- self.assertEqual(
- to_device_messages[0]["to_device_id"], self.exclusive_as_user_device_id
- )
- self.assertEqual(to_device_messages[0]["content"], message_content)
- @unittest.override_config(
- {"experimental_features": {"msc2409_to_device_messages_enabled": True}}
- )
- def test_application_services_receive_bursts_of_to_device(self) -> None:
- """
- Test that when a user sends >100 to-device messages at once, any
- interested AS's will receive them in separate transactions.
- Also tests that uninterested application services do not receive messages.
- """
- # Register two application services with exclusive interest in a user
- interested_appservices = []
- for _ in range(2):
- appservice = self._register_application_service(
- namespaces={
- ApplicationService.NS_USERS: [
- {
- "regex": "@exclusive_as_user:.+",
- "exclusive": True,
- }
- ],
- },
- )
- interested_appservices.append(appservice)
- # ...and an application service which does not have any user interest.
- self._register_application_service()
- to_device_message_content = {
- "some key": "some interesting value",
- }
- # We need to send a large burst of to-device messages. We also would like to
- # include them all in the same application service transaction so that we can
- # test large transactions.
- #
- # To do this, we can send a single to-device message to many user devices at
- # once.
- #
- # We insert number_of_messages - 1 messages into the database directly. We'll then
- # send a final to-device message to the real device, which will also kick off
- # an AS transaction (as just inserting messages into the DB won't).
- number_of_messages = 150
- fake_device_ids = [f"device_{num}" for num in range(number_of_messages - 1)]
- messages = {
- self.exclusive_as_user: {
- device_id: {
- "type": "test_to_device_message",
- "sender": "@some:sender",
- "content": to_device_message_content,
- }
- for device_id in fake_device_ids
- }
- }
- # Create a fake device per message. We can't send to-device messages to
- # a device that doesn't exist.
- self.get_success(
- self.hs.get_datastores().main.db_pool.simple_insert_many(
- desc="test_application_services_receive_burst_of_to_device",
- table="devices",
- keys=("user_id", "device_id"),
- values=[
- (
- self.exclusive_as_user,
- device_id,
- )
- for device_id in fake_device_ids
- ],
- )
- )
- # Seed the device_inbox table with our fake messages
- self.get_success(
- self.hs.get_datastores().main.add_messages_to_device_inbox(messages, {})
- )
- # Now have local_user send a final to-device message to exclusive_as_user. All unsent
- # to-device messages should be sent to any application services
- # interested in exclusive_as_user.
- chan = self.make_request(
- "PUT",
- "/_matrix/client/r0/sendToDevice/m.room_key_request/4",
- content={
- "messages": {
- self.exclusive_as_user: {
- self.exclusive_as_user_device_id: to_device_message_content
- }
- }
- },
- access_token=self.local_user_token,
- )
- self.assertEqual(chan.code, 200, chan.result)
- self.send_mock.assert_called()
- # Count the total number of to-device messages that were sent out per-service.
- # Ensure that we only sent to-device messages to interested services, and that
- # each interested service received the full count of to-device messages.
- service_id_to_message_count: Dict[str, int] = {}
- for call in self.send_mock.call_args_list:
- (
- service,
- _events,
- _ephemeral,
- to_device_messages,
- _otks,
- _fbks,
- _device_list_summary,
- ) = call[0]
- # Check that this was made to an interested service
- self.assertIn(service, interested_appservices)
- # Add to the count of messages for this application service
- service_id_to_message_count.setdefault(service.id, 0)
- service_id_to_message_count[service.id] += len(to_device_messages)
- # Assert that each interested service received the full count of messages
- for count in service_id_to_message_count.values():
- self.assertEqual(count, number_of_messages)
- @unittest.override_config(
- {"experimental_features": {"msc2409_to_device_messages_enabled": True}}
- )
- def test_application_services_receive_local_to_device_for_many_users(self) -> None:
- """
- Test that when a user sends a to-device message to many users
- in an application service's user namespace, the
- application service will receive all of them.
- """
- interested_appservice = self._register_application_service(
- namespaces={
- ApplicationService.NS_USERS: [
- {
- "regex": "@exclusive_as_user:.+",
- "exclusive": True,
- },
- {
- "regex": "@exclusive_as_user_2:.+",
- "exclusive": True,
- },
- {
- "regex": "@exclusive_as_user_3:.+",
- "exclusive": True,
- },
- ],
- },
- )
- # Have local_user send a to-device message to exclusive_as_users
- message_content = {"some_key": "some really interesting value"}
- chan = self.make_request(
- "PUT",
- "/_matrix/client/r0/sendToDevice/m.room_key_request/3",
- content={
- "messages": {
- self.exclusive_as_user: {
- self.exclusive_as_user_device_id: message_content
- },
- self.exclusive_as_user_2: {
- self.exclusive_as_user_2_device_id: message_content
- },
- self.exclusive_as_user_3: {
- self.exclusive_as_user_3_device_id: message_content
- },
- }
- },
- access_token=self.local_user_token,
- )
- self.assertEqual(chan.code, 200, chan.result)
- # Have exclusive_as_user send a to-device message to local_user
- for user_token in [
- self.exclusive_as_user_token,
- self.exclusive_as_user_2_token,
- self.exclusive_as_user_3_token,
- ]:
- chan = self.make_request(
- "PUT",
- "/_matrix/client/r0/sendToDevice/m.room_key_request/4",
- content={
- "messages": {
- self.local_user: {self.local_user_device_id: message_content}
- }
- },
- access_token=user_token,
- )
- self.assertEqual(chan.code, 200, chan.result)
- # Check if our application service - that is interested in exclusive_as_user - received
- # the to-device message as part of an AS transaction.
- # Only the local_user -> exclusive_as_user to-device message should have been forwarded to the AS.
- #
- # The uninterested application service should not have been notified at all.
- self.send_mock.assert_called_once()
- (
- service,
- _events,
- _ephemeral,
- to_device_messages,
- _otks,
- _fbks,
- _device_list_summary,
- ) = self.send_mock.call_args[0]
- # Assert that this was the same to-device message that local_user sent
- self.assertEqual(service, interested_appservice)
- # Assert expected number of messages
- self.assertEqual(len(to_device_messages), 3)
- for device_msg in to_device_messages:
- self.assertEqual(device_msg["type"], "m.room_key_request")
- self.assertEqual(device_msg["sender"], self.local_user)
- self.assertEqual(device_msg["content"], message_content)
- self.assertEqual(to_device_messages[0]["to_user_id"], self.exclusive_as_user)
- self.assertEqual(
- to_device_messages[0]["to_device_id"],
- self.exclusive_as_user_device_id,
- )
- self.assertEqual(to_device_messages[1]["to_user_id"], self.exclusive_as_user_2)
- self.assertEqual(
- to_device_messages[1]["to_device_id"],
- self.exclusive_as_user_2_device_id,
- )
- self.assertEqual(to_device_messages[2]["to_user_id"], self.exclusive_as_user_3)
- self.assertEqual(
- to_device_messages[2]["to_device_id"],
- self.exclusive_as_user_3_device_id,
- )
- def _register_application_service(
- self,
- namespaces: Optional[Dict[str, Iterable[Dict]]] = None,
- ) -> ApplicationService:
- """
- Register a new application service, with the given namespaces of interest.
- Args:
- namespaces: A dictionary containing any user, room or alias namespaces that
- the application service is interested in.
- Returns:
- The registered application service.
- """
- # Create an application service
- appservice = ApplicationService(
- token=random_string(10),
- id=random_string(10),
- sender="@as:example.com",
- rate_limited=False,
- namespaces=namespaces,
- supports_ephemeral=True,
- )
- # Register the application service
- self._services.append(appservice)
- return appservice
- class ApplicationServicesHandlerDeviceListsTestCase(unittest.HomeserverTestCase):
- """
- Tests that the ApplicationServicesHandler sends device list updates to application
- services correctly.
- """
- servlets = [
- synapse.rest.admin.register_servlets_for_client_rest_resource,
- login.register_servlets,
- room.register_servlets,
- ]
- def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
- # Allow us to modify cached feature flags mid-test
- self.as_handler = hs.get_application_service_handler()
- # Mock ApplicationServiceApi's put_json, so we can verify the raw JSON that
- # will be sent over the wire
- self.put_json = AsyncMock()
- hs.get_application_service_api().put_json = self.put_json # type: ignore[method-assign]
- # Mock out application services, and allow defining our own in tests
- self._services: List[ApplicationService] = []
- self.hs.get_datastores().main.get_app_services = Mock( # type: ignore[method-assign]
- return_value=self._services
- )
- # Test across a variety of configuration values
- @parameterized.expand(
- [
- (True, True, True),
- (True, False, False),
- (False, True, False),
- (False, False, False),
- ]
- )
- def test_application_service_receives_device_list_updates(
- self,
- experimental_feature_enabled: bool,
- as_supports_txn_extensions: bool,
- as_should_receive_device_list_updates: bool,
- ) -> None:
- """
- Tests that an application service receives notice of changed device
- lists for a user, when a user changes their device lists.
- Arguments above are populated by parameterized.
- Args:
- as_should_receive_device_list_updates: Whether we expect the AS to receive the
- device list changes.
- experimental_feature_enabled: Whether the "msc3202_transaction_extensions" experimental
- feature is enabled. This feature must be enabled for device lists to ASs to work.
- as_supports_txn_extensions: Whether the application service has explicitly registered
- to receive information defined by MSC3202 - which includes device list changes.
- """
- # Change whether the experimental feature is enabled or disabled before making
- # device list changes
- self.as_handler._msc3202_transaction_extensions_enabled = (
- experimental_feature_enabled
- )
- # Create an appservice that is interested in "local_user"
- appservice = ApplicationService(
- token=random_string(10),
- id=random_string(10),
- sender="@as:example.com",
- rate_limited=False,
- namespaces={
- ApplicationService.NS_USERS: [
- {
- "regex": "@local_user:.+",
- "exclusive": False,
- }
- ],
- },
- supports_ephemeral=True,
- msc3202_transaction_extensions=as_supports_txn_extensions,
- # Must be set for Synapse to try pushing data to the AS
- hs_token="abcde",
- url="some_url",
- )
- # Register the application service
- self._services.append(appservice)
- # Register a user on the homeserver
- self.local_user = self.register_user("local_user", "password")
- self.local_user_token = self.login("local_user", "password")
- if as_should_receive_device_list_updates:
- # Ensure that the resulting JSON uses the unstable prefix and contains the
- # expected users
- self.put_json.assert_called_once()
- json_body = self.put_json.call_args[1]["json_body"]
- # Our application service should have received a device list update with
- # "local_user" in the "changed" list
- device_list_dict = json_body.get("org.matrix.msc3202.device_lists", {})
- self.assertEqual([], device_list_dict["left"])
- self.assertEqual([self.local_user], device_list_dict["changed"])
- else:
- # No device list changes should have been sent out
- self.put_json.assert_not_called()
- class ApplicationServicesHandlerOtkCountsTestCase(unittest.HomeserverTestCase):
- # Argument indices for pulling out arguments from a `send_mock`.
- ARG_OTK_COUNTS = 4
- ARG_FALLBACK_KEYS = 5
- servlets = [
- synapse.rest.admin.register_servlets_for_client_rest_resource,
- login.register_servlets,
- register.register_servlets,
- room.register_servlets,
- sendtodevice.register_servlets,
- receipts.register_servlets,
- ]
- def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
- # Mock the ApplicationServiceScheduler's _TransactionController's send method so that
- # we can track what's going out
- self.send_mock = AsyncMock()
- hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock # type: ignore[method-assign] # We assign to a method.
- # Define an application service for the tests
- self._service_token = "VERYSECRET"
- self._service = ApplicationService(
- self._service_token,
- "as1",
- "@as.sender:test",
- namespaces={
- "users": [
- {"regex": "@_as_.*:test", "exclusive": True},
- {"regex": "@as.sender:test", "exclusive": True},
- ]
- },
- msc3202_transaction_extensions=True,
- )
- self.hs.get_datastores().main.services_cache = [self._service]
- # Register some appservice users
- self._sender_user, self._sender_device = self.register_appservice_user(
- "as.sender", self._service_token
- )
- self._namespaced_user, self._namespaced_device = self.register_appservice_user(
- "_as_user1", self._service_token
- )
- # Register a real user as well.
- self._real_user = self.register_user("real.user", "meow")
- self._real_user_token = self.login("real.user", "meow")
- async def _add_otks_for_device(
- self, user_id: str, device_id: str, otk_count: int
- ) -> None:
- """
- Add some dummy keys. It doesn't matter if they're not a real algorithm;
- that should be opaque to the server anyway.
- """
- await self.hs.get_datastores().main.add_e2e_one_time_keys(
- user_id,
- device_id,
- self.clock.time_msec(),
- [("algo", f"k{i}", "{}") for i in range(otk_count)],
- )
- async def _add_fallback_key_for_device(
- self, user_id: str, device_id: str, used: bool
- ) -> None:
- """
- Adds a fake fallback key to a device, optionally marking it as used
- right away.
- """
- store = self.hs.get_datastores().main
- await store.set_e2e_fallback_keys(user_id, device_id, {"algo:fk": "fall back!"})
- if used is True:
- # Mark the key as used
- await store.db_pool.simple_update_one(
- table="e2e_fallback_keys_json",
- keyvalues={
- "user_id": user_id,
- "device_id": device_id,
- "algorithm": "algo",
- "key_id": "fk",
- },
- updatevalues={"used": True},
- desc="_get_fallback_key_set_used",
- )
- def _set_up_devices_and_a_room(self) -> str:
- """
- Helper to set up devices for all the users
- and a room for the users to talk in.
- """
- async def preparation() -> None:
- await self._add_otks_for_device(self._sender_user, self._sender_device, 42)
- await self._add_fallback_key_for_device(
- self._sender_user, self._sender_device, used=True
- )
- await self._add_otks_for_device(
- self._namespaced_user, self._namespaced_device, 36
- )
- await self._add_fallback_key_for_device(
- self._namespaced_user, self._namespaced_device, used=False
- )
- # Register a device for the real user, too, so that we can later ensure
- # that we don't leak information to the AS about the non-AS user.
- await self.hs.get_datastores().main.store_device(
- self._real_user, "REALDEV", "UltraMatrix 3000"
- )
- await self._add_otks_for_device(self._real_user, "REALDEV", 50)
- self.get_success(preparation())
- room_id = self.helper.create_room_as(
- self._real_user, is_public=True, tok=self._real_user_token
- )
- self.helper.join(
- room_id,
- self._namespaced_user,
- tok=self._service_token,
- appservice_user_id=self._namespaced_user,
- )
- # Check it was called for sanity. (This was to send the join event to the AS.)
- self.send_mock.assert_called()
- self.send_mock.reset_mock()
- return room_id
- @override_config(
- {"experimental_features": {"msc3202_transaction_extensions": True}}
- )
- def test_application_services_receive_otk_counts_and_fallback_key_usages_with_pdus(
- self,
- ) -> None:
- """
- Tests that:
- - the AS receives one-time key counts and unused fallback keys for:
- - the specified sender; and
- - any user who is in receipt of the PDUs
- """
- room_id = self._set_up_devices_and_a_room()
- # Send a message into the AS's room
- self.helper.send(room_id, "woof woof", tok=self._real_user_token)
- # Capture what was sent as an AS transaction.
- self.send_mock.assert_called()
- last_args, _last_kwargs = self.send_mock.call_args
- otks: Optional[TransactionOneTimeKeysCount] = last_args[self.ARG_OTK_COUNTS]
- unused_fallbacks: Optional[TransactionUnusedFallbackKeys] = last_args[
- self.ARG_FALLBACK_KEYS
- ]
- self.assertEqual(
- otks,
- {
- "@as.sender:test": {self._sender_device: {"algo": 42}},
- "@_as_user1:test": {self._namespaced_device: {"algo": 36}},
- },
- )
- self.assertEqual(
- unused_fallbacks,
- {
- "@as.sender:test": {self._sender_device: []},
- "@_as_user1:test": {self._namespaced_device: ["algo"]},
- },
- )
|