@@ -241,3 +241,72 @@ func testUnprocessedToDeviceMessagesArentLostOnRestartJS(t *testing.T, tc *TestC
241
241
must .Equal (t , ev .Text , "Kick to make a new room key!" , "event text mismatch" )
242
242
})
243
243
}
244
+
245
+ // Regression test for https://github.com/element-hq/element-web/issues/24680
246
+ //
247
+ // It's important that room keys are sent out ASAP, else the encrypted event may arrive
248
+ // before the keys, causing a temporary unable-to-decrypt error. Clients SHOULD be batching
249
+ // to-device messages, but old implementations batched too low (20 messages per request).
250
+ // This test asserts we batch at least 100 per request.
251
+ //
252
+ // It does this by creating an E2EE room with 100 E2EE users, and forces a key rotation
253
+ // by sending a message with rotation_period_msgs=1. It does not ensure that the room key
254
+ // is correctly sent to all 100 users as that would entail having 100 users running at
255
+ // the same time (think 100 browsers = expensive). Instead, we sequentially spin up 100
256
+ // clients and then close them before doing the test, and assert we send 100 events.
257
+ //
258
+ // In the future, it may be difficult to run this test for 1 user with 100 devices due to
259
+ // HS limits on the number of devices and forced cross-signing.
260
+ func TestToDeviceMessagesAreBatched (t * testing.T ) {
261
+ ForEachClientType (t , func (t * testing.T , clientType api.ClientType ) {
262
+ tc := CreateTestContext (t , clientType )
263
+ roomID := tc .CreateNewEncryptedRoom (t , tc .Alice , EncRoomOptions .RotationPeriodMsgs (1 ), EncRoomOptions .PresetPublicChat ())
264
+ // create 100 users
265
+ for i := 0 ; i < 100 ; i ++ {
266
+ cli := tc .Deployment .Register (t , clientType .HS , helpers.RegistrationOpts {
267
+ LocalpartSuffix : fmt .Sprintf ("bob-%d" , i ),
268
+ Password : "complement-crypto-password" ,
269
+ })
270
+ cli .MustJoinRoom (t , roomID , []string {clientType .HS })
271
+ // this blocks until it has uploaded OTKs/device keys
272
+ clientUnderTest := tc .MustLoginClient (t , cli , tc .AliceClientType )
273
+ clientUnderTest .Close (t )
274
+ }
275
+ waiter := helpers .NewWaiter ()
276
+ tc .WithAliceSyncing (t , func (alice api.Client ) {
277
+ // intercept /sendToDevice and check we are sending 100 messages per request
278
+ tc .Deployment .WithSniffedEndpoint (t , "/sendToDevice" , func (cd deploy.CallbackData ) {
279
+ if cd .Method != "PUT" {
280
+ return
281
+ }
282
+ // format is:
283
+ /*
284
+ {
285
+ "messages": {
286
+ "@alice:example.com": {
287
+ "TLLBEANAAG": {
288
+ "example_content_key": "value"
289
+ }
290
+ }
291
+ }
292
+ }
293
+ */
294
+ usersMap := gjson .GetBytes (cd .RequestBody , "messages" )
295
+ if ! usersMap .Exists () {
296
+ t .Logf ("intercepted PUT /sendToDevice but no messages existed" )
297
+ return
298
+ }
299
+ if len (usersMap .Map ()) != 100 {
300
+ t .Errorf ("PUT /sendToDevice did not batch messages, got %d want 100" , len (usersMap .Map ()))
301
+ t .Logf (usersMap .Raw )
302
+ }
303
+ waiter .Finish ()
304
+ }, func () {
305
+ alice .SendMessage (t , roomID , "this should cause to-device msgs to be sent" )
306
+ time .Sleep (time .Second )
307
+ waiter .Waitf (t , 5 * time .Second , "did not see /sendToDevice" )
308
+ })
309
+ })
310
+
311
+ })
312
+ }
0 commit comments