@@ -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