|
11 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12 | 12 | # See the License for the specific language governing permissions and
|
13 | 13 | # limitations under the License.
|
14 |
| - |
15 | 14 | from typing import Optional
|
16 |
| -from unittest.mock import Mock |
| 15 | +from unittest.mock import MagicMock, Mock, patch |
17 | 16 |
|
18 | 17 | from synapse.api.constants import EventTypes, JoinRules
|
19 | 18 | from synapse.api.errors import Codes, ResourceLimitError
|
20 | 19 | from synapse.api.filtering import Filtering
|
21 | 20 | from synapse.api.room_versions import RoomVersions
|
22 |
| -from synapse.handlers.sync import SyncConfig |
| 21 | +from synapse.handlers.sync import SyncConfig, SyncResult |
23 | 22 | from synapse.rest import admin
|
24 | 23 | from synapse.rest.client import knock, login, room
|
25 | 24 | from synapse.server import HomeServer
|
26 | 25 | from synapse.types import UserID, create_requester
|
27 | 26 |
|
28 | 27 | import tests.unittest
|
29 | 28 | import tests.utils
|
| 29 | +from tests.test_utils import make_awaitable |
30 | 30 |
|
31 | 31 |
|
32 | 32 | class SyncTestCase(tests.unittest.HomeserverTestCase):
|
@@ -186,6 +186,97 @@ def test_unknown_room_version(self):
|
186 | 186 | self.assertNotIn(invite_room, [r.room_id for r in result.invited])
|
187 | 187 | self.assertNotIn(knock_room, [r.room_id for r in result.knocked])
|
188 | 188 |
|
| 189 | + def test_ban_wins_race_with_join(self): |
| 190 | + """Rooms shouldn't appear under "joined" if a join loses a race to a ban. |
| 191 | +
|
| 192 | + A complicated edge case. Imagine the following scenario: |
| 193 | +
|
| 194 | + * you attempt to join a room |
| 195 | + * racing with that is a ban which comes in over federation, which ends up with |
| 196 | + an earlier stream_ordering than the join. |
| 197 | + * you get a sync response with a sync token which is _after_ the ban, but before |
| 198 | + the join |
| 199 | + * now your join lands; it is a valid event because its `prev_event`s predate the |
| 200 | + ban, but will not make it into current_state_events (because bans win over |
| 201 | + joins in state res, essentially). |
| 202 | + * When we do a sync from the incremental sync, the only event in the timeline |
| 203 | + is your join ... and yet you aren't joined. |
| 204 | +
|
| 205 | + The ban coming in over federation isn't crucial for this behaviour; the key |
| 206 | + requirements are: |
| 207 | + 1. the homeserver generates a join event with prev_events that precede the ban |
| 208 | + (so that it passes the "are you banned" test) |
| 209 | + 2. the join event has a stream_ordering after that of the ban. |
| 210 | +
|
| 211 | + We use monkeypatching to artificially trigger condition (1). |
| 212 | + """ |
| 213 | + # A local user Alice creates a room. |
| 214 | + owner = self.register_user("alice", "password") |
| 215 | + owner_tok = self.login(owner, "password") |
| 216 | + room_id = self.helper.create_room_as(owner, is_public=True, tok=owner_tok) |
| 217 | + |
| 218 | + # Do a sync as Alice to get the latest event in the room. |
| 219 | + alice_sync_result: SyncResult = self.get_success( |
| 220 | + self.sync_handler.wait_for_sync_for_user( |
| 221 | + create_requester(owner), generate_sync_config(owner) |
| 222 | + ) |
| 223 | + ) |
| 224 | + self.assertEqual(len(alice_sync_result.joined), 1) |
| 225 | + self.assertEqual(alice_sync_result.joined[0].room_id, room_id) |
| 226 | + last_room_creation_event_id = ( |
| 227 | + alice_sync_result.joined[0].timeline.events[-1].event_id |
| 228 | + ) |
| 229 | + |
| 230 | + # Eve, a ne'er-do-well, registers. |
| 231 | + eve = self.register_user("eve", "password") |
| 232 | + eve_token = self.login(eve, "password") |
| 233 | + |
| 234 | + # Alice preemptively bans Eve. |
| 235 | + self.helper.ban(room_id, owner, eve, tok=owner_tok) |
| 236 | + |
| 237 | + # Eve syncs. |
| 238 | + eve_requester = create_requester(eve) |
| 239 | + eve_sync_config = generate_sync_config(eve) |
| 240 | + eve_sync_after_ban: SyncResult = self.get_success( |
| 241 | + self.sync_handler.wait_for_sync_for_user(eve_requester, eve_sync_config) |
| 242 | + ) |
| 243 | + |
| 244 | + # Sanity check this sync result. We shouldn't be joined to the room. |
| 245 | + self.assertEqual(eve_sync_after_ban.joined, []) |
| 246 | + |
| 247 | + # Eve tries to join the room. We monkey patch the internal logic which selects |
| 248 | + # the prev_events used when creating the join event, such that the ban does not |
| 249 | + # precede the join. |
| 250 | + mocked_get_prev_events = patch.object( |
| 251 | + self.hs.get_datastore(), |
| 252 | + "get_prev_events_for_room", |
| 253 | + new_callable=MagicMock, |
| 254 | + return_value=make_awaitable([last_room_creation_event_id]), |
| 255 | + ) |
| 256 | + with mocked_get_prev_events: |
| 257 | + self.helper.join(room_id, eve, tok=eve_token) |
| 258 | + |
| 259 | + # Eve makes a second, incremental sync. |
| 260 | + eve_incremental_sync_after_join: SyncResult = self.get_success( |
| 261 | + self.sync_handler.wait_for_sync_for_user( |
| 262 | + eve_requester, |
| 263 | + eve_sync_config, |
| 264 | + since_token=eve_sync_after_ban.next_batch, |
| 265 | + ) |
| 266 | + ) |
| 267 | + # Eve should not see herself as joined to the room. |
| 268 | + self.assertEqual(eve_incremental_sync_after_join.joined, []) |
| 269 | + |
| 270 | + # If we did a third initial sync, we should _still_ see eve is not joined to the room. |
| 271 | + eve_initial_sync_after_join: SyncResult = self.get_success( |
| 272 | + self.sync_handler.wait_for_sync_for_user( |
| 273 | + eve_requester, |
| 274 | + eve_sync_config, |
| 275 | + since_token=None, |
| 276 | + ) |
| 277 | + ) |
| 278 | + self.assertEqual(eve_initial_sync_after_join.joined, []) |
| 279 | + |
189 | 280 |
|
190 | 281 | _request_key = 0
|
191 | 282 |
|
|
0 commit comments