Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster room joins: Add edge case tests for device list tracking #477

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions tests/federation_room_join_partial_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
})
})
}

Expand Down