From 363aea68e2ddb9256ec016660c0c38b7382b8d1f Mon Sep 17 00:00:00 2001 From: Sean Quah Date: Fri, 23 Sep 2022 18:51:51 +0100 Subject: [PATCH] Test device list tracking when we incorrectly believe a user is in the room --- ...federation_room_join_partial_state_test.go | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) diff --git a/tests/federation_room_join_partial_state_test.go b/tests/federation_room_join_partial_state_test.go index d5bfdf0b..49c4b5fe 100644 --- a/tests/federation_room_join_partial_state_test.go +++ b/tests/federation_room_join_partial_state_test.go @@ -2476,6 +2476,265 @@ func TestPartialStateJoin(t *testing.T) { mustSyncUntilDeviceListsHas(t, alice, syncToken, "left", server.UserID("elsie")) mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) }) + + // setupUserIncorrectlyInRoom tricks the homeserver under test into thinking that @elsie is + // in the room when they have really been kicked. Once the partial state join completes, + // @elsie will be discovered to be no longer in the room. + setupUserIncorrectlyInRoom := func( + t *testing.T, deployment *docker.Deployment, alice *client.CSAPI, + server *federation.Server, room *federation.ServerRoom, + ) (syncToken string, psjResult partialStateJoinResult) { + charlie := server.UserID("charlie") + derek := server.UserID("derek") + elsie := server.UserID("elsie") + fred := server.UserID("fred") + + // The room starts with @charlie and @derek in it. + // @charlie makes @fred an admin. + // @charlie makes @derek a moderator. + var powerLevelsContent map[string]interface{} + json.Unmarshal(room.CurrentState("m.room.power_levels", "").Content(), &powerLevelsContent) + powerLevelsContent["users"].(map[string]interface{})[derek] = 50 + powerLevelsContent["users"].(map[string]interface{})[fred] = 100 + room.AddEvent(server.MustCreateEvent(t, room, b.Event{ + Type: "m.room.power_levels", + StateKey: b.Ptr(""), + Sender: charlie, + Content: powerLevelsContent, + })) + + // @fred joins and leaves the room. + fredJoinEvent := createJoinEvent(t, server, room, fred) + room.AddEvent(fredJoinEvent) + fredLeaveEvent := createLeaveEvent(t, server, room, fred) + room.AddEvent(fredLeaveEvent) + + // @alice:hs1 joins the room. + psjResult = beginPartialStateJoin(t, server, room, alice) + + // @elsie joins the room. + joinEvent := createJoinEvent(t, server, room, elsie) + room.AddEvent(joinEvent) + server.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{joinEvent.JSON()}, nil) + syncToken = awaitEventViaSync(t, alice, room.RoomID, joinEvent.EventID(), "") + + // @fred "bans" @derek. + // This is incorrectly accepted, since the homeserver under test does not know whether + // @fred is really in the room. + // This event has to be a ban, rather than a kick, otherwise state resolution can bring + // @derek back into the room and ruin the test setup. + badKickEvent := server.MustCreateEvent(t, room, b.Event{ + Type: "m.room.member", + StateKey: b.Ptr(derek), + Sender: fred, + Content: map[string]interface{}{"membership": "ban"}, + AuthEvents: room.EventIDsOrReferences([]*gomatrixserverlib.Event{ + room.CurrentState("m.room.create", ""), + room.CurrentState("m.room.power_levels", ""), + fredJoinEvent, + }), + }) + room.Timeline = append(room.Timeline, badKickEvent) + room.Depth = badKickEvent.Depth() + room.ForwardExtremities = []string{badKickEvent.EventID()} + server.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{badKickEvent.JSON()}, nil) + syncToken = awaitEventViaSync(t, alice, room.RoomID, badKickEvent.EventID(), syncToken) + + // @derek kicks @elsie. + // This is incorrectly rejected since the homeserver under test incorrectly thinks + // @derek had been kicked from the room. + kickEvent := server.MustCreateEvent(t, room, b.Event{ + Type: "m.room.member", + StateKey: b.Ptr(elsie), + Sender: derek, + Content: map[string]interface{}{"membership": "leave"}, + }) + room.AddEvent(kickEvent) + server.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{kickEvent.JSON()}, nil) + + // Ensure that the kick event has been persisted. + sentinelEvent := psjResult.CreateMessageEvent(t, "charlie", nil) + room.AddEvent(sentinelEvent) + server.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{sentinelEvent.JSON()}, nil) + syncToken = awaitEventViaSync(t, alice, room.RoomID, sentinelEvent.EventID(), syncToken) + + // Check that the last kick was incorrectly rejected. + must.MatchResponse(t, + alice.DoFunc(t, "GET", []string{"_matrix", "client", "r0", "rooms", room.RoomID, "event", kickEvent.EventID()}), + match.HTTPResponse{ + StatusCode: 404, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_NOT_FOUND"), + }, + }, + ) + + return syncToken, psjResult + } + + // test that device lists stop being tracked when it is discovered that a remote user is not + // in a room once a partial state join completes. + t.Run("Device list no longer tracked for user incorrectly believed to be in room", func(t *testing.T) { + alice, server, userDevicesChannel, room, _, cleanup := setupDeviceListCachingTest(t, deployment, "t36alice") + defer cleanup() + + // The room starts with @charlie and @derek in it. + // @charlie leaves the room. + // @t36alice:hs1 joins the room. + // @elsie joins the room. + // @charlie "kicks" @derek, which the homeserver under test incorrectly accepts. + // @derek kicks @elsie, which the homeserver under test incorrectly rejects. + _, psjResult := setupUserIncorrectlyInRoom(t, deployment, alice, server, room) + defer psjResult.Destroy() + // @elsie is now incorrectly believed to be in the room. + + // The homeserver under test incorrectly thinks it is subscribed to @elsie's device list updates. + mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + mustQueryKeysWithoutFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + + // Finish the partial state join. + // The homeserver under test will discover that @elsie was actually not in the room. + psjResult.FinishStateRequest() + awaitPartialStateJoinCompletion(t, room, alice) + + // @elsie's device list ought to no longer be cached. + // `device_lists.left` is not working yet: https://github.com/matrix-org/synapse/issues/13886 + // mustSyncUntilDeviceListsHas(t, alice, syncToken, "left", server.UserID("elsie")) + mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + }) + + // test that cached device lists are flushed when it is discovered that a remote user was + // not in a room the whole time once a partial state join completes. + t.Run("Device list tracking for user incorrectly believed to be in room when they rejoin before partial state join completes", func(t *testing.T) { + // Tracked in https://github.com/matrix-org/synapse/issues/13887. + t.Skip("This edge case is being ignored for now.") + + alice, server, userDevicesChannel, room, _, cleanup := setupDeviceListCachingTest(t, deployment, "t37alice") + defer cleanup() + + // The room starts with @charlie and @derek in it. + // @charlie leaves the room. + // @t37alice:hs1 joins the room. + // @elsie joins the room. + // @charlie "kicks" @derek, which the homeserver under test incorrectly accepts. + // @derek kicks @elsie, which the homeserver under test incorrectly rejects. + syncToken, psjResult := setupUserIncorrectlyInRoom(t, deployment, alice, server, room) + defer psjResult.Destroy() + // @elsie is now incorrectly believed to be in the room. + + // The homeserver under test incorrectly thinks it is subscribed to @elsie's device list updates. + mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + mustQueryKeysWithoutFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + + // @elsie rejoins the room. + joinEvent := createJoinEvent(t, server, room, server.UserID("elsie")) + room.AddEvent(joinEvent) + server.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{joinEvent.JSON()}, nil) + awaitEventViaSync(t, alice, room.RoomID, joinEvent.EventID(), syncToken) + + // @elsie's device list is still cached. + mustQueryKeysWithoutFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + + // Finish the partial state join. + // The homeserver under test will discover that there was a period where @elsie was + // actually not in the room. + psjResult.FinishStateRequest() + awaitPartialStateJoinCompletion(t, room, alice) + + // @elsie's device list ought to have been flushed from the cache. + mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + }) + + // test that device lists stop being tracked when it is discovered that a remote user is not + // in a room once a partial state join completes. + // Similar to a previous test, except @elsie rejoins the room after the partial state join + // completes, so that their device list is being tracked again at the time we test the + // device list cache. + t.Run("Device list tracking for user incorrectly believed to be in room when they rejoin after partial state join completes", func(t *testing.T) { + alice, server, userDevicesChannel, room, _, cleanup := setupDeviceListCachingTest(t, deployment, "t38alice") + defer cleanup() + + // The room starts with @charlie and @derek in it. + // @charlie leaves the room. + // @t38alice:hs1 joins the room. + // @elsie joins the room. + // @charlie "kicks" @derek, which the homeserver under test incorrectly accepts. + // @derek kicks @elsie, which the homeserver under test incorrectly rejects. + syncToken, psjResult := setupUserIncorrectlyInRoom(t, deployment, alice, server, room) + defer psjResult.Destroy() + // @elsie is now incorrectly believed to be in the room. + + // The homeserver under test incorrectly thinks it is subscribed to @elsie's device list updates. + mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + mustQueryKeysWithoutFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + + // Finish the partial state join. + // The homeserver under test will discover that @elsie was actually not in the room. + psjResult.FinishStateRequest() + awaitPartialStateJoinCompletion(t, room, alice) + // `device_lists.left` is not working yet: https://github.com/matrix-org/synapse/issues/13886 + // mustSyncUntilDeviceListsHas(t, alice, syncToken, "left", server.UserID("elsie")) + + // @elsie rejoins the room. + joinEvent := createJoinEvent(t, server, room, server.UserID("elsie")) + room.AddEvent(joinEvent) + server.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{joinEvent.JSON()}, nil) + awaitEventViaSync(t, alice, room.RoomID, joinEvent.EventID(), syncToken) + + // @elsie's device list ought to have been flushed from the cache. + mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + }) + + // test that cached device lists are flushed when it is discovered that a remote user did + // not share a room the whole time once a partial state join completes. + t.Run("Device list tracking for user incorrectly believed to be in room when they join another shared room before partial state join completes", func(t *testing.T) { + // Tracked in https://github.com/matrix-org/synapse/issues/13887. + t.Skip("This edge case is being ignored for now.") + + alice, server, userDevicesChannel, room, _, cleanup := setupDeviceListCachingTest(t, deployment, "t39alice") + defer cleanup() + + // The room starts with @charlie and @derek in it. + // @charlie leaves the room. + // @t39alice:hs1 joins the room. + // @elsie joins the room. + // @charlie "kicks" @derek, which the homeserver under test incorrectly accepts. + // @derek kicks @elsie, which the homeserver under test incorrectly rejects. + syncToken, psjResult := setupUserIncorrectlyInRoom(t, deployment, alice, server, room) + defer psjResult.Destroy() + // @elsie is now incorrectly believed to be in the room. + + // The homeserver under test incorrectly thinks it is subscribed to @elsie's device list updates. + mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + mustQueryKeysWithoutFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + + // @t39alice:hs1 creates a public room. + otherRoomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"}) + + // @elsie joins the room. + // The homeserver under test is now subscribed to @elsie's device list updates. + server.MustJoinRoom(t, deployment, "hs1", otherRoomID, server.UserID("elsie")) + alice.MustSyncUntil(t, + client.SyncReq{ + Since: syncToken, + Filter: buildLazyLoadingSyncFilter(nil), + }, + client.SyncJoinedTo(server.UserID("elsie"), otherRoomID), + ) + + // The cache device list for @elsie is stale, but the homeserver does not know that yet. + mustQueryKeysWithoutFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + + // Finish the partial state join. + // The homeserver under test will discover that @elsie was actually not in the room, and + // so did not share a room the whole time. + psjResult.FinishStateRequest() + awaitPartialStateJoinCompletion(t, room, alice) + + // @elsie's device list ought to be evicted from the cache. + mustSyncUntilDeviceListsHas(t, alice, syncToken, "changed", server.UserID("elsie")) + mustQueryKeysWithFederationRequest(t, alice, userDevicesChannel, server.UserID("elsie")) + }) }) }