atagen

macltok

public
Star0
Watch0
743123167e2946d124e2863611dea27dfa887e7d
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/state.rsRust606 lines20.8 KB
1
mod support;
2
3
use std::time::Duration;
4
5
use macltok::{
6
    ConnectionState, Engine, EngineConfig, EngineError, Event, PackUsage, Retain, RoomMembership,
7
    Termination, ThreadListEntry, ThreadUnread,
8
};
9
use matrix_sdk::{
10
    ThreadingSupport,
11
    ruma::{
12
        EventId, MilliSecondsSinceUnixEpoch, RoomId,
13
        api::client::sync::sync_events::v5,
14
        event_id,
15
        events::{
16
            AnySyncStateEvent, AnyTimelineEvent, Mentions,
17
            receipt::{ReceiptThread, ReceiptType},
18
        },
19
        room_id,
20
        serde::Raw,
21
    },
22
    test_utils::mocks::MatrixMockServer,
23
};
24
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
25
use serde_json::json;
26
use tokio::sync::watch;
27
28
#[tokio::test]
29
async fn rooms_changed_invalidates_room_summaries() {
30
    let server = MatrixMockServer::new().await;
31
    server
32
        .mock_sliding_sync()
33
        .ok(v5::Response::new("1".to_owned()))
34
        .mount()
35
        .await;
36
    let client = server.client_builder().build().await;
37
    let room_id = room_id!("!state:localhost");
38
    server.mock_room_state_encryption().plain().mount().await;
39
    server.sync_joined_room(&client, room_id).await;
40
    let config = EngineConfig::default().with_initial_history_limit(0);
41
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
42
    let running = tokio::spawn(engine.run());
43
44
    support::receive_until(&mut events, |event| matches!(event, Event::RoomsChanged)).await;
45
    let rooms = handle.rooms().await;
46
    let summary = rooms
47
        .iter()
48
        .find(|summary| summary.room_id == room_id)
49
        .expect("synced room was absent from the room summaries");
50
    assert_eq!(summary.membership, RoomMembership::Joined);
51
    assert!(!summary.name.is_empty());
52
53
    support::shut_down(&handle, &mut events, running).await;
54
}
55
56
#[tokio::test]
57
async fn connection_watch_tracks_the_engine_lifecycle() {
58
    let server = MatrixMockServer::new().await;
59
    server
60
        .mock_sliding_sync()
61
        .ok(v5::Response::new("1".to_owned()))
62
        .mount()
63
        .await;
64
    let client = server.client_builder().build().await;
65
    let config = EngineConfig::default().with_initial_history_limit(0);
66
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
67
    let mut connection = handle.connection_state();
68
    let connection_peer = connection.clone();
69
    assert_eq!(*connection.borrow(), ConnectionState::Idle);
70
    assert_eq!(*connection_peer.borrow(), ConnectionState::Idle);
71
72
    let running = tokio::spawn(engine.run());
73
    support::receive_until(&mut events, |event| {
74
        matches!(event, Event::ConnectionChanged(ConnectionState::Connecting))
75
    })
76
    .await;
77
    assert_ne!(*connection.borrow(), ConnectionState::Idle);
78
    support::receive_until(&mut events, |event| {
79
        matches!(event, Event::ConnectionChanged(ConnectionState::Online))
80
    })
81
    .await;
82
    wait_for_connection(&mut connection, ConnectionState::Online).await;
83
    assert_eq!(*connection_peer.borrow(), ConnectionState::Online);
84
85
    support::shut_down(&handle, &mut events, running).await;
86
    wait_for_connection(&mut connection, ConnectionState::Stopped).await;
87
    assert_eq!(*connection_peer.borrow(), ConnectionState::Stopped);
88
}
89
90
#[tokio::test]
91
async fn soft_logout_says_so_and_stops_the_engine() {
92
    let server = MatrixMockServer::new().await;
93
    server
94
        .mock_sliding_sync()
95
        .error_unknown_token(true)
96
        .mount()
97
        .await;
98
    let client = server.client_builder().build().await;
99
    let (engine, _handle, mut events) = Engine::from_client(
100
        client,
101
        EngineConfig::default().with_initial_history_limit(0),
102
    )
103
    .await
104
    .unwrap();
105
    let running = tokio::spawn(engine.run());
106
    support::receive_until(&mut events, |event| {
107
        matches!(
108
            event,
109
            Event::SessionAccessRejected {
110
                soft_logout: Some(_)
111
            }
112
        )
113
    })
114
    .await;
115
    support::receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
116
    let termination = running.await.unwrap().unwrap();
117
    assert!(matches!(termination, Termination::SoftLoggedOut { .. }));
118
    assert_eq!(termination.retain(), Retain::Store);
119
}
120
121
#[tokio::test]
122
async fn closing_the_event_stream_stops_an_idle_engine() {
123
    let server = MatrixMockServer::new().await;
124
    server
125
        .mock_sliding_sync()
126
        .ok(v5::Response::new("1".to_owned()))
127
        .mount()
128
        .await;
129
    let client = server.client_builder().build().await;
130
    let (engine, handle, mut events) = Engine::from_client(
131
        client,
132
        EngineConfig::default().with_initial_history_limit(0),
133
    )
134
    .await
135
    .unwrap();
136
    let running = tokio::spawn(engine.run());
137
    support::receive_until(&mut events, |event| {
138
        matches!(event, Event::ConnectionChanged(ConnectionState::Online))
139
    })
140
    .await;
141
    drop(events);
142
    let result = tokio::time::timeout(Duration::from_secs(2), running)
143
        .await
144
        .expect("engine did not stop after its event receiver closed")
145
        .unwrap();
146
    assert!(matches!(result, Err(EngineError::EventReceiverClosed)));
147
    drop(handle);
148
}
149
150
async fn wait_for_connection(
151
    connection: &mut watch::Receiver<ConnectionState>,
152
    expected: ConnectionState,
153
) {
154
    tokio::time::timeout(Duration::from_secs(2), async {
155
        loop {
156
            if *connection.borrow_and_update() == expected {
157
                return;
158
            }
159
            connection
160
                .changed()
161
                .await
162
                .expect("connection watch closed before the expected state");
163
        }
164
    })
165
    .await
166
    .expect("expected connection state did not arrive");
167
}
168
169
#[tokio::test]
170
async fn image_packs_follow_room_state_and_account_data() {
171
    let server = MatrixMockServer::new().await;
172
    server
173
        .mock_sliding_sync()
174
        .ok(v5::Response::new("1".to_owned()))
175
        .mount()
176
        .await;
177
    let client = server.client_builder().build().await;
178
    let room_id = room_id!("!packs:localhost");
179
    server.mock_room_state_encryption().plain().mount().await;
180
    server.sync_joined_room(&client, room_id).await;
181
    let config = EngineConfig::default().with_initial_history_limit(0);
182
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
183
    let running = tokio::spawn(engine.run());
184
185
    let pack: Raw<AnySyncStateEvent> = serde_json::from_value(json!({
186
        "type": "im.ponies.room_emotes",
187
        "state_key": "cats",
188
        "event_id": "$pack",
189
        "sender": "@example:localhost",
190
        "origin_server_ts": 1,
191
        "content": {
192
            "pack": { "display_name": "cats", "usage": ["emoticon"] },
193
            "images": {
194
                "blep": { "url": "mxc://localhost/blep" },
195
                "broken": { "url": "not an mxc" },
196
                "sticker": { "url": "mxc://localhost/sticker", "usage": ["sticker"] },
197
            },
198
        },
199
    }))
200
    .unwrap();
201
    server
202
        .mock_sync()
203
        .ok_and_run(&client, |builder| {
204
            builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(pack));
205
            builder.add_custom_global_account_data(json!({
206
                "type": "im.ponies.emote_rooms",
207
                "content": { "rooms": { "!packs:localhost": { "cats": {} } } },
208
            }));
209
        })
210
        .await;
211
    support::receive_until(&mut events, |event| {
212
        matches!(event, Event::ImagePacksChanged)
213
    })
214
    .await;
215
216
    let packs = handle.image_packs().await.unwrap();
217
    let offered = packs.for_room(Some(room_id));
218
    assert_eq!(offered.len(), 1, "an enabled room's pack was offered twice");
219
    let pack = offered[0];
220
    assert_eq!(pack.id, "room:!packs:localhost:cats");
221
    assert_eq!(packs.global, vec![pack.clone()]);
222
    let images: Vec<_> = pack
223
        .images
224
        .iter()
225
        .map(|image| {
226
            (
227
                image.shortcode.as_str(),
228
                image.reaction_key.as_str(),
229
                image.usage.clone(),
230
            )
231
        })
232
        .collect();
233
    assert_eq!(
234
        images,
235
        [
236
            ("blep", "mxc://localhost/blep", vec![PackUsage::Emoticon]),
237
            (
238
                "sticker",
239
                "mxc://localhost/sticker",
240
                vec![PackUsage::Sticker]
241
            ),
242
        ]
243
    );
244
245
    support::shut_down(&handle, &mut events, running).await;
246
}
247
248
/// the sdk counts none of this once threads are on, and a threaded receipt never reaches
249
/// room info, so only our own recount notices it
250
#[tokio::test]
251
async fn thread_unreads_follow_threaded_receipts() {
252
    let server = MatrixMockServer::new().await;
253
    server
254
        .mock_sliding_sync()
255
        .ok(v5::Response::new("1".to_owned()))
256
        .mount()
257
        .await;
258
    let client = server
259
        .client_builder()
260
        .on_builder(|builder| {
261
            builder.with_threading_support(ThreadingSupport::Enabled {
262
                with_subscriptions: false,
263
            })
264
        })
265
        .build()
266
        .await;
267
    let room_id = room_id!("!threads:localhost");
268
    let own = client.user_id().unwrap().to_owned();
269
    server.mock_room_state_encryption().plain().mount().await;
270
    client.event_cache().subscribe().unwrap();
271
    // without our own member event there's no push context, so no push actions to count
272
    server
273
        .sync_room(
274
            &client,
275
            JoinedRoomBuilder::new(room_id)
276
                .add_state_event(EventFactory::new().room(room_id).member(&own)),
277
        )
278
        .await;
279
    let config = EngineConfig::default().with_initial_history_limit(0);
280
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
281
    let running = tokio::spawn(engine.run());
282
283
    let (root, first, second) = (
284
        event_id!("$root"),
285
        event_id!("$first"),
286
        event_id!("$second"),
287
    );
288
    let factory = EventFactory::new().room(room_id).sender(&ALICE);
289
    server
290
        .sync_room(
291
            &client,
292
            JoinedRoomBuilder::new(room_id)
293
                .add_timeline_event(factory.text_msg("root").event_id(root))
294
                .add_timeline_event(
295
                    factory
296
                        .text_msg("you there?")
297
                        .in_thread(root, root)
298
                        .mentions(Mentions::with_user_ids([own.clone()]))
299
                        .event_id(first),
300
                )
301
                .add_timeline_event(
302
                    factory
303
                        .text_msg("hello?")
304
                        .in_thread(root, first)
305
                        .event_id(second),
306
                ),
307
        )
308
        .await;
309
    let unread = thread_unread_once(&handle, &mut events, room_id, |unread| {
310
        unread.is_some_and(|unread| unread.messages == 2)
311
    })
312
    .await;
313
    assert_eq!(unread.mentions, 1);
314
    let summary = room(&handle, room_id).await;
315
    assert!(
316
        summary.unread_mentions >= 1,
317
        "the thread mention left the room's count"
318
    );
319
320
    server
321
        .sync_room(
322
            &client,
323
            JoinedRoomBuilder::new(room_id).add_receipt(
324
                factory
325
                    .read_receipts()
326
                    .add(
327
                        first,
328
                        &own,
329
                        ReceiptType::Read,
330
                        ReceiptThread::Thread(root.to_owned()),
331
                    )
332
                    .into_event(),
333
            ),
334
        )
335
        .await;
336
    let unread = thread_unread_once(&handle, &mut events, room_id, |unread| {
337
        unread.is_some_and(|unread| unread.messages == 1)
338
    })
339
    .await;
340
    assert_eq!(unread.mentions, 0);
341
342
    server
343
        .sync_room(
344
            &client,
345
            JoinedRoomBuilder::new(room_id).add_receipt(
346
                factory
347
                    .read_receipts()
348
                    .add(second, &own, ReceiptType::Read, ReceiptThread::Unthreaded)
349
                    .into_event(),
350
            ),
351
        )
352
        .await;
353
    thread_unread_once(&handle, &mut events, room_id, |unread| unread.is_none()).await;
354
355
    support::shut_down(&handle, &mut events, running).await;
356
}
357
358
async fn room(handle: &macltok::ClientHandle, room_id: &RoomId) -> macltok::RoomSummary {
359
    handle
360
        .rooms()
361
        .await
362
        .into_iter()
363
        .find(|summary| summary.room_id == room_id)
364
        .expect("synced room was absent from the room summaries")
365
}
366
367
/// waits out `RoomsChanged` until the root's counts satisfy `done`
368
async fn thread_unread_once(
369
    handle: &macltok::ClientHandle,
370
    events: &mut macltok::EventStream,
371
    room_id: &RoomId,
372
    done: impl Fn(Option<&ThreadUnread>) -> bool,
373
) -> ThreadUnread {
374
    let root = event_id!("$root");
375
    tokio::time::timeout(Duration::from_secs(10), async {
376
        loop {
377
            let summary = room(handle, room_id).await;
378
            let unread = summary.thread_unreads.get(root);
379
            if done(unread) {
380
                return unread.copied().unwrap_or_default();
381
            }
382
            support::receive_until(events, |event| matches!(event, Event::RoomsChanged)).await;
383
        }
384
    })
385
    .await
386
    .expect("thread unreads never settled")
387
}
388
389
/// continuwuity 26.9 bundles a thread's latest reply as bare content, so nothing dates the
390
/// page's count, and the cache handing back replies the server already counted counted them
391
/// again
392
#[tokio::test]
393
async fn a_thread_list_page_keeps_the_servers_count() {
394
    let server = MatrixMockServer::new().await;
395
    server
396
        .mock_sliding_sync()
397
        .ok(v5::Response::new("1".to_owned()))
398
        .mount()
399
        .await;
400
    let client = server
401
        .client_builder()
402
        .on_builder(|builder| {
403
            builder.with_threading_support(ThreadingSupport::Enabled {
404
                with_subscriptions: false,
405
            })
406
        })
407
        .build()
408
        .await;
409
    let room_id = room_id!("!thread-list:localhost");
410
    let own = client.user_id().unwrap().to_owned();
411
    server.mock_room_state_encryption().plain().mount().await;
412
    client.event_cache().subscribe().unwrap();
413
    let factory = EventFactory::new().room(room_id).sender(&ALICE);
414
    let (root, first, second, third) = (
415
        event_id!("$list-root"),
416
        event_id!("$list-first"),
417
        event_id!("$list-second"),
418
        event_id!("$list-third"),
419
    );
420
    let reply = |event_id, previous, body| {
421
        factory
422
            .text_msg(body)
423
            .in_thread(root, previous)
424
            .event_id(event_id)
425
    };
426
    server
427
        .sync_room(
428
            &client,
429
            JoinedRoomBuilder::new(room_id)
430
                .add_state_event(EventFactory::new().room(room_id).member(&own))
431
                .add_timeline_event(factory.text_msg("root").event_id(root))
432
                .add_timeline_event(reply(first, root, "one"))
433
                .add_timeline_event(reply(second, first, "two")),
434
        )
435
        .await;
436
    let listed: Raw<AnyTimelineEvent> = serde_json::from_value(json!({
437
        "type": "m.room.message",
438
        "event_id": root,
439
        "room_id": room_id,
440
        "sender": &*ALICE,
441
        "origin_server_ts": 1,
442
        "content": { "msgtype": "m.text", "body": "root" },
443
        "unsigned": { "m.relations": { "m.thread": {
444
            "latest_event": { "msgtype": "m.text", "body": "two" },
445
            "count": 2,
446
            "current_user_participated": false,
447
        } } },
448
    }))
449
    .unwrap();
450
    server
451
        .mock_room_threads()
452
        .ok(vec![listed], None)
453
        .mount()
454
        .await;
455
    let config = EngineConfig::default().with_initial_history_limit(0);
456
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
457
    let running = tokio::spawn(engine.run());
458
459
    handle.open_thread_list(room_id.to_owned()).await.unwrap();
460
    handle
461
        .paginate_thread_list(room_id.to_owned(), 20)
462
        .await
463
        .unwrap();
464
    tokio::time::sleep(Duration::from_millis(5)).await;
465
    server
466
        .sync_room(
467
            &client,
468
            JoinedRoomBuilder::new(room_id)
469
                .add_timeline_event(reply(first, root, "one"))
470
                .add_timeline_event(reply(second, first, "two"))
471
                .add_timeline_event(
472
                    reply(third, second, "three").server_ts(MilliSecondsSinceUnixEpoch::now()),
473
                ),
474
        )
475
        .await;
476
    let changed = tokio::time::timeout(
477
        Duration::from_secs(10),
478
        support::receive_until(&mut events, |event| {
479
            matches!(event, Event::ThreadListChanged { upserts, .. }
480
                if upserts.iter().any(|entry| entry.summary.latest_event_id.as_deref() == Some(third)))
481
        }),
482
    )
483
    .await
484
    .expect("the new reply never reached the thread list");
485
    let Event::ThreadListChanged { upserts, .. } = changed else {
486
        unreachable!()
487
    };
488
    let entry = upserts.last().unwrap();
489
    assert_eq!(
490
        entry.summary.reply_count, 3,
491
        "replies the server had counted were counted again"
492
    );
493
494
    support::shut_down(&handle, &mut events, running).await;
495
}
496
497
/// with continuwuity's undated latest, the page has to date each thread from the replies the
498
/// cache already holds, or it sorts by the root
499
#[tokio::test]
500
async fn a_thread_list_page_dates_threads_from_cached_replies() {
501
    let server = MatrixMockServer::new().await;
502
    server
503
        .mock_sliding_sync()
504
        .ok(v5::Response::new("1".to_owned()))
505
        .mount()
506
        .await;
507
    let client = server
508
        .client_builder()
509
        .on_builder(|builder| {
510
            builder.with_threading_support(ThreadingSupport::Enabled {
511
                with_subscriptions: false,
512
            })
513
        })
514
        .build()
515
        .await;
516
    let room_id = room_id!("!thread-dates:localhost");
517
    let own = client.user_id().unwrap().to_owned();
518
    server.mock_room_state_encryption().plain().mount().await;
519
    client.event_cache().subscribe().unwrap();
520
    let factory = EventFactory::new().room(room_id).sender(&ALICE);
521
    let (early, first, second, late) = (
522
        event_id!("$dates-early"),
523
        event_id!("$dates-first"),
524
        event_id!("$dates-second"),
525
        event_id!("$dates-late"),
526
    );
527
    server
528
        .sync_room(
529
            &client,
530
            JoinedRoomBuilder::new(room_id)
531
                .add_state_event(EventFactory::new().room(room_id).member(&own))
532
                .add_timeline_event(factory.text_msg("early").event_id(early).server_ts(1_000))
533
                .add_timeline_event(
534
                    factory
535
                        .text_msg("one")
536
                        .in_thread(early, early)
537
                        .event_id(first)
538
                        .server_ts(5_000),
539
                )
540
                .add_timeline_event(factory.text_msg("late").event_id(late).server_ts(7_000))
541
                .add_timeline_event(
542
                    factory
543
                        .text_msg("two")
544
                        .in_thread(early, first)
545
                        .event_id(second)
546
                        .server_ts(9_000),
547
                ),
548
        )
549
        .await;
550
    let listed = |root: &EventId, timestamp: u64, count: u64| -> Raw<AnyTimelineEvent> {
551
        serde_json::from_value(json!({
552
            "type": "m.room.message",
553
            "event_id": root,
554
            "room_id": room_id,
555
            "sender": &*ALICE,
556
            "origin_server_ts": timestamp,
557
            "content": { "msgtype": "m.text", "body": "root" },
558
            "unsigned": { "m.relations": { "m.thread": {
559
                "latest_event": { "msgtype": "m.text", "body": "latest" },
560
                "count": count,
561
                "current_user_participated": false,
562
            } } },
563
        }))
564
        .unwrap()
565
    };
566
    server
567
        .mock_room_threads()
568
        .ok(vec![listed(late, 7_000, 1), listed(early, 1_000, 2)], None)
569
        .mount()
570
        .await;
571
    let config = EngineConfig::default().with_initial_history_limit(0);
572
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
573
    let running = tokio::spawn(engine.run());
574
575
    handle.open_thread_list(room_id.to_owned()).await.unwrap();
576
    handle
577
        .paginate_thread_list(room_id.to_owned(), 20)
578
        .await
579
        .unwrap();
580
    let changed = support::receive_until(&mut events, |event| {
581
        matches!(event, Event::ThreadListChanged { .. })
582
    })
583
    .await;
584
    let Event::ThreadListChanged { upserts, .. } = changed else {
585
        unreachable!()
586
    };
587
    let mut entries = macltok::imbl::Vector::new();
588
    for entry in upserts {
589
        ThreadListEntry::upsert(&mut entries, entry);
590
    }
591
    let order: Vec<_> = entries
592
        .iter()
593
        .map(|entry| (entry.root_event_id.as_str(), entry.latest_activity_ms))
594
        .collect();
595
    assert_eq!(
596
        order,
597
        [(early.as_str(), 9_000), (late.as_str(), 7_000)],
598
        "a thread was dated by its root rather than its newest cached reply"
599
    );
600
    let dated = &entries[0];
601
    assert_eq!(dated.summary.latest_event_id.as_deref(), Some(second));
602
    assert_eq!(dated.summary.latest_event_timestamp_ms, Some(9_000));
603
    assert_eq!(dated.summary.reply_count, 2);
604
605
    support::shut_down(&handle, &mut events, running).await;
606
}