diff --git a/synapse/handlers/account_data.py b/synapse/handlers/account_data.py
index 97a463d8d0..8041326cd5 100644
--- a/synapse/handlers/account_data.py
+++ b/synapse/handlers/account_data.py
@@ -327,6 +327,7 @@ class AccountDataEventSource(EventSource[int, JsonDict]):
explicit_room_id: Optional[str] = None,
) -> Tuple[List[JsonDict], int]:
user_id = user.to_string()
+ # TODO: Take `to_key` into account
last_stream_id = from_key
current_stream_id = self.store.get_max_account_data_stream_id()
diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py
index 8674a8fcdd..d04c76be2a 100644
--- a/synapse/handlers/receipts.py
+++ b/synapse/handlers/receipts.py
@@ -286,8 +286,10 @@ class ReceiptEventSource(EventSource[MultiWriterStreamToken, JsonMapping]):
room_ids: Iterable[str],
is_guest: bool,
explicit_room_id: Optional[str] = None,
+ to_key: Optional[MultiWriterStreamToken] = None,
) -> Tuple[List[JsonMapping], MultiWriterStreamToken]:
- to_key = self.get_current_key()
+ if to_key is None:
+ to_key = self.get_current_key()
if from_key == to_key:
return [], to_key
diff --git a/synapse/handlers/sliding_sync.py b/synapse/handlers/sliding_sync.py
index 103ebfbbdf..eb47569402 100644
--- a/synapse/handlers/sliding_sync.py
+++ b/synapse/handlers/sliding_sync.py
@@ -47,6 +47,7 @@ from synapse.types import (
DeviceListUpdates,
JsonDict,
JsonMapping,
+ MultiWriterStreamToken,
PersistedEventPosition,
Requester,
RoomStreamToken,
@@ -631,7 +632,7 @@ class SlidingSyncHandler:
extensions = await self.get_extensions_response(
sync_config=sync_config,
actual_lists=lists,
- actual_room_ids=rooms.keys(),
+ actual_room_ids=set(rooms.keys()),
from_token=from_token,
to_token=to_token,
)
@@ -1869,12 +1870,14 @@ class SlidingSyncHandler:
def find_relevant_room_ids_for_extension(
self,
requested_lists: Optional[List[str]],
- requested_rooms: Optional[List[str]],
+ requested_room_ids: Optional[List[str]],
actual_lists: Dict[str, SlidingSyncResult.SlidingWindowList],
actual_room_ids: Set[str],
) -> Set[str]:
"""
- Handle the reserved `lists`/`rooms` keys for extensions.
+ Handle the reserved `lists`/`rooms` keys for extensions. Extensions should only
+ return results for rooms in the Sliding Sync response. This matches up the
+ requested rooms/lists with the actual lists/rooms in the Sliding Sync response.
{"lists": []} // Do not process any lists.
{"lists": ["rooms", "dms"]} // Process only a subset of lists.
@@ -1886,9 +1889,9 @@ class SlidingSyncHandler:
Args:
requested_lists: The `lists` from the extension request.
- requested_rooms: The `rooms` from the extension request.
+ requested_room_ids: The `rooms` from the extension request.
actual_lists: The actual lists from the Sliding Sync response.
- actual_room_subscriptions: The actual room subscriptions from the Sliding Sync request.
+ actual_room_ids: The actual room subscriptions from the Sliding Sync request.
"""
# We only want to include account data for rooms that are already in the sliding
@@ -1896,8 +1899,8 @@ class SlidingSyncHandler:
relevant_room_ids: Set[str] = set()
# See what rooms from the room subscriptions we should get account data for
- if requested_rooms is not None:
- for room_id in requested_rooms:
+ if requested_room_ids is not None:
+ for room_id in requested_room_ids:
# A wildcard means we process all rooms from the room subscriptions
if room_id == "*":
relevant_room_ids.update(actual_room_ids)
@@ -2087,6 +2090,7 @@ class SlidingSyncHandler:
global_account_data_map: Mapping[str, JsonMapping] = {}
if from_token is not None:
+ # TODO: This should take into account the `from_token` and `to_token`
global_account_data_map = (
await self.store.get_updated_global_account_data_for_user(
user_id, from_token.stream_token.account_data_key
@@ -2098,15 +2102,18 @@ class SlidingSyncHandler:
)
if have_push_rules_changed:
global_account_data_map = dict(global_account_data_map)
+ # TODO: This should take into account the `from_token` and `to_token`
global_account_data_map[AccountDataTypes.PUSH_RULES] = (
await self.push_rules_handler.push_rules_for_user(sync_config.user)
)
else:
+ # TODO: This should take into account the `to_token`
all_global_account_data = await self.store.get_global_account_data_for_user(
user_id
)
global_account_data_map = dict(all_global_account_data)
+ # TODO: This should take into account the `to_token`
global_account_data_map[AccountDataTypes.PUSH_RULES] = (
await self.push_rules_handler.push_rules_for_user(sync_config.user)
)
@@ -2115,18 +2122,20 @@ class SlidingSyncHandler:
account_data_by_room_map: Mapping[str, Mapping[str, JsonMapping]] = {}
relevant_room_ids = self.find_relevant_room_ids_for_extension(
requested_lists=account_data_request.lists,
- requested_rooms=account_data_request.rooms,
+ requested_room_ids=account_data_request.rooms,
actual_lists=actual_lists,
actual_room_ids=actual_room_ids,
)
if len(relevant_room_ids) > 0:
if from_token is not None:
+ # TODO: This should take into account the `from_token` and `to_token`
account_data_by_room_map = (
await self.store.get_updated_room_account_data_for_user(
user_id, from_token.stream_token.account_data_key
)
)
else:
+ # TODO: This should take into account the `to_token`
account_data_by_room_map = (
await self.store.get_room_account_data_for_user(user_id)
)
@@ -2163,21 +2172,41 @@ class SlidingSyncHandler:
to_token: The point in the stream to sync up to.
from_token: The point in the stream to sync from.
"""
- user_id = sync_config.user.to_string()
-
# Skip if the extension is not enabled
if not receipts_request.enabled:
return None
- # receipt_source = self.event_sources.sources.receipt
- # receipts, receipt_key = await receipt_source.get_new_events(
- # user=sync_config.user,
- # from_key=receipt_key,
- # limit=sync_config.filter_collection.ephemeral_limit(),
- # room_ids=room_ids,
- # is_guest=sync_config.is_guest,
- # )
+ relevant_room_ids = self.find_relevant_room_ids_for_extension(
+ requested_lists=receipts_request.lists,
+ requested_room_ids=receipts_request.rooms,
+ actual_lists=actual_lists,
+ actual_room_ids=actual_room_ids,
+ )
+
+ room_id_to_receipt_map: Mapping[str, JsonMapping] = {}
+ if len(relevant_room_ids) > 0:
+ receipt_source = self.event_sources.sources.receipt
+ receipts, _ = await receipt_source.get_new_events(
+ user=sync_config.user,
+ from_key=(
+ from_token.stream_token.receipt_key
+ if from_token
+ else MultiWriterStreamToken(stream=0)
+ ),
+ to_key=to_token.receipt_key,
+ # This is a dummy value and isn't used in the function
+ limit=0,
+ room_ids=relevant_room_ids,
+ is_guest=False,
+ )
+
+ for receipt in receipts:
+ # These fields should exist for every receipt
+ room_id = receipt["room_id"]
+ type = receipt["type"]
+ content = receipt["content"]
+ room_id_to_receipt_map[room_id] = {type: type, content: content}
return SlidingSyncResult.Extensions.ReceiptsExtension(
- room_id_to_receipt_map=TODO,
+ room_id_to_receipt_map=room_id_to_receipt_map,
)
diff --git a/synapse/types/handlers/__init__.py b/synapse/types/handlers/__init__.py
index 654c50ef61..488ebd8365 100644
--- a/synapse/types/handlers/__init__.py
+++ b/synapse/types/handlers/__init__.py
@@ -357,7 +357,7 @@ class SlidingSyncResult:
room_id_to_receipt_map: Mapping from room_id to `m.receipt` event (type, content)
"""
- room_id_to_receipt_map: Mapping[str, Mapping[str, JsonMapping]]
+ room_id_to_receipt_map: Mapping[str, JsonMapping]
def __bool__(self) -> bool:
return bool(self.room_id_to_receipt_map)
diff --git a/tests/rest/client/test_sync.py b/tests/rest/client/test_sync.py
index 276588ad2f..5047313941 100644
--- a/tests/rest/client/test_sync.py
+++ b/tests/rest/client/test_sync.py
@@ -4625,6 +4625,179 @@ class SlidingSyncTestCase(SlidingSyncBase):
channel.json_body["rooms"].get(room_id1), channel.json_body["rooms"]
)
+ @parameterized.expand([("account_data",), ("receipts",)])
+ def test_extensions_lists_rooms_relevant_rooms(self, extension_name: str) -> None:
+ """
+ Test out different variations of `lists`/`rooms` we are requesting extensions for.
+ """
+ user1_id = self.register_user("user1", "pass")
+ user1_tok = self.login(user1_id, "pass")
+
+ # Create some rooms
+ room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
+ room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok)
+ room_id3 = self.helper.create_room_as(user1_id, tok=user1_tok)
+ room_id4 = self.helper.create_room_as(user1_id, tok=user1_tok)
+ room_id5 = self.helper.create_room_as(user1_id, tok=user1_tok)
+
+ room_id_to_human_name_map = {
+ room_id1: "room1",
+ room_id2: "room2",
+ room_id3: "room3",
+ room_id4: "room4",
+ room_id5: "room5",
+ }
+
+ for room_id in room_id_to_human_name_map.keys():
+ # Add some account data to each room
+ self.get_success(
+ self.account_data_handler.add_account_data_to_room(
+ user_id=user1_id,
+ room_id=room_id,
+ account_data_type="org.matrix.roorarraz",
+ content={"roo": "rar"},
+ )
+ )
+
+ main_sync_body = {
+ "lists": {
+ # We expect this list range to include room5 and room4
+ "foo-list": {
+ "ranges": [[0, 1]],
+ "required_state": [],
+ "timeline_limit": 0,
+ },
+ # We expect this list range to include room5, room4, room3
+ "bar-list": {
+ "ranges": [[0, 2]],
+ "required_state": [],
+ "timeline_limit": 0,
+ },
+ },
+ "room_subscriptions": {
+ room_id1: {
+ "required_state": [],
+ "timeline_limit": 0,
+ }
+ },
+ }
+
+ # Mix lists and rooms
+ sync_body = {
+ **main_sync_body,
+ "extensions": {
+ "account_data": {
+ "enabled": True,
+ "lists": ["foo-list", "non-existent-list"],
+ "rooms": [room_id1, room_id2, "!non-existent-room"],
+ }
+ },
+ }
+ response_body, _ = self.do_sync(sync_body, tok=user1_tok)
+
+ # room1: ✅ Requested via `rooms` and a room subscription exists
+ # room2: ❌ Requested via `rooms` but not in the response (from lists or room subscriptions)
+ # room3: ❌ Not requested
+ # room4: ✅ Shows up because requested via `lists` and list exists in the response
+ # room5: ✅ Shows up because requested via `lists` and list exists in the response
+ self.assertIncludes(
+ {
+ room_id_to_human_name_map[room_id]
+ for room_id in response_body["extensions"]["account_data"]
+ .get("rooms")
+ .keys()
+ },
+ {"room1", "room4", "room5"},
+ exact=True,
+ )
+
+ # Try wildcards (this is the default)
+ sync_body = {
+ **main_sync_body,
+ "extensions": {
+ "account_data": {
+ "enabled": True,
+ # "lists": ["*"],
+ # "rooms": ["*"],
+ }
+ },
+ }
+ response_body, _ = self.do_sync(sync_body, tok=user1_tok)
+
+ # room1: ✅ Shows up because of default `rooms` wildcard and is in one of the room subscriptions
+ # room2: ❌ Not requested
+ # room3: ✅ Shows up because of default `lists` wildcard and is in a list
+ # room4: ✅ Shows up because of default `lists` wildcard and is in a list
+ # room5: ✅ Shows up because of default `lists` wildcard and is in a list
+ self.assertIncludes(
+ {
+ room_id_to_human_name_map[room_id]
+ for room_id in response_body["extensions"]["account_data"]
+ .get("rooms")
+ .keys()
+ },
+ {"room1", "room3", "room4", "room5"},
+ exact=True,
+ )
+
+ # Empty list will return nothing
+ sync_body = {
+ **main_sync_body,
+ "extensions": {
+ "account_data": {
+ "enabled": True,
+ "lists": [],
+ "rooms": [],
+ }
+ },
+ }
+ response_body, _ = self.do_sync(sync_body, tok=user1_tok)
+
+ # room1: ❌ Not requested
+ # room2: ❌ Not requested
+ # room3: ❌ Not requested
+ # room4: ❌ Not requested
+ # room5: ❌ Not requested
+ self.assertIncludes(
+ {
+ room_id_to_human_name_map[room_id]
+ for room_id in response_body["extensions"]["account_data"]
+ .get("rooms")
+ .keys()
+ },
+ set(),
+ exact=True,
+ )
+
+ # Try wildcard and none
+ sync_body = {
+ **main_sync_body,
+ "extensions": {
+ "account_data": {
+ "enabled": True,
+ "lists": ["*"],
+ "rooms": [],
+ }
+ },
+ }
+ response_body, _ = self.do_sync(sync_body, tok=user1_tok)
+
+ # room1: ❌ Not requested
+ # room2: ❌ Not requested
+ # room3: ✅ Shows up because of default `lists` wildcard and is in a list
+ # room4: ✅ Shows up because of default `lists` wildcard and is in a list
+ # room5: ✅ Shows up because of default `lists` wildcard and is in a list
+ self.assertIncludes(
+ {
+ room_id_to_human_name_map[room_id]
+ for room_id in response_body["extensions"]["account_data"]
+ .get("rooms")
+ .keys()
+ },
+ {"room3", "room4", "room5"},
+ exact=True,
+ )
+
class SlidingSyncToDeviceExtensionTestCase(SlidingSyncBase):
"""Tests for the to-device sliding sync extension"""
@@ -5871,244 +6044,6 @@ class SlidingSyncAccountDataExtensionTestCase(SlidingSyncBase):
exact=True,
)
- def test_room_account_data_relevant_rooms(self) -> None:
- """
- Test out different variations of `lists`/`rooms` we are requesting account data for.
- """
- user1_id = self.register_user("user1", "pass")
- user1_tok = self.login(user1_id, "pass")
-
- # Create a room and add some room account data
- room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
- self.get_success(
- self.account_data_handler.add_account_data_to_room(
- user_id=user1_id,
- room_id=room_id1,
- account_data_type="org.matrix.roorarraz",
- content={"roo": "rar"},
- )
- )
-
- # Create another room with some room account data
- room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok)
- self.get_success(
- self.account_data_handler.add_account_data_to_room(
- user_id=user1_id,
- room_id=room_id2,
- account_data_type="org.matrix.roorarraz",
- content={"roo": "rar"},
- )
- )
-
- # Create another room with some room account data
- room_id3 = self.helper.create_room_as(user1_id, tok=user1_tok)
- self.get_success(
- self.account_data_handler.add_account_data_to_room(
- user_id=user1_id,
- room_id=room_id3,
- account_data_type="org.matrix.roorarraz",
- content={"roo": "rar"},
- )
- )
-
- # Create another room with some room account data
- room_id4 = self.helper.create_room_as(user1_id, tok=user1_tok)
- self.get_success(
- self.account_data_handler.add_account_data_to_room(
- user_id=user1_id,
- room_id=room_id4,
- account_data_type="org.matrix.roorarraz",
- content={"roo": "rar"},
- )
- )
-
- # Create another room with some room account data
- room_id5 = self.helper.create_room_as(user1_id, tok=user1_tok)
- self.get_success(
- self.account_data_handler.add_account_data_to_room(
- user_id=user1_id,
- room_id=room_id5,
- account_data_type="org.matrix.roorarraz",
- content={"roo": "rar"},
- )
- )
-
- room_id_to_human_name_map = {
- room_id1: "room1",
- room_id2: "room2",
- room_id3: "room3",
- room_id4: "room4",
- room_id5: "room5",
- }
-
- main_sync_body = {
- "lists": {
- # We expect this list range to include room5 and room4
- "foo-list": {
- "ranges": [[0, 1]],
- "required_state": [],
- "timeline_limit": 0,
- },
- # We expect this list range to include room5, room4, room3
- "bar-list": {
- "ranges": [[0, 2]],
- "required_state": [],
- "timeline_limit": 0,
- },
- },
- "room_subscriptions": {
- room_id1: {
- "required_state": [],
- "timeline_limit": 0,
- }
- },
- }
-
- # Mix lists and rooms
- sync_body = {
- **main_sync_body,
- "extensions": {
- "account_data": {
- "enabled": True,
- "lists": ["foo-list", "non-existent-list"],
- "rooms": [room_id1, room_id2, "!non-existent-room"],
- }
- },
- }
- response_body, _ = self.do_sync(sync_body, tok=user1_tok)
-
- # room1: ✅ Requested via `rooms` and a room subscription exists
- # room2: ❌ Requested via `rooms` but not in the response (from lists or room subscriptions)
- # room3: ❌ Not requested
- # room4: ✅ Shows up because requested via `lists` and list exists in the response
- # room5: ✅ Shows up because requested via `lists` and list exists in the response
- self.assertIncludes(
- {
- room_id_to_human_name_map[room_id]
- for room_id in response_body["extensions"]["account_data"]
- .get("rooms")
- .keys()
- },
- {"room1", "room4", "room5"},
- exact=True,
- )
-
- # Try wildcards (this is the default)
- sync_body = {
- **main_sync_body,
- "extensions": {
- "account_data": {
- "enabled": True,
- # "lists": ["*"],
- # "rooms": ["*"],
- }
- },
- }
- response_body, _ = self.do_sync(sync_body, tok=user1_tok)
-
- # room1: ✅ Shows up because of default `rooms` wildcard and is in one of the room subscriptions
- # room2: ❌ Not requested
- # room3: ✅ Shows up because of default `lists` wildcard and is in a list
- # room4: ✅ Shows up because of default `lists` wildcard and is in a list
- # room5: ✅ Shows up because of default `lists` wildcard and is in a list
- self.assertIncludes(
- {
- room_id_to_human_name_map[room_id]
- for room_id in response_body["extensions"]["account_data"]
- .get("rooms")
- .keys()
- },
- {"room1", "room3", "room4", "room5"},
- exact=True,
- )
-
- # Empty list will return nothing
- sync_body = {
- **main_sync_body,
- "extensions": {
- "account_data": {
- "enabled": True,
- "lists": [],
- "rooms": [],
- }
- },
- }
- response_body, _ = self.do_sync(sync_body, tok=user1_tok)
-
- # room1: ❌ Not requested
- # room2: ❌ Not requested
- # room3: ❌ Not requested
- # room4: ❌ Not requested
- # room5: ❌ Not requested
- self.assertIncludes(
- {
- room_id_to_human_name_map[room_id]
- for room_id in response_body["extensions"]["account_data"]
- .get("rooms")
- .keys()
- },
- set(),
- exact=True,
- )
-
- # Try wildcard and none
- sync_body = {
- **main_sync_body,
- "extensions": {
- "account_data": {
- "enabled": True,
- "lists": ["*"],
- "rooms": [],
- }
- },
- }
- response_body, _ = self.do_sync(sync_body, tok=user1_tok)
-
- # room1: ❌ Not requested
- # room2: ❌ Not requested
- # room3: ✅ Shows up because of default `lists` wildcard and is in a list
- # room4: ✅ Shows up because of default `lists` wildcard and is in a list
- # room5: ✅ Shows up because of default `lists` wildcard and is in a list
- self.assertIncludes(
- {
- room_id_to_human_name_map[room_id]
- for room_id in response_body["extensions"]["account_data"]
- .get("rooms")
- .keys()
- },
- {"room3", "room4", "room5"},
- exact=True,
- )
-
- # Try requesting a room that is only in a list
- sync_body = {
- **main_sync_body,
- "extensions": {
- "account_data": {
- "enabled": True,
- "lists": [],
- "rooms": [room_id5],
- }
- },
- }
- response_body, _ = self.do_sync(sync_body, tok=user1_tok)
-
- # room1: ❌ Not requested
- # room2: ❌ Not requested
- # room3: ❌ Not requested
- # room4: ❌ Not requested
- # room5: ✅ Requested via `rooms` and is in a list
- self.assertIncludes(
- {
- room_id_to_human_name_map[room_id]
- for room_id in response_body["extensions"]["account_data"]
- .get("rooms")
- .keys()
- },
- {"room5"},
- exact=True,
- )
-
def test_wait_for_new_data(self) -> None:
"""
Test to make sure that the Sliding Sync request waits for new data to arrive.
@@ -6220,3 +6155,319 @@ class SlidingSyncAccountDataExtensionTestCase(SlidingSyncBase):
self.assertIsNotNone(
channel.json_body["extensions"]["account_data"].get("rooms")
)
+
+class SlidingSyncReceiptsExtensionTestCase(unittest.HomeserverTestCase):
+ """Tests for the receipts sliding sync extension"""
+
+ servlets = [
+ synapse.rest.admin.register_servlets,
+ login.register_servlets,
+ room.register_servlets,
+ sync.register_servlets,
+ ]
+
+ def default_config(self) -> JsonDict:
+ config = super().default_config()
+ # Enable sliding sync
+ config["experimental_features"] = {"msc3575_enabled": True}
+ return config
+
+ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
+ self.store = hs.get_datastores().main
+ self.event_sources = hs.get_event_sources()
+ self.e2e_keys_handler = hs.get_e2e_keys_handler()
+ self.account_data_handler = hs.get_account_data_handler()
+ self.notifier = hs.get_notifier()
+ self.sync_endpoint = (
+ "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"
+ )
+
+ # TODO: Remove once https://github.com/element-hq/synapse/pull/17481 lands
+ def _bump_notifier_wait_for_events(self, user_id: str) -> None:
+ """
+ Wake-up a `notifier.wait_for_events(user_id)` call without affecting the Sliding
+ Sync results.
+ """
+ # We're expecting some new activity from this point onwards
+ from_token = self.event_sources.get_current_token()
+
+ triggered_notifier_wait_for_events = False
+
+ async def _on_new_acivity(
+ before_token: StreamToken, after_token: StreamToken
+ ) -> bool:
+ nonlocal triggered_notifier_wait_for_events
+ triggered_notifier_wait_for_events = True
+ return True
+
+ # Listen for some new activity for the user. We're just trying to confirm that
+ # our bump below actually does what we think it does (triggers new activity for
+ # the user).
+ result_awaitable = self.notifier.wait_for_events(
+ user_id,
+ 1000,
+ _on_new_acivity,
+ from_token=from_token,
+ )
+
+ # Update the account data so that `notifier.wait_for_events(...)` wakes up.
+ # We're bumping account data because it won't show up in the Sliding Sync
+ # response so it won't affect whether we have results.
+ self.get_success(
+ self.account_data_handler.add_account_data_for_user(
+ user_id,
+ "org.matrix.foobarbaz",
+ {"foo": "bar"},
+ )
+ )
+
+ # Wait for our notifier result
+ self.get_success(result_awaitable)
+
+ if not triggered_notifier_wait_for_events:
+ raise AssertionError(
+ "Expected `notifier.wait_for_events(...)` to be triggered"
+ )
+
+ def test_no_data_initial_sync(self) -> None:
+ """
+ Test that enabling e2ee extension works during an intitial sync, even if there
+ is no-data
+ """
+ user1_id = self.register_user("user1", "pass")
+ user1_tok = self.login(user1_id, "pass")
+
+ # Make an initial Sliding Sync request with the e2ee extension enabled
+ channel = self.make_request(
+ "POST",
+ self.sync_endpoint,
+ {
+ "lists": {},
+ "extensions": {
+ "receipts": {
+ "enabled": True,
+ }
+ },
+ },
+ access_token=user1_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.json_body)
+
+ # Device list updates are only present for incremental syncs
+ self.assertIsNone(channel.json_body["extensions"]["e2ee"].get("device_lists"))
+
+ # Both of these should be present even when empty
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]["device_one_time_keys_count"],
+ {
+ # This is always present because of
+ # https://github.com/element-hq/element-android/issues/3725 and
+ # https://github.com/matrix-org/synapse/issues/10456
+ "signed_curve25519": 0
+ },
+ )
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]["device_unused_fallback_key_types"],
+ [],
+ )
+
+ def test_no_data_incremental_sync(self) -> None:
+ """
+ Test that enabling e2ee extension works during an incremental sync, even if
+ there is no-data
+ """
+ user1_id = self.register_user("user1", "pass")
+ user1_tok = self.login(user1_id, "pass")
+
+ from_token = self.event_sources.get_current_token()
+
+ # Make an incremental Sliding Sync request with the e2ee extension enabled
+ channel = self.make_request(
+ "POST",
+ self.sync_endpoint
+ + f"?pos={self.get_success(from_token.to_string(self.store))}",
+ {
+ "lists": {},
+ "extensions": {
+ "e2ee": {
+ "enabled": True,
+ }
+ },
+ },
+ access_token=user1_tok,
+ )
+ self.assertEqual(channel.code, 200, channel.json_body)
+
+ # Device list shows up for incremental syncs
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]
+ .get("device_lists", {})
+ .get("changed"),
+ [],
+ )
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"].get("device_lists", {}).get("left"),
+ [],
+ )
+
+ # Both of these should be present even when empty
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]["device_one_time_keys_count"],
+ {
+ # Note that "signed_curve25519" is always returned in key count responses
+ # regardless of whether we uploaded any keys for it. This is necessary until
+ # https://github.com/matrix-org/matrix-doc/issues/3298 is fixed.
+ #
+ # Also related:
+ # https://github.com/element-hq/element-android/issues/3725 and
+ # https://github.com/matrix-org/synapse/issues/10456
+ "signed_curve25519": 0
+ },
+ )
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]["device_unused_fallback_key_types"],
+ [],
+ )
+
+ def test_wait_for_new_data(self) -> None:
+ """
+ Test to make sure that the Sliding Sync request waits for new data to arrive.
+
+ (Only applies to incremental syncs with a `timeout` specified)
+ """
+ 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")
+ test_device_id = "TESTDEVICE"
+ user3_id = self.register_user("user3", "pass")
+ user3_tok = self.login(user3_id, "pass", device_id=test_device_id)
+
+ room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
+ self.helper.join(room_id, user1_id, tok=user1_tok)
+ self.helper.join(room_id, user3_id, tok=user3_tok)
+
+ from_token = self.event_sources.get_current_token()
+
+ # Make the Sliding Sync request
+ channel = self.make_request(
+ "POST",
+ self.sync_endpoint
+ + "?timeout=10000"
+ + f"&pos={self.get_success(from_token.to_string(self.store))}",
+ {
+ "lists": {},
+ "extensions": {
+ "e2ee": {
+ "enabled": True,
+ }
+ },
+ },
+ access_token=user1_tok,
+ await_result=False,
+ )
+ # Block for 5 seconds to make sure we are `notifier.wait_for_events(...)`
+ with self.assertRaises(TimedOutException):
+ channel.await_result(timeout_ms=5000)
+ # Bump the device lists to trigger new results
+ # Have user3 update their device list
+ device_update_channel = self.make_request(
+ "PUT",
+ f"/devices/{test_device_id}",
+ {
+ "display_name": "New Device Name",
+ },
+ access_token=user3_tok,
+ )
+ self.assertEqual(
+ device_update_channel.code, 200, device_update_channel.json_body
+ )
+ # Should respond before the 10 second timeout
+ channel.await_result(timeout_ms=3000)
+ self.assertEqual(channel.code, 200, channel.json_body)
+
+ # We should see the device list update
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]
+ .get("device_lists", {})
+ .get("changed"),
+ [user3_id],
+ )
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"].get("device_lists", {}).get("left"),
+ [],
+ )
+
+ def test_wait_for_new_data_timeout(self) -> None:
+ """
+ Test to make sure that the Sliding Sync request waits for new data to arrive but
+ no data ever arrives so we timeout. We're also making sure that the default data
+ from the E2EE extension doesn't trigger a false-positive for new data (see
+ `device_one_time_keys_count.signed_curve25519`).
+ """
+ user1_id = self.register_user("user1", "pass")
+ user1_tok = self.login(user1_id, "pass")
+
+ from_token = self.event_sources.get_current_token()
+
+ # Make the Sliding Sync request
+ channel = self.make_request(
+ "POST",
+ self.sync_endpoint
+ + "?timeout=10000"
+ + f"&pos={self.get_success(from_token.to_string(self.store))}",
+ {
+ "lists": {},
+ "extensions": {
+ "e2ee": {
+ "enabled": True,
+ }
+ },
+ },
+ access_token=user1_tok,
+ await_result=False,
+ )
+ # Block for 5 seconds to make sure we are `notifier.wait_for_events(...)`
+ with self.assertRaises(TimedOutException):
+ channel.await_result(timeout_ms=5000)
+ # Wake-up `notifier.wait_for_events(...)` that will cause us test
+ # `SlidingSyncResult.__bool__` for new results.
+ self._bump_notifier_wait_for_events(user1_id)
+ # Block for a little bit more to ensure we don't see any new results.
+ with self.assertRaises(TimedOutException):
+ channel.await_result(timeout_ms=4000)
+ # Wait for the sync to complete (wait for the rest of the 10 second timeout,
+ # 5000 + 4000 + 1200 > 10000)
+ channel.await_result(timeout_ms=1200)
+ self.assertEqual(channel.code, 200, channel.json_body)
+
+ # Device lists are present for incremental syncs but empty because no device changes
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]
+ .get("device_lists", {})
+ .get("changed"),
+ [],
+ )
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"].get("device_lists", {}).get("left"),
+ [],
+ )
+
+ # Both of these should be present even when empty
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]["device_one_time_keys_count"],
+ {
+ # Note that "signed_curve25519" is always returned in key count responses
+ # regardless of whether we uploaded any keys for it. This is necessary until
+ # https://github.com/matrix-org/matrix-doc/issues/3298 is fixed.
+ #
+ # Also related:
+ # https://github.com/element-hq/element-android/issues/3725 and
+ # https://github.com/matrix-org/synapse/issues/10456
+ "signed_curve25519": 0
+ },
+ )
+ self.assertEqual(
+ channel.json_body["extensions"]["e2ee"]["device_unused_fallback_key_types"],
+ [],
+ )
|