Sliding Sync: Split and move tests (#17504)
Split and move Sliding Sync tests so we have some more sane test file sizes
This commit is contained in:
parent
1d6186265a
commit
39731bb205
|
@ -0,0 +1 @@
|
||||||
|
Split and move Sliding Sync tests so we have some more sane test file sizes.
|
|
@ -0,0 +1,13 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
|
@ -0,0 +1,453 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import EventTypes
|
||||||
|
from synapse.rest.client import login, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import SlidingSyncStreamToken
|
||||||
|
from synapse.types.handlers import SlidingSyncConfig
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncConnectionTrackingTestCase(SlidingSyncBase):
|
||||||
|
"""
|
||||||
|
Test connection tracking in the Sliding Sync API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.storage_controllers = hs.get_storage_controllers()
|
||||||
|
|
||||||
|
def test_rooms_required_state_incremental_sync_LIVE(self) -> None:
|
||||||
|
"""Test that we only get state updates in incremental sync for rooms
|
||||||
|
we've already seen (LIVE).
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.RoomHistoryVisibility, ""],
|
||||||
|
# This one doesn't exist in the room
|
||||||
|
[EventTypes.Name, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response_body, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
state_map[(EventTypes.RoomHistoryVisibility, "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send a state event
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1, EventTypes.Name, body={"name": "foo"}, tok=user2_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotIn("initial", response_body["rooms"][room_id1])
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Name, "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@parameterized.expand([(False,), (True,)])
|
||||||
|
def test_rooms_timeline_incremental_sync_PREVIOUSLY(self, limited: bool) -> None:
|
||||||
|
"""
|
||||||
|
Test getting room data where we have previously sent down the room, but
|
||||||
|
we missed sending down some timeline events previously and so its status
|
||||||
|
is considered PREVIOUSLY.
|
||||||
|
|
||||||
|
There are two versions of this test, one where there are more messages
|
||||||
|
than the timeline limit, and one where there isn't.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
timeline_limit = 5
|
||||||
|
conn_id = "conn_id"
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 0]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": timeline_limit,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"conn_id": "conn_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
# The first room gets sent down the initial sync
|
||||||
|
response_body, initial_from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id1}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now send down some events in room1 (depending on the test param).
|
||||||
|
expected_events = [] # The set of events in the timeline
|
||||||
|
if limited:
|
||||||
|
for _ in range(10):
|
||||||
|
resp = self.helper.send(room_id1, "msg1", tok=user1_tok)
|
||||||
|
expected_events.append(resp["event_id"])
|
||||||
|
else:
|
||||||
|
resp = self.helper.send(room_id1, "msg1", tok=user1_tok)
|
||||||
|
expected_events.append(resp["event_id"])
|
||||||
|
|
||||||
|
# A second messages happens in the other room, so room1 won't get sent down.
|
||||||
|
self.helper.send(room_id2, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
# Only the second room gets sent down sync.
|
||||||
|
response_body, from_token = self.do_sync(
|
||||||
|
sync_body, since=initial_from_token, tok=user1_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id2}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME: This is a hack to record that the first room wasn't sent down
|
||||||
|
# sync, as we don't implement that currently.
|
||||||
|
sliding_sync_handler = self.hs.get_sliding_sync_handler()
|
||||||
|
requester = self.get_success(
|
||||||
|
self.hs.get_auth().get_user_by_access_token(user1_tok)
|
||||||
|
)
|
||||||
|
sync_config = SlidingSyncConfig(
|
||||||
|
user=requester.user,
|
||||||
|
requester=requester,
|
||||||
|
conn_id=conn_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
parsed_initial_from_token = self.get_success(
|
||||||
|
SlidingSyncStreamToken.from_string(self.store, initial_from_token)
|
||||||
|
)
|
||||||
|
connection_position = self.get_success(
|
||||||
|
sliding_sync_handler.connection_store.record_rooms(
|
||||||
|
sync_config,
|
||||||
|
parsed_initial_from_token,
|
||||||
|
sent_room_ids=[],
|
||||||
|
unsent_room_ids=[room_id1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME: Now fix up `from_token` with new connect position above.
|
||||||
|
parsed_from_token = self.get_success(
|
||||||
|
SlidingSyncStreamToken.from_string(self.store, from_token)
|
||||||
|
)
|
||||||
|
parsed_from_token = SlidingSyncStreamToken(
|
||||||
|
stream_token=parsed_from_token.stream_token,
|
||||||
|
connection_position=connection_position,
|
||||||
|
)
|
||||||
|
from_token = self.get_success(parsed_from_token.to_string(self.store))
|
||||||
|
|
||||||
|
# We now send another event to room1, so we should sync all the missing events.
|
||||||
|
resp = self.helper.send(room_id1, "msg2", tok=user1_tok)
|
||||||
|
expected_events.append(resp["event_id"])
|
||||||
|
|
||||||
|
# This sync should contain the messages from room1 not yet sent down.
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id1}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
self.assertNotIn("initial", response_body["rooms"][room_id1])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[ev["event_id"] for ev in response_body["rooms"][room_id1]["timeline"]],
|
||||||
|
expected_events[-timeline_limit:],
|
||||||
|
)
|
||||||
|
self.assertEqual(response_body["rooms"][room_id1]["limited"], limited)
|
||||||
|
self.assertEqual(response_body["rooms"][room_id1].get("required_state"), None)
|
||||||
|
|
||||||
|
def test_rooms_required_state_incremental_sync_PREVIOUSLY(self) -> None:
|
||||||
|
"""
|
||||||
|
Test getting room data where we have previously sent down the room, but
|
||||||
|
we missed sending down some state previously and so its status is
|
||||||
|
considered PREVIOUSLY.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
conn_id = "conn_id"
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 0]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.RoomHistoryVisibility, ""],
|
||||||
|
# This one doesn't exist in the room
|
||||||
|
[EventTypes.Name, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"conn_id": "conn_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
# The first room gets sent down the initial sync
|
||||||
|
response_body, initial_from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id1}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now send down some state in room1
|
||||||
|
resp = self.helper.send_state(
|
||||||
|
room_id1, EventTypes.Name, {"name": "foo"}, tok=user1_tok
|
||||||
|
)
|
||||||
|
name_change_id = resp["event_id"]
|
||||||
|
|
||||||
|
# A second messages happens in the other room, so room1 won't get sent down.
|
||||||
|
self.helper.send(room_id2, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
# Only the second room gets sent down sync.
|
||||||
|
response_body, from_token = self.do_sync(
|
||||||
|
sync_body, since=initial_from_token, tok=user1_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id2}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME: This is a hack to record that the first room wasn't sent down
|
||||||
|
# sync, as we don't implement that currently.
|
||||||
|
sliding_sync_handler = self.hs.get_sliding_sync_handler()
|
||||||
|
requester = self.get_success(
|
||||||
|
self.hs.get_auth().get_user_by_access_token(user1_tok)
|
||||||
|
)
|
||||||
|
sync_config = SlidingSyncConfig(
|
||||||
|
user=requester.user,
|
||||||
|
requester=requester,
|
||||||
|
conn_id=conn_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
parsed_initial_from_token = self.get_success(
|
||||||
|
SlidingSyncStreamToken.from_string(self.store, initial_from_token)
|
||||||
|
)
|
||||||
|
connection_position = self.get_success(
|
||||||
|
sliding_sync_handler.connection_store.record_rooms(
|
||||||
|
sync_config,
|
||||||
|
parsed_initial_from_token,
|
||||||
|
sent_room_ids=[],
|
||||||
|
unsent_room_ids=[room_id1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME: Now fix up `from_token` with new connect position above.
|
||||||
|
parsed_from_token = self.get_success(
|
||||||
|
SlidingSyncStreamToken.from_string(self.store, from_token)
|
||||||
|
)
|
||||||
|
parsed_from_token = SlidingSyncStreamToken(
|
||||||
|
stream_token=parsed_from_token.stream_token,
|
||||||
|
connection_position=connection_position,
|
||||||
|
)
|
||||||
|
from_token = self.get_success(parsed_from_token.to_string(self.store))
|
||||||
|
|
||||||
|
# We now send another event to room1, so we should sync all the missing state.
|
||||||
|
self.helper.send(room_id1, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
# This sync should contain the state changes from room1.
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id1}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
self.assertNotIn("initial", response_body["rooms"][room_id1])
|
||||||
|
|
||||||
|
# We should only see the name change.
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
ev["event_id"]
|
||||||
|
for ev in response_body["rooms"][room_id1]["required_state"]
|
||||||
|
],
|
||||||
|
[name_change_id],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_required_state_incremental_sync_NEVER(self) -> None:
|
||||||
|
"""
|
||||||
|
Test getting `required_state` where we have NEVER sent down the room before
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 0]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.RoomHistoryVisibility, ""],
|
||||||
|
# This one doesn't exist in the room
|
||||||
|
[EventTypes.Name, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# A message happens in the other room, so room1 won't get sent down.
|
||||||
|
self.helper.send(room_id2, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
# Only the second room gets sent down sync.
|
||||||
|
response_body, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id2}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now send another event to room1, so we should send down the full
|
||||||
|
# room.
|
||||||
|
self.helper.send(room_id1, "msg2", tok=user1_tok)
|
||||||
|
|
||||||
|
# This sync should contain the messages from room1 not yet sent down.
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id1}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response_body["rooms"][room_id1]["initial"], True)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
state_map[(EventTypes.RoomHistoryVisibility, "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_timeline_incremental_sync_NEVER(self) -> None:
|
||||||
|
"""
|
||||||
|
Test getting timeline room data where we have NEVER sent down the room
|
||||||
|
before
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 0]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_events = []
|
||||||
|
for _ in range(4):
|
||||||
|
resp = self.helper.send(room_id1, "msg", tok=user1_tok)
|
||||||
|
expected_events.append(resp["event_id"])
|
||||||
|
|
||||||
|
# A message happens in the other room, so room1 won't get sent down.
|
||||||
|
self.helper.send(room_id2, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
# Only the second room gets sent down sync.
|
||||||
|
response_body, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id2}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# We now send another event to room1 so it comes down sync
|
||||||
|
resp = self.helper.send(room_id1, "msg2", tok=user1_tok)
|
||||||
|
expected_events.append(resp["event_id"])
|
||||||
|
|
||||||
|
# This sync should contain the messages from room1 not yet sent down.
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"].keys(), {room_id1}, response_body["rooms"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[ev["event_id"] for ev in response_body["rooms"][room_id1]["timeline"]],
|
||||||
|
expected_events,
|
||||||
|
)
|
||||||
|
self.assertEqual(response_body["rooms"][room_id1]["limited"], True)
|
||||||
|
self.assertEqual(response_body["rooms"][room_id1]["initial"], True)
|
|
@ -0,0 +1,495 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import AccountDataTypes
|
||||||
|
from synapse.rest.client import login, room, sendtodevice, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import StreamKeyType
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
from tests.server import TimedOutException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncAccountDataExtensionTestCase(SlidingSyncBase):
|
||||||
|
"""Tests for the account_data sliding sync extension"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
sendtodevice.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.account_data_handler = hs.get_account_data_handler()
|
||||||
|
|
||||||
|
def test_no_data_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that enabling the account_data 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 account_data extension enabled
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"account_data": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
global_event["type"]
|
||||||
|
for global_event in response_body["extensions"]["account_data"].get(
|
||||||
|
"global"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
# Even though we don't have any global account data set, Synapse saves some
|
||||||
|
# default push rules for us.
|
||||||
|
{AccountDataTypes.PUSH_RULES},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["account_data"].get("rooms").keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_data_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that enabling account_data 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")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"account_data": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the account_data extension enabled
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# There has been no account data changes since the `from_token` so we shouldn't
|
||||||
|
# see any account data here.
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
global_event["type"]
|
||||||
|
for global_event in response_body["extensions"]["account_data"].get(
|
||||||
|
"global"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["account_data"].get("rooms").keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_global_account_data_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
On initial sync, we should return all global account data on initial sync.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
# Update the global account data
|
||||||
|
self.get_success(
|
||||||
|
self.account_data_handler.add_account_data_for_user(
|
||||||
|
user_id=user1_id,
|
||||||
|
account_data_type="org.matrix.foobarbaz",
|
||||||
|
content={"foo": "bar"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make an initial Sliding Sync request with the account_data extension enabled
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"account_data": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# It should show us all of the global account data
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
global_event["type"]
|
||||||
|
for global_event in response_body["extensions"]["account_data"].get(
|
||||||
|
"global"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{AccountDataTypes.PUSH_RULES, "org.matrix.foobarbaz"},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["account_data"].get("rooms").keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_global_account_data_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
On incremental sync, we should only account data that has changed since the
|
||||||
|
`from_token`.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
# Add some global account data
|
||||||
|
self.get_success(
|
||||||
|
self.account_data_handler.add_account_data_for_user(
|
||||||
|
user_id=user1_id,
|
||||||
|
account_data_type="org.matrix.foobarbaz",
|
||||||
|
content={"foo": "bar"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"account_data": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Add some other global account data
|
||||||
|
self.get_success(
|
||||||
|
self.account_data_handler.add_account_data_for_user(
|
||||||
|
user_id=user1_id,
|
||||||
|
account_data_type="org.matrix.doodardaz",
|
||||||
|
content={"doo": "dar"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the account_data extension enabled
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
global_event["type"]
|
||||||
|
for global_event in response_body["extensions"]["account_data"].get(
|
||||||
|
"global"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
# We should only see the new global account data that happened after the `from_token`
|
||||||
|
{"org.matrix.doodardaz"},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["account_data"].get("rooms").keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_room_account_data_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
On initial sync, we return all account data for a given room but only for
|
||||||
|
rooms that we request and are being returned in the Sliding Sync response.
|
||||||
|
"""
|
||||||
|
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"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make an initial Sliding Sync request with the account_data extension enabled
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"account_data": {
|
||||||
|
"enabled": True,
|
||||||
|
"rooms": [room_id1, room_id2],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertIsNotNone(response_body["extensions"]["account_data"].get("global"))
|
||||||
|
# Even though we requested room2, we only expect room1 to show up because that's
|
||||||
|
# the only room in the Sliding Sync response (room2 is not one of our room
|
||||||
|
# subscriptions or in a sliding window list).
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["account_data"].get("rooms").keys(),
|
||||||
|
{room_id1},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
event["type"]
|
||||||
|
for event in response_body["extensions"]["account_data"]
|
||||||
|
.get("rooms")
|
||||||
|
.get(room_id1)
|
||||||
|
},
|
||||||
|
{"org.matrix.roorarraz"},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_room_account_data_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
On incremental sync, we return all account data for a given room but only for
|
||||||
|
rooms that we request and are being returned in the Sliding Sync response.
|
||||||
|
"""
|
||||||
|
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"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"account_data": {
|
||||||
|
"enabled": True,
|
||||||
|
"rooms": [room_id1, room_id2],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Add some other room account data
|
||||||
|
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.roorarraz2",
|
||||||
|
content={"roo": "rar"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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.roorarraz2",
|
||||||
|
content={"roo": "rar"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the account_data extension enabled
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertIsNotNone(response_body["extensions"]["account_data"].get("global"))
|
||||||
|
# Even though we requested room2, we only expect room1 to show up because that's
|
||||||
|
# the only room in the Sliding Sync response (room2 is not one of our room
|
||||||
|
# subscriptions or in a sliding window list).
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["account_data"].get("rooms").keys(),
|
||||||
|
{room_id1},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# We should only see the new room account data that happened after the `from_token`
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
event["type"]
|
||||||
|
for event in response_body["extensions"]["account_data"]
|
||||||
|
.get("rooms")
|
||||||
|
.get(room_id1)
|
||||||
|
},
|
||||||
|
{"org.matrix.roorarraz2"},
|
||||||
|
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.
|
||||||
|
|
||||||
|
(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")
|
||||||
|
|
||||||
|
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"account_data": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the account_data extension enabled
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?timeout=10000&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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 global account data to trigger new results
|
||||||
|
self.get_success(
|
||||||
|
self.account_data_handler.add_account_data_for_user(
|
||||||
|
user1_id,
|
||||||
|
"org.matrix.foobarbaz",
|
||||||
|
{"foo": "bar"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 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 global account data update
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
global_event["type"]
|
||||||
|
for global_event in channel.json_body["extensions"]["account_data"].get(
|
||||||
|
"global"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{"org.matrix.foobarbaz"},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIncludes(
|
||||||
|
channel.json_body["extensions"]["account_data"].get("rooms").keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 account_data extension doesn't trigger a false-positive for new data.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"account_data": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?timeout=10000&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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,
|
||||||
|
# We choose `StreamKeyType.PRESENCE` because we're testing for account data
|
||||||
|
# and don't want to contaminate the account data results using
|
||||||
|
# `StreamKeyType.ACCOUNT_DATA`.
|
||||||
|
wake_stream_key=StreamKeyType.PRESENCE,
|
||||||
|
)
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
self.assertIsNotNone(
|
||||||
|
channel.json_body["extensions"]["account_data"].get("global")
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(
|
||||||
|
channel.json_body["extensions"]["account_data"].get("rooms")
|
||||||
|
)
|
|
@ -0,0 +1,441 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.rest.client import devices, login, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import JsonDict, StreamKeyType
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
from tests.server import TimedOutException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncE2eeExtensionTestCase(SlidingSyncBase):
|
||||||
|
"""Tests for the e2ee sliding sync extension"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
devices.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.e2e_keys_handler = hs.get_e2e_keys_handler()
|
||||||
|
|
||||||
|
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
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"e2ee": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Device list updates are only present for incremental syncs
|
||||||
|
self.assertIsNone(response_body["extensions"]["e2ee"].get("device_lists"))
|
||||||
|
|
||||||
|
# Both of these should be present even when empty
|
||||||
|
self.assertEqual(
|
||||||
|
response_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(
|
||||||
|
response_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")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"e2ee": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the e2ee extension enabled
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# Device list shows up for incremental syncs
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["e2ee"].get("device_lists", {}).get("changed"),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["e2ee"].get("device_lists", {}).get("left"),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Both of these should be present even when empty
|
||||||
|
self.assertEqual(
|
||||||
|
response_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(
|
||||||
|
response_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)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"e2ee": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + "?timeout=10000" + f"&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"e2ee": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?timeout=10000&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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, wake_stream_key=StreamKeyType.ACCOUNT_DATA
|
||||||
|
)
|
||||||
|
# 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"],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_device_lists(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that device list updates are included in the response
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
user4_id = self.register_user("user4", "pass")
|
||||||
|
user4_tok = self.login(user4_id, "pass")
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.helper.join(room_id, user4_id, tok=user4_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"e2ee": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Have user3 update their device list
|
||||||
|
channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
f"/devices/{test_device_id}",
|
||||||
|
{
|
||||||
|
"display_name": "New Device Name",
|
||||||
|
},
|
||||||
|
access_token=user3_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# User4 leaves the room
|
||||||
|
self.helper.leave(room_id, user4_id, tok=user4_tok)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the e2ee extension enabled
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# Device list updates show up
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["e2ee"].get("device_lists", {}).get("changed"),
|
||||||
|
[user3_id],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["e2ee"].get("device_lists", {}).get("left"),
|
||||||
|
[user4_id],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_device_one_time_keys_count(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `device_one_time_keys_count` are included in the response
|
||||||
|
"""
|
||||||
|
test_device_id = "TESTDEVICE"
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass", device_id=test_device_id)
|
||||||
|
|
||||||
|
# Upload one time keys for the user/device
|
||||||
|
keys: JsonDict = {
|
||||||
|
"alg1:k1": "key1",
|
||||||
|
"alg2:k2": {"key": "key2", "signatures": {"k1": "sig1"}},
|
||||||
|
"alg2:k3": {"key": "key3"},
|
||||||
|
}
|
||||||
|
upload_keys_response = self.get_success(
|
||||||
|
self.e2e_keys_handler.upload_keys_for_user(
|
||||||
|
user1_id, test_device_id, {"one_time_keys": keys}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertDictEqual(
|
||||||
|
upload_keys_response,
|
||||||
|
{
|
||||||
|
"one_time_key_counts": {
|
||||||
|
"alg1": 1,
|
||||||
|
"alg2": 2,
|
||||||
|
# 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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make a Sliding Sync request with the e2ee extension enabled
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"e2ee": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Check for those one time key counts
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["e2ee"].get("device_one_time_keys_count"),
|
||||||
|
{
|
||||||
|
"alg1": 1,
|
||||||
|
"alg2": 2,
|
||||||
|
# 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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_device_unused_fallback_key_types(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `device_unused_fallback_key_types` are included in the response
|
||||||
|
"""
|
||||||
|
test_device_id = "TESTDEVICE"
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass", device_id=test_device_id)
|
||||||
|
|
||||||
|
# We shouldn't have any unused fallback keys yet
|
||||||
|
res = self.get_success(
|
||||||
|
self.store.get_e2e_unused_fallback_key_types(user1_id, test_device_id)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, [])
|
||||||
|
|
||||||
|
# Upload a fallback key for the user/device
|
||||||
|
self.get_success(
|
||||||
|
self.e2e_keys_handler.upload_keys_for_user(
|
||||||
|
user1_id,
|
||||||
|
test_device_id,
|
||||||
|
{"fallback_keys": {"alg1:k1": "fallback_key1"}},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# We should now have an unused alg1 key
|
||||||
|
fallback_res = self.get_success(
|
||||||
|
self.store.get_e2e_unused_fallback_key_types(user1_id, test_device_id)
|
||||||
|
)
|
||||||
|
self.assertEqual(fallback_res, ["alg1"], fallback_res)
|
||||||
|
|
||||||
|
# Make a Sliding Sync request with the e2ee extension enabled
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"e2ee": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Check for the unused fallback key types
|
||||||
|
self.assertListEqual(
|
||||||
|
response_body["extensions"]["e2ee"].get("device_unused_fallback_key_types"),
|
||||||
|
["alg1"],
|
||||||
|
)
|
|
@ -0,0 +1,679 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import EduTypes, ReceiptTypes
|
||||||
|
from synapse.rest.client import login, receipts, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import StreamKeyType
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
from tests.server import TimedOutException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncReceiptsExtensionTestCase(SlidingSyncBase):
|
||||||
|
"""Tests for the receipts sliding sync extension"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
receipts.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
|
||||||
|
def test_no_data_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that enabling the receipts 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 receipts extension enabled
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"receipts": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"].get("rooms").keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_data_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that enabling receipts 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")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"receipts": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the receipts extension enabled
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"].get("rooms").keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_receipts_initial_sync_with_timeline(self) -> None:
|
||||||
|
"""
|
||||||
|
On initial sync, we only return receipts for events in a given room's timeline.
|
||||||
|
|
||||||
|
We also make sure that we only return receipts for rooms that we request and are
|
||||||
|
already being returned in the Sliding Sync response.
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
user3_id = self.register_user("user3", "pass")
|
||||||
|
user3_tok = self.login(user3_id, "pass")
|
||||||
|
user4_id = self.register_user("user4", "pass")
|
||||||
|
user4_tok = self.login(user4_id, "pass")
|
||||||
|
|
||||||
|
# Create a room
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
self.helper.join(room_id1, user3_id, tok=user3_tok)
|
||||||
|
self.helper.join(room_id1, user4_id, tok=user4_tok)
|
||||||
|
room1_event_response1 = self.helper.send(
|
||||||
|
room_id1, body="new event1", tok=user2_tok
|
||||||
|
)
|
||||||
|
room1_event_response2 = self.helper.send(
|
||||||
|
room_id1, body="new event2", tok=user2_tok
|
||||||
|
)
|
||||||
|
# User1 reads the last event
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id1}/receipt/{ReceiptTypes.READ}/{room1_event_response2['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# User2 reads the last event
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id1}/receipt/{ReceiptTypes.READ}/{room1_event_response2['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user2_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# User3 reads the first event
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id1}/receipt/{ReceiptTypes.READ}/{room1_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user3_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# User4 privately reads the last event (make sure this doesn't leak to the other users)
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id1}/receipt/{ReceiptTypes.READ_PRIVATE}/{room1_event_response2['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user4_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Create another room
|
||||||
|
room_id2 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id2, user1_id, tok=user1_tok)
|
||||||
|
self.helper.join(room_id2, user3_id, tok=user3_tok)
|
||||||
|
self.helper.join(room_id2, user4_id, tok=user4_tok)
|
||||||
|
room2_event_response1 = self.helper.send(
|
||||||
|
room_id2, body="new event2", tok=user2_tok
|
||||||
|
)
|
||||||
|
# User1 reads the last event
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id2}/receipt/{ReceiptTypes.READ}/{room2_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# User2 reads the last event
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id2}/receipt/{ReceiptTypes.READ}/{room2_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user2_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# User4 privately reads the last event (make sure this doesn't leak to the other users)
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id2}/receipt/{ReceiptTypes.READ_PRIVATE}/{room2_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user4_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Make an initial Sliding Sync request with the receipts extension enabled
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [],
|
||||||
|
# On initial sync, we only have receipts for events in the timeline
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"receipts": {
|
||||||
|
"enabled": True,
|
||||||
|
"rooms": [room_id1, room_id2],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Only the latest event in the room is in the timelie because the `timeline_limit` is 1
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1].get("timeline", [])
|
||||||
|
},
|
||||||
|
{room1_event_response2["event_id"]},
|
||||||
|
exact=True,
|
||||||
|
message=str(response_body["rooms"][room_id1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Even though we requested room2, we only expect room1 to show up because that's
|
||||||
|
# the only room in the Sliding Sync response (room2 is not one of our room
|
||||||
|
# subscriptions or in a sliding window list).
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"].get("rooms").keys(),
|
||||||
|
{room_id1},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# Sanity check that it's the correct ephemeral event type
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["type"],
|
||||||
|
EduTypes.RECEIPT,
|
||||||
|
)
|
||||||
|
# We can see user1 and user2 read receipts
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["content"][
|
||||||
|
room1_event_response2["event_id"]
|
||||||
|
][ReceiptTypes.READ].keys(),
|
||||||
|
{user1_id, user2_id},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# User1 did not have a private read receipt and we shouldn't leak others'
|
||||||
|
# private read receipts
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["content"][
|
||||||
|
room1_event_response2["event_id"]
|
||||||
|
]
|
||||||
|
.get(ReceiptTypes.READ_PRIVATE, {})
|
||||||
|
.keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We shouldn't see receipts for event2 since it wasn't in the timeline and this is an initial sync
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["content"].get(
|
||||||
|
room1_event_response1["event_id"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_receipts_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
On incremental sync, we return all receipts in the token range for a given room
|
||||||
|
but only for rooms that we request and are being returned in the Sliding Sync
|
||||||
|
response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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")
|
||||||
|
user3_id = self.register_user("user3", "pass")
|
||||||
|
user3_tok = self.login(user3_id, "pass")
|
||||||
|
|
||||||
|
# Create room1
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
self.helper.join(room_id1, user3_id, tok=user3_tok)
|
||||||
|
room1_event_response1 = self.helper.send(
|
||||||
|
room_id1, body="new event2", tok=user2_tok
|
||||||
|
)
|
||||||
|
# User2 reads the last event (before the `from_token`)
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id1}/receipt/{ReceiptTypes.READ}/{room1_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user2_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Create room2
|
||||||
|
room_id2 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id2, user1_id, tok=user1_tok)
|
||||||
|
room2_event_response1 = self.helper.send(
|
||||||
|
room_id2, body="new event2", tok=user2_tok
|
||||||
|
)
|
||||||
|
# User1 reads the last event (before the `from_token`)
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id2}/receipt/{ReceiptTypes.READ}/{room2_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Create room3
|
||||||
|
room_id3 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id3, user1_id, tok=user1_tok)
|
||||||
|
self.helper.join(room_id3, user3_id, tok=user3_tok)
|
||||||
|
room3_event_response1 = self.helper.send(
|
||||||
|
room_id3, body="new event", tok=user2_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create room4
|
||||||
|
room_id4 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id4, user1_id, tok=user1_tok)
|
||||||
|
self.helper.join(room_id4, user3_id, tok=user3_tok)
|
||||||
|
event_response4 = self.helper.send(room_id4, body="new event", tok=user2_tok)
|
||||||
|
# User1 reads the last event (before the `from_token`)
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id4}/receipt/{ReceiptTypes.READ}/{event_response4['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
},
|
||||||
|
room_id3: {
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
},
|
||||||
|
room_id4: {
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"receipts": {
|
||||||
|
"enabled": True,
|
||||||
|
"rooms": [room_id1, room_id2, room_id3, room_id4],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Add some more read receipts after the `from_token`
|
||||||
|
#
|
||||||
|
# User1 reads room1
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id1}/receipt/{ReceiptTypes.READ}/{room1_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# User1 privately reads room2
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id2}/receipt/{ReceiptTypes.READ_PRIVATE}/{room2_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# User3 reads room3
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id3}/receipt/{ReceiptTypes.READ}/{room3_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user3_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# No activity for room4 after the `from_token`
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the receipts extension enabled
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# Even though we requested room2, we only expect rooms to show up if they are
|
||||||
|
# already in the Sliding Sync response. room4 doesn't show up because there is
|
||||||
|
# no activity after the `from_token`.
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"].get("rooms").keys(),
|
||||||
|
{room_id1, room_id3},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check room1:
|
||||||
|
#
|
||||||
|
# Sanity check that it's the correct ephemeral event type
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["type"],
|
||||||
|
EduTypes.RECEIPT,
|
||||||
|
)
|
||||||
|
# We only see that user1 has read something in room1 since the `from_token`
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["content"][
|
||||||
|
room1_event_response1["event_id"]
|
||||||
|
][ReceiptTypes.READ].keys(),
|
||||||
|
{user1_id},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# User1 did not send a private read receipt in this room and we shouldn't leak
|
||||||
|
# others' private read receipts
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["content"][
|
||||||
|
room1_event_response1["event_id"]
|
||||||
|
]
|
||||||
|
.get(ReceiptTypes.READ_PRIVATE, {})
|
||||||
|
.keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# No events in the timeline since they were sent before the `from_token`
|
||||||
|
self.assertNotIn(room_id1, response_body["rooms"])
|
||||||
|
|
||||||
|
# Check room3:
|
||||||
|
#
|
||||||
|
# Sanity check that it's the correct ephemeral event type
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id3]["type"],
|
||||||
|
EduTypes.RECEIPT,
|
||||||
|
)
|
||||||
|
# We only see that user3 has read something in room1 since the `from_token`
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id3]["content"][
|
||||||
|
room3_event_response1["event_id"]
|
||||||
|
][ReceiptTypes.READ].keys(),
|
||||||
|
{user3_id},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# User1 did not send a private read receipt in this room and we shouldn't leak
|
||||||
|
# others' private read receipts
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id3]["content"][
|
||||||
|
room3_event_response1["event_id"]
|
||||||
|
]
|
||||||
|
.get(ReceiptTypes.READ_PRIVATE, {})
|
||||||
|
.keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# No events in the timeline since they were sent before the `from_token`
|
||||||
|
self.assertNotIn(room_id3, response_body["rooms"])
|
||||||
|
|
||||||
|
def test_receipts_incremental_sync_all_live_receipts(self) -> None:
|
||||||
|
"""
|
||||||
|
On incremental sync, we return all receipts in the token range for a given room
|
||||||
|
even if they are not in the timeline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Create room1
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [],
|
||||||
|
# The timeline will only include event2
|
||||||
|
"timeline_limit": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"receipts": {
|
||||||
|
"enabled": True,
|
||||||
|
"rooms": [room_id1],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
room1_event_response1 = self.helper.send(
|
||||||
|
room_id1, body="new event1", tok=user2_tok
|
||||||
|
)
|
||||||
|
room1_event_response2 = self.helper.send(
|
||||||
|
room_id1, body="new event2", tok=user2_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
# User1 reads event1
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id1}/receipt/{ReceiptTypes.READ}/{room1_event_response1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
# User2 reads event2
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id1}/receipt/{ReceiptTypes.READ}/{room1_event_response2['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user2_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the receipts extension enabled
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# We should see room1 because it has receipts in the token range
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"].get("rooms").keys(),
|
||||||
|
{room_id1},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# Sanity check that it's the correct ephemeral event type
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["type"],
|
||||||
|
EduTypes.RECEIPT,
|
||||||
|
)
|
||||||
|
# We should see all receipts in the token range regardless of whether the events
|
||||||
|
# are in the timeline
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["content"][
|
||||||
|
room1_event_response1["event_id"]
|
||||||
|
][ReceiptTypes.READ].keys(),
|
||||||
|
{user1_id},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIncludes(
|
||||||
|
response_body["extensions"]["receipts"]["rooms"][room_id1]["content"][
|
||||||
|
room1_event_response2["event_id"]
|
||||||
|
][ReceiptTypes.READ].keys(),
|
||||||
|
{user2_id},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# Only the latest event in the timeline because the `timeline_limit` is 1
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1].get("timeline", [])
|
||||||
|
},
|
||||||
|
{room1_event_response2["event_id"]},
|
||||||
|
exact=True,
|
||||||
|
message=str(response_body["rooms"][room_id1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||||
|
event_response = self.helper.send(room_id, body="new event", tok=user2_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id: {
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"receipts": {
|
||||||
|
"enabled": True,
|
||||||
|
"rooms": [room_id],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request with the receipts extension enabled
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?timeout=10000&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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 receipts to trigger new results
|
||||||
|
receipt_channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id}/receipt/{ReceiptTypes.READ}/{event_response['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user2_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(receipt_channel.code, 200, receipt_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 new receipt
|
||||||
|
self.assertIncludes(
|
||||||
|
channel.json_body.get("extensions", {})
|
||||||
|
.get("receipts", {})
|
||||||
|
.get("rooms", {})
|
||||||
|
.keys(),
|
||||||
|
{room_id},
|
||||||
|
exact=True,
|
||||||
|
message=str(channel.json_body),
|
||||||
|
)
|
||||||
|
self.assertIncludes(
|
||||||
|
channel.json_body["extensions"]["receipts"]["rooms"][room_id]["content"][
|
||||||
|
event_response["event_id"]
|
||||||
|
][ReceiptTypes.READ].keys(),
|
||||||
|
{user2_id},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
# User1 did not send a private read receipt in this room and we shouldn't leak
|
||||||
|
# others' private read receipts
|
||||||
|
self.assertIncludes(
|
||||||
|
channel.json_body["extensions"]["receipts"]["rooms"][room_id]["content"][
|
||||||
|
event_response["event_id"]
|
||||||
|
]
|
||||||
|
.get(ReceiptTypes.READ_PRIVATE, {})
|
||||||
|
.keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 receipts extension doesn't trigger a false-positive for new data.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"receipts": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?timeout=10000&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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, wake_stream_key=StreamKeyType.ACCOUNT_DATA
|
||||||
|
)
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
self.assertIncludes(
|
||||||
|
channel.json_body["extensions"]["receipts"].get("rooms").keys(),
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
|
@ -0,0 +1,278 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.rest.client import login, sendtodevice, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import JsonDict, StreamKeyType
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
from tests.server import TimedOutException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncToDeviceExtensionTestCase(SlidingSyncBase):
|
||||||
|
"""Tests for the to-device sliding sync extension"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
sendtodevice.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
|
||||||
|
def _assert_to_device_response(
|
||||||
|
self, response_body: JsonDict, expected_messages: List[JsonDict]
|
||||||
|
) -> str:
|
||||||
|
"""Assert the sliding sync response was successful and has the expected
|
||||||
|
to-device messages.
|
||||||
|
|
||||||
|
Returns the next_batch token from the to-device section.
|
||||||
|
"""
|
||||||
|
extensions = response_body["extensions"]
|
||||||
|
to_device = extensions["to_device"]
|
||||||
|
self.assertIsInstance(to_device["next_batch"], str)
|
||||||
|
self.assertEqual(to_device["events"], expected_messages)
|
||||||
|
|
||||||
|
return to_device["next_batch"]
|
||||||
|
|
||||||
|
def test_no_data(self) -> None:
|
||||||
|
"""Test that enabling to-device extension works, even if there is
|
||||||
|
no-data
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"to_device": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# We expect no to-device messages
|
||||||
|
self._assert_to_device_response(response_body, [])
|
||||||
|
|
||||||
|
def test_data_initial_sync(self) -> None:
|
||||||
|
"""Test that we get to-device messages when we don't specify a since
|
||||||
|
token"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass", "d1")
|
||||||
|
user2_id = self.register_user("u2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass", "d2")
|
||||||
|
|
||||||
|
# Send the to-device message
|
||||||
|
test_msg = {"foo": "bar"}
|
||||||
|
chan = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_matrix/client/r0/sendToDevice/m.test/1234",
|
||||||
|
content={"messages": {user1_id: {"d1": test_msg}}},
|
||||||
|
access_token=user2_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(chan.code, 200, chan.result)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"to_device": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
self._assert_to_device_response(
|
||||||
|
response_body,
|
||||||
|
[{"content": test_msg, "sender": user2_id, "type": "m.test"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_data_incremental_sync(self) -> None:
|
||||||
|
"""Test that we get to-device messages over incremental syncs"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass", "d1")
|
||||||
|
user2_id = self.register_user("u2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass", "d2")
|
||||||
|
|
||||||
|
sync_body: JsonDict = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"to_device": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
# No to-device messages yet.
|
||||||
|
next_batch = self._assert_to_device_response(response_body, [])
|
||||||
|
|
||||||
|
test_msg = {"foo": "bar"}
|
||||||
|
chan = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_matrix/client/r0/sendToDevice/m.test/1234",
|
||||||
|
content={"messages": {user1_id: {"d1": test_msg}}},
|
||||||
|
access_token=user2_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(chan.code, 200, chan.result)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"to_device": {
|
||||||
|
"enabled": True,
|
||||||
|
"since": next_batch,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
next_batch = self._assert_to_device_response(
|
||||||
|
response_body,
|
||||||
|
[{"content": test_msg, "sender": user2_id, "type": "m.test"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
# The next sliding sync request should not include the to-device
|
||||||
|
# message.
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"to_device": {
|
||||||
|
"enabled": True,
|
||||||
|
"since": next_batch,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
self._assert_to_device_response(response_body, [])
|
||||||
|
|
||||||
|
# An initial sliding sync request should not include the to-device
|
||||||
|
# message, as it should have been deleted
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"to_device": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
self._assert_to_device_response(response_body, [])
|
||||||
|
|
||||||
|
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", "d1")
|
||||||
|
user2_id = self.register_user("u2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass", "d2")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"to_device": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + "?timeout=10000" + f"&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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 to-device messages to trigger new results
|
||||||
|
test_msg = {"foo": "bar"}
|
||||||
|
send_to_device_channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_matrix/client/r0/sendToDevice/m.test/1234",
|
||||||
|
content={"messages": {user1_id: {"d1": test_msg}}},
|
||||||
|
access_token=user2_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
send_to_device_channel.code, 200, send_to_device_channel.result
|
||||||
|
)
|
||||||
|
# Should respond before the 10 second timeout
|
||||||
|
channel.await_result(timeout_ms=3000)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
self._assert_to_device_response(
|
||||||
|
channel.json_body,
|
||||||
|
[{"content": test_msg, "sender": user2_id, "type": "m.test"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
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 To-Device extension doesn't trigger a false-positive for new data.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {},
|
||||||
|
"extensions": {
|
||||||
|
"to_device": {
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + "?timeout=10000" + f"&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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, wake_stream_key=StreamKeyType.ACCOUNT_DATA
|
||||||
|
)
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
self._assert_to_device_response(channel.json_body, [])
|
|
@ -0,0 +1,267 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import ReceiptTypes
|
||||||
|
from synapse.rest.client import login, receipts, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncExtensionsTestCase(SlidingSyncBase):
|
||||||
|
"""
|
||||||
|
Test general extensions behavior in the Sliding Sync API. Each extension has their
|
||||||
|
own suite of tests in their own file as well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
receipts.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.storage_controllers = hs.get_storage_controllers()
|
||||||
|
self.account_data_handler = hs.get_account_data_handler()
|
||||||
|
|
||||||
|
# Any extensions that use `lists`/`rooms` should be tested here
|
||||||
|
@parameterized.expand([("account_data",), ("receipts",)])
|
||||||
|
def test_extensions_lists_rooms_relevant_rooms(self, extension_name: str) -> None:
|
||||||
|
"""
|
||||||
|
With various extensions, test out requesting different variations of
|
||||||
|
`lists`/`rooms`.
|
||||||
|
"""
|
||||||
|
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():
|
||||||
|
if extension_name == "account_data":
|
||||||
|
# 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"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif extension_name == "receipts":
|
||||||
|
event_response = self.helper.send(
|
||||||
|
room_id, body="new event", tok=user1_tok
|
||||||
|
)
|
||||||
|
# Read last event
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{room_id}/receipt/{ReceiptTypes.READ}/{event_response['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"Unknown extension name: {extension_name}")
|
||||||
|
|
||||||
|
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": {
|
||||||
|
extension_name: {
|
||||||
|
"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"][extension_name]
|
||||||
|
.get("rooms")
|
||||||
|
.keys()
|
||||||
|
},
|
||||||
|
{"room1", "room4", "room5"},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try wildcards (this is the default)
|
||||||
|
sync_body = {
|
||||||
|
**main_sync_body,
|
||||||
|
"extensions": {
|
||||||
|
extension_name: {
|
||||||
|
"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"][extension_name]
|
||||||
|
.get("rooms")
|
||||||
|
.keys()
|
||||||
|
},
|
||||||
|
{"room1", "room3", "room4", "room5"},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Empty list will return nothing
|
||||||
|
sync_body = {
|
||||||
|
**main_sync_body,
|
||||||
|
"extensions": {
|
||||||
|
extension_name: {
|
||||||
|
"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"][extension_name]
|
||||||
|
.get("rooms")
|
||||||
|
.keys()
|
||||||
|
},
|
||||||
|
set(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try wildcard and none
|
||||||
|
sync_body = {
|
||||||
|
**main_sync_body,
|
||||||
|
"extensions": {
|
||||||
|
extension_name: {
|
||||||
|
"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"][extension_name]
|
||||||
|
.get("rooms")
|
||||||
|
.keys()
|
||||||
|
},
|
||||||
|
{"room3", "room4", "room5"},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try requesting a room that is only in a list
|
||||||
|
sync_body = {
|
||||||
|
**main_sync_body,
|
||||||
|
"extensions": {
|
||||||
|
extension_name: {
|
||||||
|
"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"][extension_name]
|
||||||
|
.get("rooms")
|
||||||
|
.keys()
|
||||||
|
},
|
||||||
|
{"room5"},
|
||||||
|
exact=True,
|
||||||
|
)
|
|
@ -0,0 +1,285 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import EventTypes, HistoryVisibility
|
||||||
|
from synapse.rest.client import login, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncRoomSubscriptionsTestCase(SlidingSyncBase):
|
||||||
|
"""
|
||||||
|
Test `room_subscriptions` in the Sliding Sync API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.storage_controllers = hs.get_storage_controllers()
|
||||||
|
|
||||||
|
def test_room_subscriptions_with_join_membership(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `room_subscriptions` with a joined room should give us timeline and current
|
||||||
|
state events.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
join_response = self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with just the room subscription
|
||||||
|
sync_body = {
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# We should see some state
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
# We should see some events
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1]["timeline"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
join_response["event_id"],
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["timeline"],
|
||||||
|
)
|
||||||
|
# No "live" events in an initial sync (no `from_token` to define the "live"
|
||||||
|
# range)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["num_live"],
|
||||||
|
0,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# There are more events to paginate to
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["limited"],
|
||||||
|
True,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_room_subscriptions_with_leave_membership(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `room_subscriptions` with a leave room should give us timeline and state
|
||||||
|
events up to the leave event.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key="",
|
||||||
|
body={"foo": "bar"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
join_response = self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
leave_response = self.helper.leave(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send some events after user1 leaves
|
||||||
|
self.helper.send(room_id1, "activity after leave", tok=user2_tok)
|
||||||
|
# Update state after user1 leaves
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key="",
|
||||||
|
body={"foo": "qux"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with just the room subscription
|
||||||
|
sync_body = {
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [
|
||||||
|
["org.matrix.foo_state", ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# We should see the state at the time of the leave
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[("org.matrix.foo_state", "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
# We should see some before we left (nothing after)
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1]["timeline"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
join_response["event_id"],
|
||||||
|
leave_response["event_id"],
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["timeline"],
|
||||||
|
)
|
||||||
|
# No "live" events in an initial sync (no `from_token` to define the "live"
|
||||||
|
# range)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["num_live"],
|
||||||
|
0,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# There are more events to paginate to
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["limited"],
|
||||||
|
True,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_room_subscriptions_no_leak_private_room(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `room_subscriptions` with a private room we have never been in should not
|
||||||
|
leak any data to the user.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok, is_public=False)
|
||||||
|
|
||||||
|
# We should not be able to join the private room
|
||||||
|
self.helper.join(
|
||||||
|
room_id1, user1_id, tok=user1_tok, expect_code=HTTPStatus.FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with just the room subscription
|
||||||
|
sync_body = {
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# We should not see the room at all (we're not in it)
|
||||||
|
self.assertIsNone(response_body["rooms"].get(room_id1), response_body["rooms"])
|
||||||
|
|
||||||
|
def test_room_subscriptions_world_readable(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `room_subscriptions` with a room that has `world_readable` history visibility
|
||||||
|
|
||||||
|
FIXME: We should be able to see the room timeline and state
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Create a room with `world_readable` history visibility
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
"preset": "public_chat",
|
||||||
|
"initial_state": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"history_visibility": HistoryVisibility.WORLD_READABLE
|
||||||
|
},
|
||||||
|
"state_key": "",
|
||||||
|
"type": EventTypes.RoomHistoryVisibility,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Ensure we're testing with a room with `world_readable` history visibility
|
||||||
|
# which means events are visible to anyone even without membership.
|
||||||
|
history_visibility_response = self.helper.get_state(
|
||||||
|
room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
history_visibility_response.get("history_visibility"),
|
||||||
|
HistoryVisibility.WORLD_READABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note: We never join the room
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with just the room subscription
|
||||||
|
sync_body = {
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# FIXME: In the future, we should be able to see the room because it's
|
||||||
|
# `world_readable` but currently we don't support this.
|
||||||
|
self.assertIsNone(response_body["rooms"].get(room_id1), response_body["rooms"])
|
|
@ -0,0 +1,510 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import EventTypes, HistoryVisibility
|
||||||
|
from synapse.rest.client import login, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import UserID
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncRoomsInvitesTestCase(SlidingSyncBase):
|
||||||
|
"""
|
||||||
|
Test to make sure the `rooms` response looks good for invites in the Sliding Sync API.
|
||||||
|
|
||||||
|
Invites behave a lot different than other rooms because we don't include the
|
||||||
|
`timeline` (`num_live`, `limited`, `prev_batch`) or `required_state` in favor of
|
||||||
|
some stripped state under the `invite_state` key.
|
||||||
|
|
||||||
|
Knocks probably have the same behavior but the spec doesn't mention knocks yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.storage_controllers = hs.get_storage_controllers()
|
||||||
|
|
||||||
|
def test_rooms_invite_shared_history_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `rooms` we are invited to have some stripped `invite_state` during an
|
||||||
|
initial sync.
|
||||||
|
|
||||||
|
This is an `invite` room so we should only have `stripped_state` (no `timeline`)
|
||||||
|
but we also shouldn't see any timeline events because the history visiblity is
|
||||||
|
`shared` and we haven't joined the room yet.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user1 = UserID.from_string(user1_id)
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
user2 = UserID.from_string(user2_id)
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
# Ensure we're testing with a room with `shared` history visibility which means
|
||||||
|
# history visible until you actually join the room.
|
||||||
|
history_visibility_response = self.helper.get_state(
|
||||||
|
room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
history_visibility_response.get("history_visibility"),
|
||||||
|
HistoryVisibility.SHARED,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity before1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before2", tok=user2_tok)
|
||||||
|
self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after3", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after4", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# `timeline` is omitted for `invite` rooms with `stripped_state`
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("timeline"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `num_live` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("num_live"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `limited` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("limited"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `prev_batch` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("prev_batch"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `required_state` is omitted for `invite` rooms with `stripped_state`
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("required_state"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# We should have some `stripped_state` so the potential joiner can identify the
|
||||||
|
# room (we don't care about the order).
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"][room_id1]["invite_state"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": {"creator": user2_id, "room_version": "10"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.create",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"join_rule": "public"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.join_rules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"displayname": user2.localpart, "membership": "join"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": user2_id,
|
||||||
|
"type": "m.room.member",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"displayname": user1.localpart, "membership": "invite"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": user1_id,
|
||||||
|
"type": "m.room.member",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["invite_state"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_invite_shared_history_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `rooms` we are invited to have some stripped `invite_state` during an
|
||||||
|
incremental sync.
|
||||||
|
|
||||||
|
This is an `invite` room so we should only have `stripped_state` (no `timeline`)
|
||||||
|
but we also shouldn't see any timeline events because the history visiblity is
|
||||||
|
`shared` and we haven't joined the room yet.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user1 = UserID.from_string(user1_id)
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
user2 = UserID.from_string(user2_id)
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
# Ensure we're testing with a room with `shared` history visibility which means
|
||||||
|
# history visible until you actually join the room.
|
||||||
|
history_visibility_response = self.helper.get_state(
|
||||||
|
room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
history_visibility_response.get("history_visibility"),
|
||||||
|
HistoryVisibility.SHARED,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity before invite1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before invite2", tok=user2_tok)
|
||||||
|
self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after invite3", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after invite4", tok=user2_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity after token5", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after toekn6", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
response_body, from_token = self.do_sync(
|
||||||
|
sync_body, since=from_token, tok=user1_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
# `timeline` is omitted for `invite` rooms with `stripped_state`
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("timeline"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `num_live` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("num_live"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `limited` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("limited"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `prev_batch` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("prev_batch"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `required_state` is omitted for `invite` rooms with `stripped_state`
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("required_state"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# We should have some `stripped_state` so the potential joiner can identify the
|
||||||
|
# room (we don't care about the order).
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"][room_id1]["invite_state"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": {"creator": user2_id, "room_version": "10"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.create",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"join_rule": "public"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.join_rules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"displayname": user2.localpart, "membership": "join"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": user2_id,
|
||||||
|
"type": "m.room.member",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"displayname": user1.localpart, "membership": "invite"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": user1_id,
|
||||||
|
"type": "m.room.member",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["invite_state"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_invite_world_readable_history_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `rooms` we are invited to have some stripped `invite_state` during an
|
||||||
|
initial sync.
|
||||||
|
|
||||||
|
This is an `invite` room so we should only have `stripped_state` (no `timeline`)
|
||||||
|
but depending on the semantics we decide, we could potentially see some
|
||||||
|
historical events before/after the `from_token` because the history is
|
||||||
|
`world_readable`. Same situation for events after the `from_token` if the
|
||||||
|
history visibility was set to `invited`.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user1 = UserID.from_string(user1_id)
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
user2 = UserID.from_string(user2_id)
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
"preset": "public_chat",
|
||||||
|
"initial_state": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"history_visibility": HistoryVisibility.WORLD_READABLE
|
||||||
|
},
|
||||||
|
"state_key": "",
|
||||||
|
"type": EventTypes.RoomHistoryVisibility,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Ensure we're testing with a room with `world_readable` history visibility
|
||||||
|
# which means events are visible to anyone even without membership.
|
||||||
|
history_visibility_response = self.helper.get_state(
|
||||||
|
room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
history_visibility_response.get("history_visibility"),
|
||||||
|
HistoryVisibility.WORLD_READABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity before1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before2", tok=user2_tok)
|
||||||
|
self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after3", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after4", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
# Large enough to see the latest events and before the invite
|
||||||
|
"timeline_limit": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# `timeline` is omitted for `invite` rooms with `stripped_state`
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("timeline"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `num_live` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("num_live"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `limited` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("limited"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `prev_batch` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("prev_batch"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `required_state` is omitted for `invite` rooms with `stripped_state`
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("required_state"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# We should have some `stripped_state` so the potential joiner can identify the
|
||||||
|
# room (we don't care about the order).
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"][room_id1]["invite_state"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": {"creator": user2_id, "room_version": "10"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.create",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"join_rule": "public"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.join_rules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"displayname": user2.localpart, "membership": "join"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": user2_id,
|
||||||
|
"type": "m.room.member",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"displayname": user1.localpart, "membership": "invite"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": user1_id,
|
||||||
|
"type": "m.room.member",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["invite_state"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_invite_world_readable_history_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `rooms` we are invited to have some stripped `invite_state` during an
|
||||||
|
incremental sync.
|
||||||
|
|
||||||
|
This is an `invite` room so we should only have `stripped_state` (no `timeline`)
|
||||||
|
but depending on the semantics we decide, we could potentially see some
|
||||||
|
historical events before/after the `from_token` because the history is
|
||||||
|
`world_readable`. Same situation for events after the `from_token` if the
|
||||||
|
history visibility was set to `invited`.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user1 = UserID.from_string(user1_id)
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
user2 = UserID.from_string(user2_id)
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
"preset": "public_chat",
|
||||||
|
"initial_state": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"history_visibility": HistoryVisibility.WORLD_READABLE
|
||||||
|
},
|
||||||
|
"state_key": "",
|
||||||
|
"type": EventTypes.RoomHistoryVisibility,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Ensure we're testing with a room with `world_readable` history visibility
|
||||||
|
# which means events are visible to anyone even without membership.
|
||||||
|
history_visibility_response = self.helper.get_state(
|
||||||
|
room_id1, EventTypes.RoomHistoryVisibility, tok=user2_tok
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
history_visibility_response.get("history_visibility"),
|
||||||
|
HistoryVisibility.WORLD_READABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity before invite1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before invite2", tok=user2_tok)
|
||||||
|
self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after invite3", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after invite4", tok=user2_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
# Large enough to see the latest events and before the invite
|
||||||
|
"timeline_limit": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity after token5", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after toekn6", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the incremental Sliding Sync request
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# `timeline` is omitted for `invite` rooms with `stripped_state`
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("timeline"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `num_live` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("num_live"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `limited` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("limited"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `prev_batch` is omitted for `invite` rooms with `stripped_state` (no timeline anyway)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("prev_batch"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# `required_state` is omitted for `invite` rooms with `stripped_state`
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("required_state"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# We should have some `stripped_state` so the potential joiner can identify the
|
||||||
|
# room (we don't care about the order).
|
||||||
|
self.assertCountEqual(
|
||||||
|
response_body["rooms"][room_id1]["invite_state"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": {"creator": user2_id, "room_version": "10"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.create",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"join_rule": "public"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": "",
|
||||||
|
"type": "m.room.join_rules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"displayname": user2.localpart, "membership": "join"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": user2_id,
|
||||||
|
"type": "m.room.member",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {"displayname": user1.localpart, "membership": "invite"},
|
||||||
|
"sender": user2_id,
|
||||||
|
"state_key": user1_id,
|
||||||
|
"type": "m.room.member",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["invite_state"],
|
||||||
|
)
|
|
@ -0,0 +1,710 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import EventTypes, Membership
|
||||||
|
from synapse.api.room_versions import RoomVersions
|
||||||
|
from synapse.rest.client import login, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
from tests.test_utils.event_injection import create_event
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncRoomsMetaTestCase(SlidingSyncBase):
|
||||||
|
"""
|
||||||
|
Test rooms meta info like name, avatar, joined_count, invited_count, is_dm,
|
||||||
|
bump_stamp in the Sliding Sync API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.storage_controllers = hs.get_storage_controllers()
|
||||||
|
|
||||||
|
def test_rooms_meta_when_joined(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that the `rooms` `name` and `avatar` are included in the response and
|
||||||
|
reflect the current state of the room when the user is joined to the room.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
"name": "my super room",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Set the room avatar URL
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
EventTypes.RoomAvatar,
|
||||||
|
{"url": "mxc://DUMMY_MEDIA_ID"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Reflect the current state of the room
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["name"],
|
||||||
|
"my super room",
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["avatar"],
|
||||||
|
"mxc://DUMMY_MEDIA_ID",
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["joined_count"],
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["invited_count"],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("is_dm"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_meta_when_invited(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that the `rooms` `name` and `avatar` are included in the response and
|
||||||
|
reflect the current state of the room when the user is invited to the room.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
"name": "my super room",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Set the room avatar URL
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
EventTypes.RoomAvatar,
|
||||||
|
{"url": "mxc://DUMMY_MEDIA_ID"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# User1 is invited to the room
|
||||||
|
self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
|
||||||
|
# Update the room name after user1 has left
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
EventTypes.Name,
|
||||||
|
{"name": "my super duper room"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
# Update the room avatar URL after user1 has left
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
EventTypes.RoomAvatar,
|
||||||
|
{"url": "mxc://UPDATED_DUMMY_MEDIA_ID"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# This should still reflect the current state of the room even when the user is
|
||||||
|
# invited.
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["name"],
|
||||||
|
"my super duper room",
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["avatar"],
|
||||||
|
"mxc://UPDATED_DUMMY_MEDIA_ID",
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["joined_count"],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["invited_count"],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("is_dm"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_meta_when_banned(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that the `rooms` `name` and `avatar` reflect the state of the room when the
|
||||||
|
user was banned (do not leak current state).
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
"name": "my super room",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Set the room avatar URL
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
EventTypes.RoomAvatar,
|
||||||
|
{"url": "mxc://DUMMY_MEDIA_ID"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
self.helper.ban(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
|
||||||
|
# Update the room name after user1 has left
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
EventTypes.Name,
|
||||||
|
{"name": "my super duper room"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
# Update the room avatar URL after user1 has left
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
EventTypes.RoomAvatar,
|
||||||
|
{"url": "mxc://UPDATED_DUMMY_MEDIA_ID"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Reflect the state of the room at the time of leaving
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["name"],
|
||||||
|
"my super room",
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["avatar"],
|
||||||
|
"mxc://DUMMY_MEDIA_ID",
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["joined_count"],
|
||||||
|
# FIXME: The actual number should be "1" (user2) but we currently don't
|
||||||
|
# support this for rooms where the user has left/been banned.
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["invited_count"],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("is_dm"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_meta_heroes(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that the `rooms` `heroes` are included in the response when the room
|
||||||
|
doesn't have a room name set.
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
user3_id = self.register_user("user3", "pass")
|
||||||
|
_user3_tok = self.login(user3_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
"name": "my super room",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
# User3 is invited
|
||||||
|
self.helper.invite(room_id1, src=user2_id, targ=user3_id, tok=user2_tok)
|
||||||
|
|
||||||
|
room_id2 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
# No room name set so that `heroes` is populated
|
||||||
|
#
|
||||||
|
# "name": "my super room2",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.helper.join(room_id2, user1_id, tok=user1_tok)
|
||||||
|
# User3 is invited
|
||||||
|
self.helper.invite(room_id2, src=user2_id, targ=user3_id, tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Room1 has a name so we shouldn't see any `heroes` which the client would use
|
||||||
|
# the calculate the room name themselves.
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["name"],
|
||||||
|
"my super room",
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("heroes"))
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["joined_count"],
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["invited_count"],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Room2 doesn't have a name so we should see `heroes` populated
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id2].get("name"))
|
||||||
|
self.assertCountEqual(
|
||||||
|
[
|
||||||
|
hero["user_id"]
|
||||||
|
for hero in response_body["rooms"][room_id2].get("heroes", [])
|
||||||
|
],
|
||||||
|
# Heroes shouldn't include the user themselves (we shouldn't see user1)
|
||||||
|
[user2_id, user3_id],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id2]["joined_count"],
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id2]["invited_count"],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We didn't request any state so we shouldn't see any `required_state`
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("required_state"))
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id2].get("required_state"))
|
||||||
|
|
||||||
|
def test_rooms_meta_heroes_max(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that the `rooms` `heroes` only includes the first 5 users (not including
|
||||||
|
yourself).
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
user3_id = self.register_user("user3", "pass")
|
||||||
|
user3_tok = self.login(user3_id, "pass")
|
||||||
|
user4_id = self.register_user("user4", "pass")
|
||||||
|
user4_tok = self.login(user4_id, "pass")
|
||||||
|
user5_id = self.register_user("user5", "pass")
|
||||||
|
user5_tok = self.login(user5_id, "pass")
|
||||||
|
user6_id = self.register_user("user6", "pass")
|
||||||
|
user6_tok = self.login(user6_id, "pass")
|
||||||
|
user7_id = self.register_user("user7", "pass")
|
||||||
|
user7_tok = self.login(user7_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
# No room name set so that `heroes` is populated
|
||||||
|
#
|
||||||
|
# "name": "my super room",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
self.helper.join(room_id1, user3_id, tok=user3_tok)
|
||||||
|
self.helper.join(room_id1, user4_id, tok=user4_tok)
|
||||||
|
self.helper.join(room_id1, user5_id, tok=user5_tok)
|
||||||
|
self.helper.join(room_id1, user6_id, tok=user6_tok)
|
||||||
|
self.helper.join(room_id1, user7_id, tok=user7_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Room2 doesn't have a name so we should see `heroes` populated
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("name"))
|
||||||
|
self.assertCountEqual(
|
||||||
|
[
|
||||||
|
hero["user_id"]
|
||||||
|
for hero in response_body["rooms"][room_id1].get("heroes", [])
|
||||||
|
],
|
||||||
|
# Heroes should be the first 5 users in the room (excluding the user
|
||||||
|
# themselves, we shouldn't see `user1`)
|
||||||
|
[user2_id, user3_id, user4_id, user5_id, user6_id],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["joined_count"],
|
||||||
|
7,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["invited_count"],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We didn't request any state so we shouldn't see any `required_state`
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("required_state"))
|
||||||
|
|
||||||
|
def test_rooms_meta_heroes_when_banned(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that the `rooms` `heroes` are included in the response when the room
|
||||||
|
doesn't have a room name set but doesn't leak information past their ban.
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
user3_id = self.register_user("user3", "pass")
|
||||||
|
_user3_tok = self.login(user3_id, "pass")
|
||||||
|
user4_id = self.register_user("user4", "pass")
|
||||||
|
user4_tok = self.login(user4_id, "pass")
|
||||||
|
user5_id = self.register_user("user5", "pass")
|
||||||
|
_user5_tok = self.login(user5_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
# No room name set so that `heroes` is populated
|
||||||
|
#
|
||||||
|
# "name": "my super room",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# User1 joins the room
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
# User3 is invited
|
||||||
|
self.helper.invite(room_id1, src=user2_id, targ=user3_id, tok=user2_tok)
|
||||||
|
|
||||||
|
# User1 is banned from the room
|
||||||
|
self.helper.ban(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
|
||||||
|
# User4 joins the room after user1 is banned
|
||||||
|
self.helper.join(room_id1, user4_id, tok=user4_tok)
|
||||||
|
# User5 is invited after user1 is banned
|
||||||
|
self.helper.invite(room_id1, src=user2_id, targ=user5_id, tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Room2 doesn't have a name so we should see `heroes` populated
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("name"))
|
||||||
|
self.assertCountEqual(
|
||||||
|
[
|
||||||
|
hero["user_id"]
|
||||||
|
for hero in response_body["rooms"][room_id1].get("heroes", [])
|
||||||
|
],
|
||||||
|
# Heroes shouldn't include the user themselves (we shouldn't see user1). We
|
||||||
|
# also shouldn't see user4 since they joined after user1 was banned.
|
||||||
|
#
|
||||||
|
# FIXME: The actual result should be `[user2_id, user3_id]` but we currently
|
||||||
|
# don't support this for rooms where the user has left/been banned.
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["joined_count"],
|
||||||
|
# FIXME: The actual number should be "1" (user2) but we currently don't
|
||||||
|
# support this for rooms where the user has left/been banned.
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["invited_count"],
|
||||||
|
# We shouldn't see user5 since they were invited after user1 was banned.
|
||||||
|
#
|
||||||
|
# FIXME: The actual number should be "1" (user3) but we currently don't
|
||||||
|
# support this for rooms where the user has left/been banned.
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_bump_stamp(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `bump_stamp` is present and pointing to relevant events.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(
|
||||||
|
user1_id,
|
||||||
|
tok=user1_tok,
|
||||||
|
)
|
||||||
|
event_response1 = message_response = self.helper.send(
|
||||||
|
room_id1, "message in room1", tok=user1_tok
|
||||||
|
)
|
||||||
|
event_pos1 = self.get_success(
|
||||||
|
self.store.get_position_for_event(event_response1["event_id"])
|
||||||
|
)
|
||||||
|
room_id2 = self.helper.create_room_as(
|
||||||
|
user1_id,
|
||||||
|
tok=user1_tok,
|
||||||
|
)
|
||||||
|
send_response2 = self.helper.send(room_id2, "message in room2", tok=user1_tok)
|
||||||
|
event_pos2 = self.get_success(
|
||||||
|
self.store.get_position_for_event(send_response2["event_id"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send a reaction in room1 but it shouldn't affect the `bump_stamp`
|
||||||
|
# because reactions are not part of the `DEFAULT_BUMP_EVENT_TYPES`
|
||||||
|
self.helper.send_event(
|
||||||
|
room_id1,
|
||||||
|
type=EventTypes.Reaction,
|
||||||
|
content={
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": message_response["event_id"],
|
||||||
|
"key": "👍",
|
||||||
|
"rel_type": "m.annotation",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tok=user1_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make sure it has the foo-list we requested
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"].keys()),
|
||||||
|
["foo-list"],
|
||||||
|
response_body["lists"].keys(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the list includes the rooms in the right order
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 1],
|
||||||
|
# room1 sorts before room2 because it has the latest event (the
|
||||||
|
# reaction)
|
||||||
|
"room_ids": [room_id1, room_id2],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_body["lists"]["foo-list"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# The `bump_stamp` for room1 should point at the latest message (not the
|
||||||
|
# reaction since it's not one of the `DEFAULT_BUMP_EVENT_TYPES`)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["bump_stamp"],
|
||||||
|
event_pos1.stream,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
# The `bump_stamp` for room2 should point at the latest message
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id2]["bump_stamp"],
|
||||||
|
event_pos2.stream,
|
||||||
|
response_body["rooms"][room_id2],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_bump_stamp_backfill(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `bump_stamp` ignores backfilled events, i.e. events with a
|
||||||
|
negative stream ordering.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
# Create a remote room
|
||||||
|
creator = "@user:other"
|
||||||
|
room_id = "!foo:other"
|
||||||
|
shared_kwargs = {
|
||||||
|
"room_id": room_id,
|
||||||
|
"room_version": "10",
|
||||||
|
}
|
||||||
|
|
||||||
|
create_tuple = self.get_success(
|
||||||
|
create_event(
|
||||||
|
self.hs,
|
||||||
|
prev_event_ids=[],
|
||||||
|
type=EventTypes.Create,
|
||||||
|
state_key="",
|
||||||
|
sender=creator,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
creator_tuple = self.get_success(
|
||||||
|
create_event(
|
||||||
|
self.hs,
|
||||||
|
prev_event_ids=[create_tuple[0].event_id],
|
||||||
|
auth_event_ids=[create_tuple[0].event_id],
|
||||||
|
type=EventTypes.Member,
|
||||||
|
state_key=creator,
|
||||||
|
content={"membership": Membership.JOIN},
|
||||||
|
sender=creator,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# We add a message event as a valid "bump type"
|
||||||
|
msg_tuple = self.get_success(
|
||||||
|
create_event(
|
||||||
|
self.hs,
|
||||||
|
prev_event_ids=[creator_tuple[0].event_id],
|
||||||
|
auth_event_ids=[create_tuple[0].event_id],
|
||||||
|
type=EventTypes.Message,
|
||||||
|
content={"body": "foo", "msgtype": "m.text"},
|
||||||
|
sender=creator,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
invite_tuple = self.get_success(
|
||||||
|
create_event(
|
||||||
|
self.hs,
|
||||||
|
prev_event_ids=[msg_tuple[0].event_id],
|
||||||
|
auth_event_ids=[create_tuple[0].event_id, creator_tuple[0].event_id],
|
||||||
|
type=EventTypes.Member,
|
||||||
|
state_key=user1_id,
|
||||||
|
content={"membership": Membership.INVITE},
|
||||||
|
sender=creator,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
remote_events_and_contexts = [
|
||||||
|
create_tuple,
|
||||||
|
creator_tuple,
|
||||||
|
msg_tuple,
|
||||||
|
invite_tuple,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ensure the local HS knows the room version
|
||||||
|
self.get_success(
|
||||||
|
self.store.store_room(room_id, creator, False, RoomVersions.V10)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Persist these events as backfilled events.
|
||||||
|
persistence = self.hs.get_storage_controllers().persistence
|
||||||
|
assert persistence is not None
|
||||||
|
|
||||||
|
for event, context in remote_events_and_contexts:
|
||||||
|
self.get_success(persistence.persist_event(event, context, backfilled=True))
|
||||||
|
|
||||||
|
# Now we join the local user to the room
|
||||||
|
join_tuple = self.get_success(
|
||||||
|
create_event(
|
||||||
|
self.hs,
|
||||||
|
prev_event_ids=[invite_tuple[0].event_id],
|
||||||
|
auth_event_ids=[create_tuple[0].event_id, invite_tuple[0].event_id],
|
||||||
|
type=EventTypes.Member,
|
||||||
|
state_key=user1_id,
|
||||||
|
content={"membership": Membership.JOIN},
|
||||||
|
sender=user1_id,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.get_success(persistence.persist_event(*join_tuple))
|
||||||
|
|
||||||
|
# Doing an SS request should return a positive `bump_stamp`, even though
|
||||||
|
# the only event that matches the bump types has as negative stream
|
||||||
|
# ordering.
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.assertGreater(response_body["rooms"][room_id]["bump_stamp"], 0)
|
|
@ -0,0 +1,713 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import EventTypes, Membership
|
||||||
|
from synapse.handlers.sliding_sync import StateValues
|
||||||
|
from synapse.rest.client import login, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
from tests.test_utils.event_injection import mark_event_as_partial_state
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncRoomsRequiredStateTestCase(SlidingSyncBase):
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` in the Sliding Sync API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.storage_controllers = hs.get_storage_controllers()
|
||||||
|
|
||||||
|
def test_rooms_no_required_state(self) -> None:
|
||||||
|
"""
|
||||||
|
Empty `rooms.required_state` should not return any state events in the room
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
# Empty `required_state`
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# No `required_state` in response
|
||||||
|
self.assertIsNone(
|
||||||
|
response_body["rooms"][room_id1].get("required_state"),
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_required_state_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` returns requested state events in the room during an
|
||||||
|
initial sync.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.RoomHistoryVisibility, ""],
|
||||||
|
# This one doesn't exist in the room
|
||||||
|
[EventTypes.Tombstone, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
state_map[(EventTypes.RoomHistoryVisibility, "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` returns requested state events in the room during an
|
||||||
|
incremental sync.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.RoomHistoryVisibility, ""],
|
||||||
|
# This one doesn't exist in the room
|
||||||
|
[EventTypes.Tombstone, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Send a message so the room comes down sync.
|
||||||
|
self.helper.send(room_id1, "msg", tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the incremental Sliding Sync request
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# We only return updates but only if we've sent the room down the
|
||||||
|
# connection before.
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("required_state"))
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_incremental_sync_restart(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` returns requested state events in the room during an
|
||||||
|
incremental sync, after a restart (and so the in memory caches are reset).
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.RoomHistoryVisibility, ""],
|
||||||
|
# This one doesn't exist in the room
|
||||||
|
[EventTypes.Tombstone, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Reset the in-memory cache
|
||||||
|
self.hs.get_sliding_sync_handler().connection_store._connections.clear()
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# If the cache has been cleared then we do expect the state to come down
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
state_map[(EventTypes.RoomHistoryVisibility, "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_wildcard(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` returns all state events when using wildcard `["*", "*"]`.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key="",
|
||||||
|
body={"foo": "bar"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key="namespaced",
|
||||||
|
body={"foo": "bar"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with wildcards for the `event_type` and `state_key`
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[StateValues.WILDCARD, StateValues.WILDCARD],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
# We should see all the state events in the room
|
||||||
|
state_map.values(),
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_wildcard_event_type(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` returns relevant state events when using wildcard in
|
||||||
|
the event_type `["*", "foobarbaz"]`.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key="",
|
||||||
|
body={"foo": "bar"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key=user2_id,
|
||||||
|
body={"foo": "bar"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with wildcards for the `event_type`
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[StateValues.WILDCARD, user2_id],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# We expect at-least any state event with the `user2_id` as the `state_key`
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Member, user2_id)],
|
||||||
|
state_map[("org.matrix.foo_state", user2_id)],
|
||||||
|
},
|
||||||
|
# Ideally, this would be exact but we're currently returning all state
|
||||||
|
# events when the `event_type` is a wildcard.
|
||||||
|
exact=False,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_wildcard_state_key(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` returns relevant state events when using wildcard in
|
||||||
|
the state_key `["foobarbaz","*"]`.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with wildcards for the `state_key`
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Member, StateValues.WILDCARD],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Member, user1_id)],
|
||||||
|
state_map[(EventTypes.Member, user2_id)],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_lazy_loading_room_members(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` returns people relevant to the timeline when
|
||||||
|
lazy-loading room members, `["m.room.member","$LAZY"]`.
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
user3_id = self.register_user("user3", "pass")
|
||||||
|
user3_tok = self.login(user3_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
self.helper.join(room_id1, user3_id, tok=user3_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "2", tok=user3_tok)
|
||||||
|
self.helper.send(room_id1, "3", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with lazy loading for the room members
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.Member, StateValues.LAZY],
|
||||||
|
],
|
||||||
|
"timeline_limit": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only user2 and user3 sent events in the 3 events we see in the `timeline`
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
state_map[(EventTypes.Member, user2_id)],
|
||||||
|
state_map[(EventTypes.Member, user3_id)],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_me(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` correctly handles $ME.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "1", tok=user2_tok)
|
||||||
|
|
||||||
|
# Also send normal state events with state keys of the users, first
|
||||||
|
# change the power levels to allow this.
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type=EventTypes.PowerLevels,
|
||||||
|
body={"users": {user1_id: 50, user2_id: 100}},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo",
|
||||||
|
state_key=user1_id,
|
||||||
|
body={},
|
||||||
|
tok=user1_tok,
|
||||||
|
)
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo",
|
||||||
|
state_key=user2_id,
|
||||||
|
body={},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with a request for '$ME'.
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.Member, StateValues.ME],
|
||||||
|
["org.matrix.foo", StateValues.ME],
|
||||||
|
],
|
||||||
|
"timeline_limit": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only user2 and user3 sent events in the 3 events we see in the `timeline`
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
state_map[(EventTypes.Member, user1_id)],
|
||||||
|
state_map[("org.matrix.foo", user1_id)],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
@parameterized.expand([(Membership.LEAVE,), (Membership.BAN,)])
|
||||||
|
def test_rooms_required_state_leave_ban(self, stop_membership: str) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` should not return state past a leave/ban event.
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
user3_id = self.register_user("user3", "pass")
|
||||||
|
user3_tok = self.login(user3_id, "pass")
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.Member, "*"],
|
||||||
|
["org.matrix.foo_state", ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
self.helper.join(room_id1, user3_id, tok=user3_tok)
|
||||||
|
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key="",
|
||||||
|
body={"foo": "bar"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
if stop_membership == Membership.LEAVE:
|
||||||
|
# User 1 leaves
|
||||||
|
self.helper.leave(room_id1, user1_id, tok=user1_tok)
|
||||||
|
elif stop_membership == Membership.BAN:
|
||||||
|
# User 1 is banned
|
||||||
|
self.helper.ban(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change the state after user 1 leaves
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key="",
|
||||||
|
body={"foo": "qux"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
self.helper.leave(room_id1, user3_id, tok=user3_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with lazy loading for the room members
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# Only user2 and user3 sent events in the 3 events we see in the `timeline`
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
state_map[(EventTypes.Member, user1_id)],
|
||||||
|
state_map[(EventTypes.Member, user2_id)],
|
||||||
|
state_map[(EventTypes.Member, user3_id)],
|
||||||
|
state_map[("org.matrix.foo_state", "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_combine_superset(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms.required_state` is combined across lists and room subscriptions.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.foo_state",
|
||||||
|
state_key="",
|
||||||
|
body={"foo": "bar"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
self.helper.send_state(
|
||||||
|
room_id1,
|
||||||
|
event_type="org.matrix.bar_state",
|
||||||
|
state_key="",
|
||||||
|
body={"bar": "qux"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request with wildcards for the `state_key`
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
[EventTypes.Member, user1_id],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
},
|
||||||
|
"bar-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Member, StateValues.WILDCARD],
|
||||||
|
["org.matrix.foo_state", ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"room_subscriptions": {
|
||||||
|
room_id1: {
|
||||||
|
"required_state": [["org.matrix.bar_state", ""]],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
state_map = self.get_success(
|
||||||
|
self.storage_controllers.state.get_current_state(room_id1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._assertRequiredStateIncludes(
|
||||||
|
response_body["rooms"][room_id1]["required_state"],
|
||||||
|
{
|
||||||
|
state_map[(EventTypes.Create, "")],
|
||||||
|
state_map[(EventTypes.Member, user1_id)],
|
||||||
|
state_map[(EventTypes.Member, user2_id)],
|
||||||
|
state_map[("org.matrix.foo_state", "")],
|
||||||
|
state_map[("org.matrix.bar_state", "")],
|
||||||
|
},
|
||||||
|
exact=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
|
||||||
|
|
||||||
|
def test_rooms_required_state_partial_state(self) -> None:
|
||||||
|
"""
|
||||||
|
Test partially-stated room are excluded unless `rooms.required_state` is
|
||||||
|
lazy-loading room members.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
room_id2 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
_join_response1 = self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
join_response2 = self.helper.join(room_id2, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Mark room2 as partial state
|
||||||
|
self.get_success(
|
||||||
|
mark_event_as_partial_state(self.hs, join_response2["event_id"], room_id2)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request (NOT lazy-loading room members)
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make sure the list includes room1 but room2 is excluded because it's still
|
||||||
|
# partially-stated
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 1],
|
||||||
|
"room_ids": [room_id1],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_body["lists"]["foo-list"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request (with lazy-loading room members)
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [
|
||||||
|
[EventTypes.Create, ""],
|
||||||
|
# Lazy-load room members
|
||||||
|
[EventTypes.Member, StateValues.LAZY],
|
||||||
|
],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# The list should include both rooms now because we're lazy-loading room members
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 1],
|
||||||
|
"room_ids": [room_id2, room_id1],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_body["lists"]["foo-list"],
|
||||||
|
)
|
|
@ -0,0 +1,493 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.rest.client import login, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import StreamToken
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.rest.client.sliding_sync.test_sliding_sync import SlidingSyncBase
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncRoomsTimelineTestCase(SlidingSyncBase):
|
||||||
|
"""
|
||||||
|
Test `rooms.timeline` in the Sliding Sync API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.storage_controllers = hs.get_storage_controllers()
|
||||||
|
|
||||||
|
def test_rooms_limited_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that we mark `rooms` as `limited=True` when we saturate the `timeline_limit`
|
||||||
|
on initial sync.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity2", tok=user2_tok)
|
||||||
|
event_response3 = self.helper.send(room_id1, "activity3", tok=user2_tok)
|
||||||
|
event_pos3 = self.get_success(
|
||||||
|
self.store.get_position_for_event(event_response3["event_id"])
|
||||||
|
)
|
||||||
|
event_response4 = self.helper.send(room_id1, "activity4", tok=user2_tok)
|
||||||
|
event_pos4 = self.get_success(
|
||||||
|
self.store.get_position_for_event(event_response4["event_id"])
|
||||||
|
)
|
||||||
|
event_response5 = self.helper.send(room_id1, "activity5", tok=user2_tok)
|
||||||
|
user1_join_response = self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# We expect to saturate the `timeline_limit` (there are more than 3 messages in the room)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["limited"],
|
||||||
|
True,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# Check to make sure the latest events are returned
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1]["timeline"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
event_response4["event_id"],
|
||||||
|
event_response5["event_id"],
|
||||||
|
user1_join_response["event_id"],
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["timeline"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check to make sure the `prev_batch` points at the right place
|
||||||
|
prev_batch_token = self.get_success(
|
||||||
|
StreamToken.from_string(
|
||||||
|
self.store, response_body["rooms"][room_id1]["prev_batch"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
prev_batch_room_stream_token_serialized = self.get_success(
|
||||||
|
prev_batch_token.room_key.to_string(self.store)
|
||||||
|
)
|
||||||
|
# If we use the `prev_batch` token to look backwards, we should see `event3`
|
||||||
|
# next so make sure the token encompasses it
|
||||||
|
self.assertEqual(
|
||||||
|
event_pos3.persisted_after(prev_batch_token.room_key),
|
||||||
|
False,
|
||||||
|
f"`prev_batch` token {prev_batch_room_stream_token_serialized} should be >= event_pos3={self.get_success(event_pos3.to_room_stream_token().to_string(self.store))}",
|
||||||
|
)
|
||||||
|
# If we use the `prev_batch` token to look backwards, we shouldn't see `event4`
|
||||||
|
# anymore since it was just returned in this response.
|
||||||
|
self.assertEqual(
|
||||||
|
event_pos4.persisted_after(prev_batch_token.room_key),
|
||||||
|
True,
|
||||||
|
f"`prev_batch` token {prev_batch_room_stream_token_serialized} should be < event_pos4={self.get_success(event_pos4.to_room_stream_token().to_string(self.store))}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# With no `from_token` (initial sync), it's all historical since there is no
|
||||||
|
# "live" range
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["num_live"],
|
||||||
|
0,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_not_limited_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that we mark `rooms` as `limited=False` when there are no more events to
|
||||||
|
paginate to.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity2", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity3", tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
timeline_limit = 100
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": timeline_limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# The timeline should be `limited=False` because we have all of the events (no
|
||||||
|
# more to paginate to)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["limited"],
|
||||||
|
False,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
expected_number_of_events = 9
|
||||||
|
# We're just looking to make sure we got all of the events before hitting the `timeline_limit`
|
||||||
|
self.assertEqual(
|
||||||
|
len(response_body["rooms"][room_id1]["timeline"]),
|
||||||
|
expected_number_of_events,
|
||||||
|
response_body["rooms"][room_id1]["timeline"],
|
||||||
|
)
|
||||||
|
self.assertLessEqual(expected_number_of_events, timeline_limit)
|
||||||
|
|
||||||
|
# With no `from_token` (initial sync), it's all historical since there is no
|
||||||
|
# "live" token range.
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["num_live"],
|
||||||
|
0,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test `rooms` data during an incremental sync after an initial sync.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
self.helper.send(room_id1, "activity before initial sync1", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make an initial Sliding Sync request to grab a token. This is also a sanity
|
||||||
|
# check that we can go from initial to incremental sync.
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Send some events but don't send enough to saturate the `timeline_limit`.
|
||||||
|
# We want to later test that we only get the new events since the `next_pos`
|
||||||
|
event_response2 = self.helper.send(room_id1, "activity after2", tok=user2_tok)
|
||||||
|
event_response3 = self.helper.send(room_id1, "activity after3", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request (what we're trying to test)
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# We only expect to see the new events since the last sync which isn't enough to
|
||||||
|
# fill up the `timeline_limit`.
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["limited"],
|
||||||
|
False,
|
||||||
|
f'Our `timeline_limit` was {sync_body["lists"]["foo-list"]["timeline_limit"]} '
|
||||||
|
+ f'and {len(response_body["rooms"][room_id1]["timeline"])} events were returned in the timeline. '
|
||||||
|
+ str(response_body["rooms"][room_id1]),
|
||||||
|
)
|
||||||
|
# Check to make sure the latest events are returned
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1]["timeline"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
event_response2["event_id"],
|
||||||
|
event_response3["event_id"],
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["timeline"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# All events are "live"
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["num_live"],
|
||||||
|
2,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_newly_joined_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that when we make an incremental sync with a `newly_joined` `rooms`, we are
|
||||||
|
able to see some historical events before the `from_token`.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before token1", tok=user2_tok)
|
||||||
|
event_response2 = self.helper.send(
|
||||||
|
room_id1, "activity before token2", tok=user2_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
# The `timeline_limit` is set to 4 so we can at least see one historical event
|
||||||
|
# before the `from_token`. We should see historical events because this is a
|
||||||
|
# `newly_joined` room.
|
||||||
|
timeline_limit = 4
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": timeline_limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Join the room after the `from_token` which will make us consider this room as
|
||||||
|
# `newly_joined`.
|
||||||
|
user1_join_response = self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Send some events but don't send enough to saturate the `timeline_limit`.
|
||||||
|
# We want to later test that we only get the new events since the `next_pos`
|
||||||
|
event_response3 = self.helper.send(
|
||||||
|
room_id1, "activity after token3", tok=user2_tok
|
||||||
|
)
|
||||||
|
event_response4 = self.helper.send(
|
||||||
|
room_id1, "activity after token4", tok=user2_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request (what we're trying to test)
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# We should see the new events and the rest should be filled with historical
|
||||||
|
# events which will make us `limited=True` since there are more to paginate to.
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["limited"],
|
||||||
|
True,
|
||||||
|
f"Our `timeline_limit` was {timeline_limit} "
|
||||||
|
+ f'and {len(response_body["rooms"][room_id1]["timeline"])} events were returned in the timeline. '
|
||||||
|
+ str(response_body["rooms"][room_id1]),
|
||||||
|
)
|
||||||
|
# Check to make sure that the "live" and historical events are returned
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1]["timeline"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
event_response2["event_id"],
|
||||||
|
user1_join_response["event_id"],
|
||||||
|
event_response3["event_id"],
|
||||||
|
event_response4["event_id"],
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["timeline"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only events after the `from_token` are "live" (join, event3, event4)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["num_live"],
|
||||||
|
3,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_ban_initial_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `rooms` we are banned from in an intial sync only allows us to see
|
||||||
|
timeline events up to the ban event.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before2", tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
event_response3 = self.helper.send(room_id1, "activity after3", tok=user2_tok)
|
||||||
|
event_response4 = self.helper.send(room_id1, "activity after4", tok=user2_tok)
|
||||||
|
user1_ban_response = self.helper.ban(
|
||||||
|
room_id1, src=user2_id, targ=user1_id, tok=user2_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity after5", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after6", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# We should see events before the ban but not after
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1]["timeline"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
event_response3["event_id"],
|
||||||
|
event_response4["event_id"],
|
||||||
|
user1_ban_response["event_id"],
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["timeline"],
|
||||||
|
)
|
||||||
|
# No "live" events in an initial sync (no `from_token` to define the "live"
|
||||||
|
# range)
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["num_live"],
|
||||||
|
0,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# There are more events to paginate to
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["limited"],
|
||||||
|
True,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_ban_incremental_sync1(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `rooms` we are banned from during the next incremental sync only
|
||||||
|
allows us to see timeline events up to the ban event.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before1", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before2", tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
event_response3 = self.helper.send(room_id1, "activity after3", tok=user2_tok)
|
||||||
|
event_response4 = self.helper.send(room_id1, "activity after4", tok=user2_tok)
|
||||||
|
# The ban is within the token range (between the `from_token` and the sliding
|
||||||
|
# sync request)
|
||||||
|
user1_ban_response = self.helper.ban(
|
||||||
|
room_id1, src=user2_id, targ=user1_id, tok=user2_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity after5", tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity after6", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the incremental Sliding Sync request
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# We should see events before the ban but not after
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
event["event_id"]
|
||||||
|
for event in response_body["rooms"][room_id1]["timeline"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
event_response3["event_id"],
|
||||||
|
event_response4["event_id"],
|
||||||
|
user1_ban_response["event_id"],
|
||||||
|
],
|
||||||
|
response_body["rooms"][room_id1]["timeline"],
|
||||||
|
)
|
||||||
|
# All live events in the incremental sync
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["num_live"],
|
||||||
|
3,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
# There aren't anymore events to paginate to in this range
|
||||||
|
self.assertEqual(
|
||||||
|
response_body["rooms"][room_id1]["limited"],
|
||||||
|
False,
|
||||||
|
response_body["rooms"][room_id1],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_ban_incremental_sync2(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that `rooms` we are banned from before the incremental sync don't return
|
||||||
|
any events in the timeline.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.send(room_id1, "activity before1", tok=user2_tok)
|
||||||
|
self.helper.join(room_id1, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity after2", tok=user2_tok)
|
||||||
|
# The ban is before we get our `from_token`
|
||||||
|
self.helper.ban(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity after3", tok=user2_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
self.helper.send(room_id1, "activity after4", tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the incremental Sliding Sync request
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# Nothing to see for this banned user in the room in the token range
|
||||||
|
self.assertIsNone(response_body["rooms"].get(room_id1))
|
|
@ -0,0 +1,974 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple
|
||||||
|
|
||||||
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
import synapse.rest.admin
|
||||||
|
from synapse.api.constants import (
|
||||||
|
AccountDataTypes,
|
||||||
|
EventContentFields,
|
||||||
|
EventTypes,
|
||||||
|
RoomTypes,
|
||||||
|
)
|
||||||
|
from synapse.events import EventBase
|
||||||
|
from synapse.rest.client import devices, login, receipts, room, sync
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import (
|
||||||
|
JsonDict,
|
||||||
|
RoomStreamToken,
|
||||||
|
SlidingSyncStreamToken,
|
||||||
|
StreamKeyType,
|
||||||
|
StreamToken,
|
||||||
|
)
|
||||||
|
from synapse.util import Clock
|
||||||
|
from synapse.util.stringutils import random_string
|
||||||
|
|
||||||
|
from tests import unittest
|
||||||
|
from tests.server import TimedOutException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncBase(unittest.HomeserverTestCase):
|
||||||
|
"""Base class for sliding sync test cases"""
|
||||||
|
|
||||||
|
sync_endpoint = "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"
|
||||||
|
|
||||||
|
def default_config(self) -> JsonDict:
|
||||||
|
config = super().default_config()
|
||||||
|
# Enable sliding sync
|
||||||
|
config["experimental_features"] = {"msc3575_enabled": True}
|
||||||
|
return config
|
||||||
|
|
||||||
|
def do_sync(
|
||||||
|
self, sync_body: JsonDict, *, since: Optional[str] = None, tok: str
|
||||||
|
) -> Tuple[JsonDict, str]:
|
||||||
|
"""Do a sliding sync request with given body.
|
||||||
|
|
||||||
|
Asserts the request was successful.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
sync_body: The full request body to use
|
||||||
|
since: Optional since token
|
||||||
|
tok: Access token to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple of the response body and the `pos` field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sync_path = self.sync_endpoint
|
||||||
|
if since:
|
||||||
|
sync_path += f"?pos={since}"
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
method="POST",
|
||||||
|
path=sync_path,
|
||||||
|
content=sync_body,
|
||||||
|
access_token=tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
return channel.json_body, channel.json_body["pos"]
|
||||||
|
|
||||||
|
def _assertRequiredStateIncludes(
|
||||||
|
self,
|
||||||
|
actual_required_state: Any,
|
||||||
|
expected_state_events: Iterable[EventBase],
|
||||||
|
exact: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Wrapper around `assertIncludes` to give slightly better looking diff error
|
||||||
|
messages that include some context "$event_id (type, state_key)".
|
||||||
|
|
||||||
|
Args:
|
||||||
|
actual_required_state: The "required_state" of a room from a Sliding Sync
|
||||||
|
request response.
|
||||||
|
expected_state_events: The expected state events to be included in the
|
||||||
|
`actual_required_state`.
|
||||||
|
exact: Whether the actual state should be exactly equal to the expected
|
||||||
|
state (no extras).
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert isinstance(actual_required_state, list)
|
||||||
|
for event in actual_required_state:
|
||||||
|
assert isinstance(event, dict)
|
||||||
|
|
||||||
|
self.assertIncludes(
|
||||||
|
{
|
||||||
|
f'{event["event_id"]} ("{event["type"]}", "{event["state_key"]}")'
|
||||||
|
for event in actual_required_state
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f'{event.event_id} ("{event.type}", "{event.state_key}")'
|
||||||
|
for event in expected_state_events
|
||||||
|
},
|
||||||
|
exact=exact,
|
||||||
|
# Message to help understand the diff in context
|
||||||
|
message=str(actual_required_state),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _bump_notifier_wait_for_events(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
wake_stream_key: Literal[
|
||||||
|
StreamKeyType.ACCOUNT_DATA,
|
||||||
|
StreamKeyType.PRESENCE,
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Wake-up a `notifier.wait_for_events(user_id)` call without affecting the Sliding
|
||||||
|
Sync results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user ID to wake up the notifier for
|
||||||
|
wake_stream_key: The stream key to wake up. This will create an actual new
|
||||||
|
entity in that stream so it's best to choose one that won't affect the
|
||||||
|
Sliding Sync results you're testing for. In other words, if your testing
|
||||||
|
account data, choose `StreamKeyType.PRESENCE` instead. We support two
|
||||||
|
possible stream keys because you're probably testing one or the other so
|
||||||
|
one is always a "safe" option.
|
||||||
|
"""
|
||||||
|
# We're expecting some new activity from this point onwards
|
||||||
|
from_token = self.hs.get_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
|
||||||
|
|
||||||
|
notifier = self.hs.get_notifier()
|
||||||
|
|
||||||
|
# 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 = notifier.wait_for_events(
|
||||||
|
user_id,
|
||||||
|
1000,
|
||||||
|
_on_new_acivity,
|
||||||
|
from_token=from_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the account data or presence so that `notifier.wait_for_events(...)`
|
||||||
|
# wakes up. We chose these two options because they're least likely to show up
|
||||||
|
# in the Sliding Sync response so it won't affect whether we have results.
|
||||||
|
if wake_stream_key == StreamKeyType.ACCOUNT_DATA:
|
||||||
|
self.get_success(
|
||||||
|
self.hs.get_account_data_handler().add_account_data_for_user(
|
||||||
|
user_id,
|
||||||
|
"org.matrix.foobarbaz",
|
||||||
|
{"foo": "bar"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif wake_stream_key == StreamKeyType.PRESENCE:
|
||||||
|
sending_user_id = self.register_user(
|
||||||
|
"user_bump_notifier_wait_for_events_" + random_string(10), "pass"
|
||||||
|
)
|
||||||
|
sending_user_tok = self.login(sending_user_id, "pass")
|
||||||
|
test_msg = {"foo": "bar"}
|
||||||
|
chan = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_matrix/client/r0/sendToDevice/m.test/1234",
|
||||||
|
content={"messages": {user_id: {"d1": test_msg}}},
|
||||||
|
access_token=sending_user_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(chan.code, 200, chan.result)
|
||||||
|
else:
|
||||||
|
assert_never(wake_stream_key)
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SlidingSyncTestCase(SlidingSyncBase):
|
||||||
|
"""
|
||||||
|
Tests regarding MSC3575 Sliding Sync `/sync` endpoint.
|
||||||
|
|
||||||
|
Please put tests in more specific test files if applicable. This test class is meant
|
||||||
|
for generic behavior of the endpoint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
sync.register_servlets,
|
||||||
|
devices.register_servlets,
|
||||||
|
receipts.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
self.event_sources = hs.get_event_sources()
|
||||||
|
self.storage_controllers = hs.get_storage_controllers()
|
||||||
|
self.account_data_handler = hs.get_account_data_handler()
|
||||||
|
|
||||||
|
def _add_new_dm_to_global_account_data(
|
||||||
|
self, source_user_id: str, target_user_id: str, target_room_id: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Helper to handle inserting a new DM for the source user into global account data
|
||||||
|
(handles all of the list merging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_user_id: The user ID of the DM mapping we're going to update
|
||||||
|
target_user_id: User ID of the person the DM is with
|
||||||
|
target_room_id: Room ID of the DM
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get the current DM map
|
||||||
|
existing_dm_map = self.get_success(
|
||||||
|
self.store.get_global_account_data_by_type_for_user(
|
||||||
|
source_user_id, AccountDataTypes.DIRECT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Scrutinize the account data since it has no concrete type. We're just copying
|
||||||
|
# everything into a known type. It should be a mapping from user ID to a list of
|
||||||
|
# room IDs. Ignore anything else.
|
||||||
|
new_dm_map: Dict[str, List[str]] = {}
|
||||||
|
if isinstance(existing_dm_map, dict):
|
||||||
|
for user_id, room_ids in existing_dm_map.items():
|
||||||
|
if isinstance(user_id, str) and isinstance(room_ids, list):
|
||||||
|
for room_id in room_ids:
|
||||||
|
if isinstance(room_id, str):
|
||||||
|
new_dm_map[user_id] = new_dm_map.get(user_id, []) + [
|
||||||
|
room_id
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add the new DM to the map
|
||||||
|
new_dm_map[target_user_id] = new_dm_map.get(target_user_id, []) + [
|
||||||
|
target_room_id
|
||||||
|
]
|
||||||
|
# Save the DM map to global account data
|
||||||
|
self.get_success(
|
||||||
|
self.store.add_account_data_for_user(
|
||||||
|
source_user_id,
|
||||||
|
AccountDataTypes.DIRECT,
|
||||||
|
new_dm_map,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_dm_room(
|
||||||
|
self,
|
||||||
|
inviter_user_id: str,
|
||||||
|
inviter_tok: str,
|
||||||
|
invitee_user_id: str,
|
||||||
|
invitee_tok: str,
|
||||||
|
should_join_room: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Helper to create a DM room as the "inviter" and invite the "invitee" user to the
|
||||||
|
room. The "invitee" user also will join the room. The `m.direct` account data
|
||||||
|
will be set for both users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a room and send an invite the other user
|
||||||
|
room_id = self.helper.create_room_as(
|
||||||
|
inviter_user_id,
|
||||||
|
is_public=False,
|
||||||
|
tok=inviter_tok,
|
||||||
|
)
|
||||||
|
self.helper.invite(
|
||||||
|
room_id,
|
||||||
|
src=inviter_user_id,
|
||||||
|
targ=invitee_user_id,
|
||||||
|
tok=inviter_tok,
|
||||||
|
extra_data={"is_direct": True},
|
||||||
|
)
|
||||||
|
if should_join_room:
|
||||||
|
# Person that was invited joins the room
|
||||||
|
self.helper.join(room_id, invitee_user_id, tok=invitee_tok)
|
||||||
|
|
||||||
|
# Mimic the client setting the room as a direct message in the global account
|
||||||
|
# data for both users.
|
||||||
|
self._add_new_dm_to_global_account_data(
|
||||||
|
invitee_user_id, inviter_user_id, room_id
|
||||||
|
)
|
||||||
|
self._add_new_dm_to_global_account_data(
|
||||||
|
inviter_user_id, invitee_user_id, room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return room_id
|
||||||
|
|
||||||
|
def test_sync_list(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that room IDs show up in the Sliding Sync `lists`
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make sure it has the foo-list we requested
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"].keys()),
|
||||||
|
["foo-list"],
|
||||||
|
response_body["lists"].keys(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the list includes the room we are joined to
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [room_id],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_body["lists"]["foo-list"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wait_for_sync_token(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that worker will wait until it catches up to the given token
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
# Create a future token that will cause us to wait. Since we never send a new
|
||||||
|
# event to reach that future stream_ordering, the worker will wait until the
|
||||||
|
# full timeout.
|
||||||
|
stream_id_gen = self.store.get_events_stream_id_generator()
|
||||||
|
stream_id = self.get_success(stream_id_gen.get_next().__aenter__())
|
||||||
|
current_token = self.event_sources.get_current_token()
|
||||||
|
future_position_token = current_token.copy_and_replace(
|
||||||
|
StreamKeyType.ROOM,
|
||||||
|
RoomStreamToken(stream=stream_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
future_position_token_serialized = self.get_success(
|
||||||
|
SlidingSyncStreamToken(future_position_token, 0).to_string(self.store)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?pos={future_position_token_serialized}",
|
||||||
|
content=sync_body,
|
||||||
|
access_token=user1_tok,
|
||||||
|
await_result=False,
|
||||||
|
)
|
||||||
|
# Block for 10 seconds to make `notifier.wait_for_stream_token(from_token)`
|
||||||
|
# timeout
|
||||||
|
with self.assertRaises(TimedOutException):
|
||||||
|
channel.await_result(timeout_ms=9900)
|
||||||
|
channel.await_result(timeout_ms=200)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# We expect the next `pos` in the result to be the same as what we requested
|
||||||
|
# with because we weren't able to find anything new yet.
|
||||||
|
self.assertEqual(channel.json_body["pos"], future_position_token_serialized)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 0]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?timeout=10000&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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 room with new events to trigger new results
|
||||||
|
event_response1 = self.helper.send(
|
||||||
|
room_id, "new activity in room", tok=user1_tok
|
||||||
|
)
|
||||||
|
# Should respond before the 10 second timeout
|
||||||
|
channel.await_result(timeout_ms=3000)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Check to make sure the new event is returned
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
event["event_id"]
|
||||||
|
for event in channel.json_body["rooms"][room_id]["timeline"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
event_response1["event_id"],
|
||||||
|
],
|
||||||
|
channel.json_body["rooms"][room_id]["timeline"],
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
doesn't trigger a false-positive for new data.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
user2_id = self.register_user("user2", "pass")
|
||||||
|
user2_tok = self.login(user2_id, "pass")
|
||||||
|
|
||||||
|
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 0]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?timeout=10000&pos={from_token}",
|
||||||
|
content=sync_body,
|
||||||
|
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, wake_stream_key=StreamKeyType.ACCOUNT_DATA
|
||||||
|
)
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# There should be no room sent down.
|
||||||
|
self.assertFalse(channel.json_body["rooms"])
|
||||||
|
|
||||||
|
def test_filter_list(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that filters apply to `lists`
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Create a DM room
|
||||||
|
joined_dm_room_id = self._create_dm_room(
|
||||||
|
inviter_user_id=user1_id,
|
||||||
|
inviter_tok=user1_tok,
|
||||||
|
invitee_user_id=user2_id,
|
||||||
|
invitee_tok=user2_tok,
|
||||||
|
should_join_room=True,
|
||||||
|
)
|
||||||
|
invited_dm_room_id = self._create_dm_room(
|
||||||
|
inviter_user_id=user1_id,
|
||||||
|
inviter_tok=user1_tok,
|
||||||
|
invitee_user_id=user2_id,
|
||||||
|
invitee_tok=user2_tok,
|
||||||
|
should_join_room=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a normal room
|
||||||
|
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Create a room that user1 is invited to
|
||||||
|
invite_room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||||
|
self.helper.invite(invite_room_id, src=user2_id, targ=user1_id, tok=user2_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
# Absense of filters does not imply "False" values
|
||||||
|
"all": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
"filters": {},
|
||||||
|
},
|
||||||
|
# Test single truthy filter
|
||||||
|
"dms": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
"filters": {"is_dm": True},
|
||||||
|
},
|
||||||
|
# Test single falsy filter
|
||||||
|
"non-dms": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
"filters": {"is_dm": False},
|
||||||
|
},
|
||||||
|
# Test how multiple filters should stack (AND'd together)
|
||||||
|
"room-invites": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
"filters": {"is_dm": False, "is_invite": True},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make sure it has the foo-list we requested
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"].keys()),
|
||||||
|
["all", "dms", "non-dms", "room-invites"],
|
||||||
|
response_body["lists"].keys(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the lists have the correct rooms
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["all"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [
|
||||||
|
invite_room_id,
|
||||||
|
room_id,
|
||||||
|
invited_dm_room_id,
|
||||||
|
joined_dm_room_id,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
list(response_body["lists"]["all"]),
|
||||||
|
)
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["dms"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [invited_dm_room_id, joined_dm_room_id],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
list(response_body["lists"]["dms"]),
|
||||||
|
)
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["non-dms"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [invite_room_id, room_id],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
list(response_body["lists"]["non-dms"]),
|
||||||
|
)
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["room-invites"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [invite_room_id],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
list(response_body["lists"]["room-invites"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure DM's are correctly marked
|
||||||
|
self.assertDictEqual(
|
||||||
|
{
|
||||||
|
room_id: room.get("is_dm")
|
||||||
|
for room_id, room in response_body["rooms"].items()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
invite_room_id: None,
|
||||||
|
room_id: None,
|
||||||
|
invited_dm_room_id: True,
|
||||||
|
joined_dm_room_id: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_filter_regardless_of_membership_server_left_room(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that filters apply to rooms regardless of membership. We're also
|
||||||
|
compounding the problem by having all of the local users leave the room causing
|
||||||
|
our server to leave the room.
|
||||||
|
|
||||||
|
We want to make sure that if someone is filtering rooms, and leaves, you still
|
||||||
|
get that final update down sync that you left.
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Create a normal room
|
||||||
|
room_id = self.helper.create_room_as(user1_id, tok=user2_tok)
|
||||||
|
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Create an encrypted space room
|
||||||
|
space_room_id = self.helper.create_room_as(
|
||||||
|
user2_id,
|
||||||
|
tok=user2_tok,
|
||||||
|
extra_content={
|
||||||
|
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.helper.send_state(
|
||||||
|
space_room_id,
|
||||||
|
EventTypes.RoomEncryption,
|
||||||
|
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||||
|
tok=user2_tok,
|
||||||
|
)
|
||||||
|
self.helper.join(space_room_id, user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make an initial Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint,
|
||||||
|
{
|
||||||
|
"lists": {
|
||||||
|
"all-list": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
"filters": {},
|
||||||
|
},
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
"filters": {
|
||||||
|
"is_encrypted": True,
|
||||||
|
"room_types": [RoomTypes.SPACE],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
from_token = channel.json_body["pos"]
|
||||||
|
|
||||||
|
# Make sure the response has the lists we requested
|
||||||
|
self.assertListEqual(
|
||||||
|
list(channel.json_body["lists"].keys()),
|
||||||
|
["all-list", "foo-list"],
|
||||||
|
channel.json_body["lists"].keys(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the lists have the correct rooms
|
||||||
|
self.assertListEqual(
|
||||||
|
list(channel.json_body["lists"]["all-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [space_room_id, room_id],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertListEqual(
|
||||||
|
list(channel.json_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [space_room_id],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Everyone leaves the encrypted space room
|
||||||
|
self.helper.leave(space_room_id, user1_id, tok=user1_tok)
|
||||||
|
self.helper.leave(space_room_id, user2_id, tok=user2_tok)
|
||||||
|
|
||||||
|
# Make an incremental Sliding Sync request
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.sync_endpoint + f"?pos={from_token}",
|
||||||
|
{
|
||||||
|
"lists": {
|
||||||
|
"all-list": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
"filters": {},
|
||||||
|
},
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
"filters": {
|
||||||
|
"is_encrypted": True,
|
||||||
|
"room_types": [RoomTypes.SPACE],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
access_token=user1_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Make sure the lists have the correct rooms even though we `newly_left`
|
||||||
|
self.assertListEqual(
|
||||||
|
list(channel.json_body["lists"]["all-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [space_room_id, room_id],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertListEqual(
|
||||||
|
list(channel.json_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [space_room_id],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sort_list(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that the `lists` are sorted by `stream_ordering`
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True)
|
||||||
|
room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True)
|
||||||
|
room_id3 = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True)
|
||||||
|
|
||||||
|
# Activity that will order the rooms
|
||||||
|
self.helper.send(room_id3, "activity in room3", tok=user1_tok)
|
||||||
|
self.helper.send(room_id1, "activity in room1", tok=user1_tok)
|
||||||
|
self.helper.send(room_id2, "activity in room2", tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 99]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make sure it has the foo-list we requested
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"].keys()),
|
||||||
|
["foo-list"],
|
||||||
|
response_body["lists"].keys(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure the list is sorted in the way we expect
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 99],
|
||||||
|
"room_ids": [room_id2, room_id1, room_id3],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_body["lists"]["foo-list"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sliced_windows(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that the `lists` `ranges` are sliced correctly. Both sides of each range
|
||||||
|
are inclusive.
|
||||||
|
"""
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
_room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True)
|
||||||
|
room_id2 = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True)
|
||||||
|
room_id3 = self.helper.create_room_as(user1_id, tok=user1_tok, is_public=True)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request for a single room
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 0]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make sure it has the foo-list we requested
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"].keys()),
|
||||||
|
["foo-list"],
|
||||||
|
response_body["lists"].keys(),
|
||||||
|
)
|
||||||
|
# Make sure the list is sorted in the way we expect
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 0],
|
||||||
|
"room_ids": [room_id3],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_body["lists"]["foo-list"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the Sliding Sync request for the first two rooms
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make sure it has the foo-list we requested
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"].keys()),
|
||||||
|
["foo-list"],
|
||||||
|
response_body["lists"].keys(),
|
||||||
|
)
|
||||||
|
# Make sure the list is sorted in the way we expect
|
||||||
|
self.assertListEqual(
|
||||||
|
list(response_body["lists"]["foo-list"]["ops"]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "SYNC",
|
||||||
|
"range": [0, 1],
|
||||||
|
"room_ids": [room_id3, room_id2],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_body["lists"]["foo-list"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rooms_with_no_updates_do_not_come_down_incremental_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that rooms with no updates are returned in subsequent incremental
|
||||||
|
syncs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, from_token = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
|
||||||
|
# Make the incremental Sliding Sync request
|
||||||
|
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
|
||||||
|
|
||||||
|
# Nothing has happened in the room, so the room should not come down
|
||||||
|
# /sync.
|
||||||
|
self.assertIsNone(response_body["rooms"].get(room_id1))
|
||||||
|
|
||||||
|
def test_empty_initial_room_comes_down_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Test that rooms come down /sync even with empty required state and
|
||||||
|
timeline limit in initial sync.
|
||||||
|
"""
|
||||||
|
|
||||||
|
user1_id = self.register_user("user1", "pass")
|
||||||
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
|
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
|
||||||
|
sync_body = {
|
||||||
|
"lists": {
|
||||||
|
"foo-list": {
|
||||||
|
"ranges": [[0, 1]],
|
||||||
|
"required_state": [],
|
||||||
|
"timeline_limit": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make the Sliding Sync request
|
||||||
|
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
|
||||||
|
self.assertEqual(response_body["rooms"][room_id1]["initial"], True)
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue