Skip to content

Commit 4acae02

Browse files
authored
Add more thorough tests for device list tracking (#459)
Test what happens when local and remote users join and leave rooms. The previous test only covered local users already in the same room. Signed-off-by: Sean Quah <seanq@matrix.org>
1 parent 0091cd2 commit 4acae02

File tree

1 file changed

+318
-48
lines changed

1 file changed

+318
-48
lines changed

tests/csapi/device_lists_test.go

Lines changed: 318 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,331 @@ import (
66

77
"github.com/matrix-org/complement/internal/b"
88
"github.com/matrix-org/complement/internal/client"
9+
"github.com/matrix-org/complement/internal/docker"
10+
"github.com/matrix-org/complement/internal/match"
11+
"github.com/matrix-org/complement/internal/must"
12+
"github.com/matrix-org/complement/runtime"
13+
"maunium.net/go/mautrix/crypto/olm"
914

1015
"github.com/tidwall/gjson"
1116
)
1217

13-
// TestLocalUsersReceiveDeviceListUpdates tests that users on the same
14-
// homeserver receive device list updates from other local users, as
15-
// long as they share a room.
16-
func TestLocalUsersReceiveDeviceListUpdates(t *testing.T) {
17-
// Create a homeserver with two users that share a room
18-
deployment := Deploy(t, b.BlueprintOneToOneRoom)
19-
defer deployment.Destroy(t)
18+
// TestDeviceListUpdates tests various flows and checks that:
19+
// 1. `/sync`'s `device_lists.changed/left` contain the correct user IDs.
20+
// 2. `/keys/query` returns the correct information after device list updates.
21+
func TestDeviceListUpdates(t *testing.T) {
22+
localpartIndex := 0
23+
// generateLocalpart generates a unique localpart based on the given name.
24+
generateLocalpart := func(localpart string) string {
25+
localpartIndex++
26+
return fmt.Sprintf("%s%d", localpart, localpartIndex)
27+
}
2028

21-
// Get a reference to the already logged-in CS API clients for each user
22-
alice := deployment.Client(t, "hs1", "@alice:hs1")
23-
bob := deployment.Client(t, "hs1", "@bob:hs1")
24-
25-
// Deduce alice's device ID
26-
resp := alice.MustDoFunc(
27-
t,
28-
"GET",
29-
[]string{"_matrix", "client", "v3", "account", "whoami"},
30-
)
31-
responseBodyBytes := client.ParseJSON(t, resp)
32-
aliceDeviceID := gjson.GetBytes(responseBodyBytes, "device_id").Str
33-
34-
// Bob performs an initial sync
35-
_, bobNextBatch := bob.MustSync(t, client.SyncReq{})
36-
37-
// Alice then updates their device list by renaming their current device
38-
alice.MustDoFunc(
39-
t,
40-
"PUT",
41-
[]string{"_matrix", "client", "v3", "devices", aliceDeviceID},
42-
client.WithJSONBody(
43-
t,
44-
map[string]interface{}{
45-
"display_name": "A New Device Name",
46-
},
47-
),
48-
)
49-
50-
// Check that Bob received a device list update from Alice
51-
bob.MustSyncUntil(
52-
t,
53-
client.SyncReq{
54-
Since: bobNextBatch,
55-
}, func(clientUserID string, topLevelSyncJSON gjson.Result) error {
56-
// Ensure that Bob sees that Alice has updated their device list
57-
usersWithChangedDeviceListsArray := topLevelSyncJSON.Get("device_lists.changed").Array()
29+
// uploadNewKeys uploads a new set of keys for a given client.
30+
// Returns a check function that can be passed to mustQueryKeys.
31+
uploadNewKeys := func(t *testing.T, user *client.CSAPI) []match.JSON {
32+
t.Helper()
33+
34+
account := olm.NewAccount()
35+
ed25519Key, curve25519Key := account.IdentityKeys()
36+
37+
ed25519KeyID := fmt.Sprintf("ed25519:%s", user.DeviceID)
38+
curve25519KeyID := fmt.Sprintf("curve25519:%s", user.DeviceID)
39+
40+
user.MustDoFunc(t, "POST", []string{"_matrix", "client", "v3", "keys", "upload"},
41+
client.WithJSONBody(t, map[string]interface{}{
42+
"device_keys": map[string]interface{}{
43+
"user_id": user.UserID,
44+
"device_id": user.DeviceID,
45+
"algorithms": []interface{}{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"},
46+
"keys": map[string]interface{}{
47+
ed25519KeyID: ed25519Key.String(),
48+
curve25519KeyID: curve25519Key.String(),
49+
},
50+
},
51+
}),
52+
)
53+
54+
algorithmsPath := fmt.Sprintf("device_keys.%s.%s.algorithms", user.UserID, user.DeviceID)
55+
ed25519Path := fmt.Sprintf("device_keys.%s.%s.keys.%s", user.UserID, user.DeviceID, ed25519KeyID)
56+
curve25519Path := fmt.Sprintf("device_keys.%s.%s.keys.%s", user.UserID, user.DeviceID, curve25519KeyID)
57+
return []match.JSON{
58+
match.JSONKeyEqual(algorithmsPath, []interface{}{"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}),
59+
match.JSONKeyEqual(ed25519Path, ed25519Key.String()),
60+
match.JSONKeyEqual(curve25519Path, curve25519Key.String()),
61+
}
62+
}
63+
64+
// mustQueryKeys checks that /keys/query returns the correct device keys.
65+
// Accepts a check function produced by a prior call to uploadNewKeys.
66+
mustQueryKeys := func(t *testing.T, user *client.CSAPI, userID string, check []match.JSON) {
67+
t.Helper()
68+
69+
res := user.DoFunc(t, "POST", []string{"_matrix", "client", "v3", "keys", "query"},
70+
client.WithJSONBody(t, map[string]interface{}{
71+
"device_keys": map[string]interface{}{
72+
userID: []string{},
73+
},
74+
}),
75+
)
76+
must.MatchResponse(t, res, match.HTTPResponse{
77+
StatusCode: 200,
78+
JSON: check,
79+
})
80+
}
81+
82+
// syncDeviceListsHas checks that `device_lists.changed` or `device_lists.left` contains a given
83+
// user ID.
84+
syncDeviceListsHas := func(section string, expectedUserID string) client.SyncCheckOpt {
85+
jsonPath := fmt.Sprintf("device_lists.%s", section)
86+
return func(clientUserID string, topLevelSyncJSON gjson.Result) error {
87+
usersWithChangedDeviceListsArray := topLevelSyncJSON.Get(jsonPath).Array()
5888
for _, userID := range usersWithChangedDeviceListsArray {
59-
if userID.Str == alice.UserID {
89+
if userID.Str == expectedUserID {
6090
return nil
6191
}
6292
}
63-
return fmt.Errorf("missing device list update for Alice")
64-
},
65-
)
93+
return fmt.Errorf(
94+
"syncDeviceListsHas: %s not found in %s",
95+
expectedUserID,
96+
jsonPath,
97+
)
98+
}
99+
}
100+
101+
// In all of these test scenarios, there are two users: Alice and Bob.
102+
// We only care about what Alice sees.
103+
104+
// testOtherUserJoin tests another user joining a room Alice is already in.
105+
testOtherUserJoin := func(t *testing.T, deployment *docker.Deployment, hsName string, otherHSName string) {
106+
alice := deployment.RegisterUser(t, hsName, generateLocalpart("alice"), "password", false)
107+
bob := deployment.RegisterUser(t, otherHSName, generateLocalpart("bob"), "password", false)
108+
checkBobKeys := uploadNewKeys(t, bob)
109+
110+
roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
111+
112+
// Alice performs an initial sync
113+
_, aliceNextBatch := alice.MustSync(t, client.SyncReq{})
114+
115+
// Bob joins the room
116+
bob.JoinRoom(t, roomID, []string{hsName})
117+
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
118+
119+
// Check that Alice receives a device list update from Bob
120+
alice.MustSyncUntil(
121+
t,
122+
client.SyncReq{Since: aliceNextBatch},
123+
syncDeviceListsHas("changed", bob.UserID),
124+
)
125+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
126+
// Some homeservers (Synapse) may emit another `changed` update after querying keys.
127+
_, aliceNextBatch = alice.MustSync(t, client.SyncReq{TimeoutMillis: "0"})
128+
129+
// Both homeservers think Bob has joined now
130+
// Bob then updates their device list
131+
checkBobKeys = uploadNewKeys(t, bob)
132+
133+
// Check that Alice receives a device list update from Bob
134+
alice.MustSyncUntil(
135+
t,
136+
client.SyncReq{Since: aliceNextBatch},
137+
syncDeviceListsHas("changed", bob.UserID),
138+
)
139+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
140+
}
141+
142+
// testJoin tests Alice joining a room another user is already in.
143+
testJoin := func(
144+
t *testing.T, deployment *docker.Deployment, hsName string, otherHSName string,
145+
) {
146+
alice := deployment.RegisterUser(t, hsName, generateLocalpart("alice"), "password", false)
147+
bob := deployment.RegisterUser(t, otherHSName, generateLocalpart("bob"), "password", false)
148+
checkBobKeys := uploadNewKeys(t, bob)
149+
150+
roomID := bob.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
151+
152+
// Alice performs an initial sync
153+
_, aliceNextBatch := alice.MustSync(t, client.SyncReq{})
154+
155+
// Alice joins the room
156+
alice.JoinRoom(t, roomID, []string{otherHSName})
157+
bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))
158+
159+
// Check that Alice receives a device list update from Bob
160+
alice.MustSyncUntil(
161+
t,
162+
client.SyncReq{Since: aliceNextBatch},
163+
syncDeviceListsHas("changed", bob.UserID),
164+
)
165+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
166+
// Some homeservers (Synapse) may emit another `changed` update after querying keys.
167+
_, aliceNextBatch = alice.MustSync(t, client.SyncReq{TimeoutMillis: "0"})
168+
169+
// Both homeservers think Alice has joined now
170+
// Bob then updates their device list
171+
checkBobKeys = uploadNewKeys(t, bob)
172+
173+
// Check that Alice receives a device list update from Bob
174+
alice.MustSyncUntil(
175+
t,
176+
client.SyncReq{Since: aliceNextBatch},
177+
syncDeviceListsHas("changed", bob.UserID),
178+
)
179+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
180+
}
181+
182+
// testOtherUserLeave tests another user leaving a room Alice is in.
183+
testOtherUserLeave := func(t *testing.T, deployment *docker.Deployment, hsName string, otherHSName string) {
184+
alice := deployment.RegisterUser(t, hsName, generateLocalpart("alice"), "password", false)
185+
bob := deployment.RegisterUser(t, otherHSName, generateLocalpart("bob"), "password", false)
186+
checkBobKeys := uploadNewKeys(t, bob)
187+
188+
roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
189+
190+
// Bob joins the room
191+
bob.JoinRoom(t, roomID, []string{hsName})
192+
bobNextBatch := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
193+
194+
// Alice performs an initial sync
195+
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
196+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
197+
// Some homeservers (Synapse) may emit another `changed` update after querying keys.
198+
_, aliceNextBatch := alice.MustSync(t, client.SyncReq{TimeoutMillis: "0"})
199+
200+
// Bob leaves the room
201+
bob.LeaveRoom(t, roomID)
202+
bob.MustSyncUntil(t, client.SyncReq{Since: bobNextBatch}, client.SyncLeftFrom(bob.UserID, roomID))
203+
204+
// Check that Alice is notified that she will no longer receive updates about Bob's devices
205+
aliceNextBatch = alice.MustSyncUntil(
206+
t,
207+
client.SyncReq{Since: aliceNextBatch},
208+
syncDeviceListsHas("left", bob.UserID),
209+
)
210+
211+
// Both homeservers think Bob has left now
212+
// Bob then updates their device list
213+
// Alice's homeserver is not expected to get the device list update and must not return a
214+
// cached device list for Bob.
215+
checkBobKeys = uploadNewKeys(t, bob)
216+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
217+
218+
// Check that Alice is not notified about Bob's device update
219+
syncResult, _ := alice.MustSync(t, client.SyncReq{Since: aliceNextBatch})
220+
if syncDeviceListsHas("changed", bob.UserID)(alice.UserID, syncResult) == nil {
221+
t.Fatalf("Alice was unexpectedly notified about Bob's device update even though they share no rooms")
222+
}
223+
}
224+
225+
// testLeave tests Alice leaving a room another user is in.
226+
testLeave := func(t *testing.T, deployment *docker.Deployment, hsName string, otherHSName string) {
227+
alice := deployment.RegisterUser(t, hsName, generateLocalpart("alice"), "password", false)
228+
bob := deployment.RegisterUser(t, otherHSName, generateLocalpart("bob"), "password", false)
229+
checkBobKeys := uploadNewKeys(t, bob)
230+
231+
roomID := bob.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
232+
233+
// Alice joins the room
234+
alice.JoinRoom(t, roomID, []string{otherHSName})
235+
bobNextBatch := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))
236+
237+
// Alice performs an initial sync
238+
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(alice.UserID, roomID))
239+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
240+
// Some homeservers (Synapse) may emit another `changed` update after querying keys.
241+
_, aliceNextBatch := alice.MustSync(t, client.SyncReq{TimeoutMillis: "0"})
242+
243+
// Alice leaves the room
244+
alice.LeaveRoom(t, roomID)
245+
bob.MustSyncUntil(t, client.SyncReq{Since: bobNextBatch}, client.SyncLeftFrom(alice.UserID, roomID))
246+
247+
// Check that Alice is notified that she will no longer receive updates about Bob's devices
248+
aliceNextBatch = alice.MustSyncUntil(
249+
t,
250+
client.SyncReq{Since: aliceNextBatch},
251+
syncDeviceListsHas("left", bob.UserID),
252+
)
253+
254+
// Both homeservers think Alice has left now
255+
// Bob then updates their device list
256+
// Alice's homeserver is not expected to get the device list update and must not return a
257+
// cached device list for Bob.
258+
checkBobKeys = uploadNewKeys(t, bob)
259+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
260+
261+
// Check that Alice is not notified about Bob's device update
262+
syncResult, _ := alice.MustSync(t, client.SyncReq{Since: aliceNextBatch})
263+
if syncDeviceListsHas("changed", bob.UserID)(alice.UserID, syncResult) == nil {
264+
t.Fatalf("Alice was unexpectedly notified about Bob's device update even though they share no rooms")
265+
}
266+
}
267+
268+
// testOtherUserRejoin tests another user leaving and rejoining a room Alice is in.
269+
testOtherUserRejoin := func(t *testing.T, deployment *docker.Deployment, hsName string, otherHSName string) {
270+
alice := deployment.RegisterUser(t, hsName, generateLocalpart("alice"), "password", false)
271+
bob := deployment.RegisterUser(t, otherHSName, generateLocalpart("bob"), "password", false)
272+
checkBobKeys := uploadNewKeys(t, bob)
273+
274+
roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
275+
276+
// Bob joins the room
277+
bob.JoinRoom(t, roomID, []string{hsName})
278+
bobNextBatch := bob.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
279+
280+
// Alice performs an initial sync
281+
alice.MustSyncUntil(t, client.SyncReq{}, client.SyncJoinedTo(bob.UserID, roomID))
282+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
283+
// Some homeservers (Synapse) may emit another `changed` update after querying keys.
284+
_, aliceNextBatch := alice.MustSync(t, client.SyncReq{TimeoutMillis: "0"})
285+
286+
// Both homeservers think Bob has joined now
287+
// Bob leaves the room
288+
bob.LeaveRoom(t, roomID)
289+
bobNextBatch = bob.MustSyncUntil(t, client.SyncReq{Since: bobNextBatch}, client.SyncLeftFrom(bob.UserID, roomID))
290+
291+
// Check that Alice is notified that she will no longer receive updates about Bob's devices
292+
alice.MustSyncUntil(
293+
t,
294+
client.SyncReq{Since: aliceNextBatch},
295+
syncDeviceListsHas("left", bob.UserID),
296+
)
297+
298+
// Both homeservers think Bob has left now
299+
// Bob then updates their device list before rejoining the room
300+
// Alice's homeserver is not expected to get the device list update.
301+
checkBobKeys = uploadNewKeys(t, bob)
302+
303+
// Bob rejoins the room
304+
bob.JoinRoom(t, roomID, []string{hsName})
305+
bob.MustSyncUntil(t, client.SyncReq{Since: bobNextBatch}, client.SyncJoinedTo(bob.UserID, roomID))
306+
307+
// Check that Alice is notified that Bob's devices have a change
308+
// Alice's homeserver must not return a cached device list for Bob.
309+
alice.MustSyncUntil(
310+
t,
311+
client.SyncReq{Since: aliceNextBatch},
312+
syncDeviceListsHas("changed", bob.UserID),
313+
)
314+
mustQueryKeys(t, alice, bob.UserID, checkBobKeys)
315+
}
316+
317+
// Create two homeservers
318+
// The users and rooms in the blueprint won't be used.
319+
// Each test creates their own Alice and Bob users.
320+
deployment := Deploy(t, b.BlueprintFederationOneToOneRoom)
321+
defer deployment.Destroy(t)
322+
323+
t.Run("when local user joins a room", func(t *testing.T) { testOtherUserJoin(t, deployment, "hs1", "hs1") })
324+
t.Run("when remote user joins a room", func(t *testing.T) { testOtherUserJoin(t, deployment, "hs1", "hs2") })
325+
t.Run("when joining a room with a local user", func(t *testing.T) { testJoin(t, deployment, "hs1", "hs1") })
326+
t.Run("when joining a room with a remote user", func(t *testing.T) { testJoin(t, deployment, "hs1", "hs2") })
327+
t.Run("when local user leaves a room", func(t *testing.T) { testOtherUserLeave(t, deployment, "hs1", "hs1") })
328+
t.Run("when remote user leaves a room", func(t *testing.T) { testOtherUserLeave(t, deployment, "hs1", "hs2") })
329+
t.Run("when leaving a room with a local user", func(t *testing.T) { testLeave(t, deployment, "hs1", "hs1") })
330+
t.Run("when leaving a room with a remote user", func(t *testing.T) {
331+
runtime.SkipIf(t, runtime.Synapse) // https://github.com/matrix-org/synapse/issues/13650
332+
testLeave(t, deployment, "hs1", "hs2")
333+
})
334+
t.Run("when local user rejoins a room", func(t *testing.T) { testOtherUserRejoin(t, deployment, "hs1", "hs1") })
335+
t.Run("when remote user rejoins a room", func(t *testing.T) { testOtherUserRejoin(t, deployment, "hs1", "hs2") })
66336
}

0 commit comments

Comments
 (0)