|
19 | 19 |
|
20 | 20 | import synapse.rest.admin
|
21 | 21 | from synapse.api.constants import UserTypes
|
| 22 | +from synapse.api.errors import SynapseError |
22 | 23 | from synapse.api.room_versions import RoomVersion, RoomVersions
|
23 | 24 | from synapse.appservice import ApplicationService
|
24 | 25 | from synapse.rest.client import login, register, room, user_directory
|
25 | 26 | from synapse.server import HomeServer
|
26 | 27 | from synapse.storage.roommember import ProfileInfo
|
27 |
| -from synapse.types import UserProfile, create_requester |
| 28 | +from synapse.types import JsonDict, UserProfile, create_requester |
28 | 29 | from synapse.util import Clock
|
29 | 30 |
|
30 | 31 | from tests import unittest
|
31 | 32 | from tests.storage.test_user_directory import GetUserDirectoryTables
|
32 |
| -from tests.test_utils import make_awaitable |
| 33 | +from tests.test_utils import event_injection, make_awaitable |
33 | 34 | from tests.test_utils.event_injection import inject_member_event
|
34 | 35 | from tests.unittest import override_config
|
35 | 36 |
|
@@ -1103,3 +1104,185 @@ def test_disabling_room_list(self) -> None:
|
1103 | 1104 | )
|
1104 | 1105 | self.assertEqual(200, channel.code, channel.result)
|
1105 | 1106 | self.assertTrue(len(channel.json_body["results"]) == 0)
|
| 1107 | + |
| 1108 | + |
| 1109 | +class UserDirectoryRemoteProfileTestCase(unittest.HomeserverTestCase): |
| 1110 | + servlets = [ |
| 1111 | + login.register_servlets, |
| 1112 | + synapse.rest.admin.register_servlets, |
| 1113 | + register.register_servlets, |
| 1114 | + room.register_servlets, |
| 1115 | + ] |
| 1116 | + |
| 1117 | + def default_config(self) -> JsonDict: |
| 1118 | + config = super().default_config() |
| 1119 | + # Re-enables updating the user directory, as that functionality is needed below. |
| 1120 | + config["update_user_directory_from_worker"] = None |
| 1121 | + return config |
| 1122 | + |
| 1123 | + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: |
| 1124 | + self.store = hs.get_datastores().main |
| 1125 | + self.alice = self.register_user("alice", "alice123") |
| 1126 | + self.alice_tok = self.login("alice", "alice123") |
| 1127 | + self.user_dir_helper = GetUserDirectoryTables(self.store) |
| 1128 | + self.user_dir_handler = hs.get_user_directory_handler() |
| 1129 | + self.profile_handler = hs.get_profile_handler() |
| 1130 | + |
| 1131 | + # Cancel the startup call: in the steady-state case we can't rely on it anyway. |
| 1132 | + assert self.user_dir_handler._refresh_remote_profiles_call_later is not None |
| 1133 | + self.user_dir_handler._refresh_remote_profiles_call_later.cancel() |
| 1134 | + |
| 1135 | + def test_public_rooms_have_profiles_collected(self) -> None: |
| 1136 | + """ |
| 1137 | + In a public room, member state events are treated as reflecting the user's |
| 1138 | + real profile and they are accepted. |
| 1139 | + (The main motivation for accepting this is to prevent having to query |
| 1140 | + *every* single profile change over federation.) |
| 1141 | + """ |
| 1142 | + room_id = self.helper.create_room_as( |
| 1143 | + self.alice, is_public=True, tok=self.alice_tok |
| 1144 | + ) |
| 1145 | + self.get_success( |
| 1146 | + event_injection.inject_member_event( |
| 1147 | + self.hs, |
| 1148 | + room_id, |
| 1149 | + "@bruce:remote", |
| 1150 | + "join", |
| 1151 | + "@bruce:remote", |
| 1152 | + extra_content={ |
| 1153 | + "displayname": "Bruce!", |
| 1154 | + "avatar_url": "mxc://remote/123", |
| 1155 | + }, |
| 1156 | + ) |
| 1157 | + ) |
| 1158 | + # Sending this event makes the streams move forward after the injection... |
| 1159 | + self.helper.send(room_id, "Test", tok=self.alice_tok) |
| 1160 | + self.pump(0.1) |
| 1161 | + |
| 1162 | + profiles = self.get_success( |
| 1163 | + self.user_dir_helper.get_profiles_in_user_directory() |
| 1164 | + ) |
| 1165 | + self.assertEqual( |
| 1166 | + profiles.get("@bruce:remote"), |
| 1167 | + ProfileInfo(display_name="Bruce!", avatar_url="mxc://remote/123"), |
| 1168 | + ) |
| 1169 | + |
| 1170 | + def test_private_rooms_do_not_have_profiles_collected(self) -> None: |
| 1171 | + """ |
| 1172 | + In a private room, member state events are not pulled out and used to populate |
| 1173 | + the user directory. |
| 1174 | + """ |
| 1175 | + room_id = self.helper.create_room_as( |
| 1176 | + self.alice, is_public=False, tok=self.alice_tok |
| 1177 | + ) |
| 1178 | + self.get_success( |
| 1179 | + event_injection.inject_member_event( |
| 1180 | + self.hs, |
| 1181 | + room_id, |
| 1182 | + "@bruce:remote", |
| 1183 | + "join", |
| 1184 | + "@bruce:remote", |
| 1185 | + extra_content={ |
| 1186 | + "displayname": "super-duper bruce", |
| 1187 | + "avatar_url": "mxc://remote/456", |
| 1188 | + }, |
| 1189 | + ) |
| 1190 | + ) |
| 1191 | + # Sending this event makes the streams move forward after the injection... |
| 1192 | + self.helper.send(room_id, "Test", tok=self.alice_tok) |
| 1193 | + self.pump(0.1) |
| 1194 | + |
| 1195 | + profiles = self.get_success( |
| 1196 | + self.user_dir_helper.get_profiles_in_user_directory() |
| 1197 | + ) |
| 1198 | + self.assertNotIn("@bruce:remote", profiles) |
| 1199 | + |
| 1200 | + def test_private_rooms_have_profiles_requested(self) -> None: |
| 1201 | + """ |
| 1202 | + When a name changes in a private room, the homeserver instead requests |
| 1203 | + the user's global profile over federation. |
| 1204 | + """ |
| 1205 | + |
| 1206 | + async def get_remote_profile( |
| 1207 | + user_id: str, ignore_backoff: bool = True |
| 1208 | + ) -> JsonDict: |
| 1209 | + if user_id == "@bruce:remote": |
| 1210 | + return { |
| 1211 | + "displayname": "Sir Bruce Bruceson", |
| 1212 | + "avatar_url": "mxc://remote/789", |
| 1213 | + } |
| 1214 | + else: |
| 1215 | + raise ValueError(f"unable to fetch {user_id}") |
| 1216 | + |
| 1217 | + with patch.object(self.profile_handler, "get_profile", get_remote_profile): |
| 1218 | + # Continue from the earlier test... |
| 1219 | + self.test_private_rooms_do_not_have_profiles_collected() |
| 1220 | + |
| 1221 | + # Advance by a minute |
| 1222 | + self.reactor.advance(61.0) |
| 1223 | + |
| 1224 | + profiles = self.get_success( |
| 1225 | + self.user_dir_helper.get_profiles_in_user_directory() |
| 1226 | + ) |
| 1227 | + self.assertEqual( |
| 1228 | + profiles.get("@bruce:remote"), |
| 1229 | + ProfileInfo( |
| 1230 | + display_name="Sir Bruce Bruceson", avatar_url="mxc://remote/789" |
| 1231 | + ), |
| 1232 | + ) |
| 1233 | + |
| 1234 | + def test_profile_requests_are_retried(self) -> None: |
| 1235 | + """ |
| 1236 | + When we fail to fetch the user's profile over federation, |
| 1237 | + we try again later. |
| 1238 | + """ |
| 1239 | + has_failed_once = False |
| 1240 | + |
| 1241 | + async def get_remote_profile( |
| 1242 | + user_id: str, ignore_backoff: bool = True |
| 1243 | + ) -> JsonDict: |
| 1244 | + nonlocal has_failed_once |
| 1245 | + if user_id == "@bruce:remote": |
| 1246 | + if not has_failed_once: |
| 1247 | + has_failed_once = True |
| 1248 | + raise SynapseError(502, "temporary network problem") |
| 1249 | + |
| 1250 | + return { |
| 1251 | + "displayname": "Sir Bruce Bruceson", |
| 1252 | + "avatar_url": "mxc://remote/789", |
| 1253 | + } |
| 1254 | + else: |
| 1255 | + raise ValueError(f"unable to fetch {user_id}") |
| 1256 | + |
| 1257 | + with patch.object(self.profile_handler, "get_profile", get_remote_profile): |
| 1258 | + # Continue from the earlier test... |
| 1259 | + self.test_private_rooms_do_not_have_profiles_collected() |
| 1260 | + |
| 1261 | + # Advance by a minute |
| 1262 | + self.reactor.advance(61.0) |
| 1263 | + |
| 1264 | + # The request has already failed once |
| 1265 | + self.assertTrue(has_failed_once) |
| 1266 | + |
| 1267 | + # The profile has yet to be updated. |
| 1268 | + profiles = self.get_success( |
| 1269 | + self.user_dir_helper.get_profiles_in_user_directory() |
| 1270 | + ) |
| 1271 | + self.assertNotIn( |
| 1272 | + "@bruce:remote", |
| 1273 | + profiles, |
| 1274 | + ) |
| 1275 | + |
| 1276 | + # Advance by five minutes, after the backoff has finished |
| 1277 | + self.reactor.advance(301.0) |
| 1278 | + |
| 1279 | + # The profile should have been updated now |
| 1280 | + profiles = self.get_success( |
| 1281 | + self.user_dir_helper.get_profiles_in_user_directory() |
| 1282 | + ) |
| 1283 | + self.assertEqual( |
| 1284 | + profiles.get("@bruce:remote"), |
| 1285 | + ProfileInfo( |
| 1286 | + display_name="Sir Bruce Bruceson", avatar_url="mxc://remote/789" |
| 1287 | + ), |
| 1288 | + ) |
0 commit comments