atagen

macltok

public
Star0
Watch0
7c7832147a2ef1974fca8cc6abea628f157a4649
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/engine.rsRust619 lines21.8 KB
1
mod support;
2
3
use std::time::Duration;
4
5
use macltok::{
6
    ClientHandle, Command, Engine, EngineConfig, ErrorKind, Event, EventStream, HistoryState,
7
    MediaSize, MessageContent, RecentHistoryState, RoomCommand, TimelineCommand, TimelineId,
8
    TimelineItem,
9
};
10
use matrix_sdk::{
11
    ruma::{api::client::sync::sync_events::v5, event_id, mxc_uri, room_id},
12
    test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
13
};
14
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
15
use support::{event_messages, receive_until, shut_down};
16
17
#[tokio::test]
18
async fn opens_a_room_and_sends_messages() {
19
    let server = MatrixMockServer::new().await;
20
    server
21
        .mock_sliding_sync()
22
        .ok(v5::Response::new("1".to_owned()))
23
        .mount()
24
        .await;
25
    let client = server.client_builder().build().await;
26
    let room_id = room_id!("!toolkit:localhost");
27
    server.mock_room_state_encryption().plain().mount().await;
28
    server.sync_joined_room(&client, room_id).await;
29
    server
30
        .mock_room_send()
31
        .body_matches_partial_json(serde_json::json!({ "body": "hello from macltok" }))
32
        .ok(event_id!("$sent:localhost"))
33
        .expect(1)
34
        .mount()
35
        .await;
36
37
    let config = EngineConfig::default().with_initial_history_limit(0);
38
    let sync_client = client.clone();
39
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
40
    let running = tokio::spawn(engine.run());
41
42
    handle.open_room(room_id.to_owned()).await.unwrap();
43
    receive_until(
44
        &mut events,
45
        |event| matches!(event, Event::TimelineReset { timeline_id: reset, .. } if reset.room_id() == room_id),
46
    )
47
    .await;
48
49
    handle
50
        .send_message(
51
            room_id.to_owned().into(),
52
            MessageContent::text("hello from macltok"),
53
        )
54
        .await
55
        .unwrap();
56
    tokio::time::timeout(Duration::from_secs(2), async {
57
        loop {
58
            let sent = server
59
                .received_requests()
60
                .await
61
                .unwrap()
62
                .iter()
63
                .any(|request| {
64
                    request.url.path().contains("/send/m.room.message/")
65
                        && serde_json::from_slice::<serde_json::Value>(&request.body)
66
                            .is_ok_and(|body| body["body"] == "hello from macltok")
67
                });
68
            if sent {
69
                break;
70
            }
71
            tokio::task::yield_now().await;
72
        }
73
    })
74
    .await
75
    .expect("message did not reach the homeserver");
76
77
    server.mock_media_download().ok_image().mount().await;
78
    server.mock_authed_media_download().ok_image().mount().await;
79
    server
80
        .sync_room(
81
            &sync_client,
82
            JoinedRoomBuilder::new(room_id).add_timeline_event(
83
                EventFactory::new()
84
                    .image(
85
                        "image.jpg".into(),
86
                        mxc_uri!("mxc://localhost/image").to_owned(),
87
                    )
88
                    .sender(&ALICE),
89
            ),
90
        )
91
        .await;
92
    let media_event = receive_until(&mut events, |event| {
93
        event_messages(event)
94
            .into_iter()
95
            .any(|event| support::media_of(event).is_some())
96
    })
97
    .await;
98
    let media_id = event_messages(&media_event)
99
        .into_iter()
100
        .find_map(|event| support::media_of(event).map(|media| media.id.clone()))
101
        .unwrap();
102
    let media = handle.media(&media_id, MediaSize::Full).await.unwrap();
103
    assert!(!media.bytes.is_empty());
104
105
    shut_down(&handle, &mut events, running).await;
106
}
107
108
#[tokio::test]
109
async fn keeps_open_room_timelines_independent() {
110
    let server = MatrixMockServer::new().await;
111
    server
112
        .mock_sliding_sync()
113
        .ok(v5::Response::new("1".to_owned()))
114
        .mount()
115
        .await;
116
    let client = server.client_builder().build().await;
117
    let sync_client = client.clone();
118
    let first_id = room_id!("!first:localhost");
119
    let second_id = room_id!("!second:localhost");
120
    server.mock_room_state_encryption().plain().mount().await;
121
    server.sync_joined_room(&client, first_id).await;
122
    server.sync_joined_room(&client, second_id).await;
123
    let config = EngineConfig::default().with_initial_history_limit(0);
124
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
125
    let running = tokio::spawn(engine.run());
126
127
    let (first_open, second_open) = tokio::join!(
128
        handle.open_room(first_id.to_owned()),
129
        handle.open_room(second_id.to_owned())
130
    );
131
    first_open.unwrap();
132
    second_open.unwrap();
133
    receive_until(
134
        &mut events,
135
        |event| matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == first_id),
136
    )
137
    .await;
138
    receive_until(
139
        &mut events,
140
        |event| matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == second_id),
141
    )
142
    .await;
143
    wait_for_room_subscriptions(&server, &[first_id, second_id]).await;
144
145
    let factory = EventFactory::new();
146
    server
147
        .sync_room(
148
            &sync_client,
149
            JoinedRoomBuilder::new(first_id)
150
                .add_timeline_event(factory.text_msg("first update").sender(&ALICE)),
151
        )
152
        .await;
153
    server
154
        .sync_room(
155
            &sync_client,
156
            JoinedRoomBuilder::new(second_id)
157
                .add_timeline_event(factory.text_msg("second update").sender(&ALICE)),
158
        )
159
        .await;
160
161
    let mut first_updated = false;
162
    let mut second_updated = false;
163
    tokio::time::timeout(Duration::from_secs(2), async {
164
        while !(first_updated && second_updated) {
165
            let event = events.recv().await.expect("event stream closed");
166
            if let Event::TimelineChanged { timeline_id, .. } = &event {
167
                first_updated |= timeline_id.room_id() == first_id
168
                    && event_messages(&event).into_iter().any(|event| {
169
                        support::message(event)
170
                            .is_some_and(|message| message.body == "first update")
171
                    });
172
                second_updated |= timeline_id.room_id() == second_id
173
                    && event_messages(&event).into_iter().any(|event| {
174
                        support::message(event)
175
                            .is_some_and(|message| message.body == "second update")
176
                    });
177
            }
178
        }
179
    })
180
    .await
181
    .expect("both retained rooms did not update");
182
183
    handle.close_room(first_id.to_owned()).await.unwrap();
184
    receive_until(&mut events, |event| {
185
        matches!(event, Event::TimelineClosed { timeline_id } if timeline_id.room_id() == first_id)
186
    })
187
    .await;
188
    server
189
        .sync_room(
190
            &sync_client,
191
            JoinedRoomBuilder::new(second_id)
192
                .add_timeline_event(factory.text_msg("still live").sender(&ALICE)),
193
        )
194
        .await;
195
    receive_until(&mut events, |event| {
196
        matches!(event, Event::TimelineChanged { timeline_id, .. } if timeline_id.room_id() == second_id)
197
            && event_messages(event).into_iter().any(|event| {
198
                support::message(event).is_some_and(|message| message.body == "still live")
199
            })
200
    })
201
    .await;
202
203
    shut_down(&handle, &mut events, running).await;
204
}
205
206
#[tokio::test]
207
async fn resync_replaces_timeline_before_ack_and_preserves_following_diffs() {
208
    let server = MatrixMockServer::new().await;
209
    server
210
        .mock_sliding_sync()
211
        .ok(v5::Response::new("1".to_owned()))
212
        .mount()
213
        .await;
214
    let client = server.client_builder().build().await;
215
    let sync_client = client.clone();
216
    let room_id = room_id!("!resync:localhost");
217
    server.mock_room_state_encryption().plain().mount().await;
218
    server.sync_joined_room(&client, room_id).await;
219
    let config = EngineConfig::default().with_initial_history_limit(0);
220
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
221
    let running = tokio::spawn(engine.run());
222
223
    handle.open_room(room_id.to_owned()).await.unwrap();
224
    receive_until(&mut events, |event| {
225
        matches!(event,
226
            Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id
227
        )
228
    })
229
    .await;
230
    server
231
        .sync_room(
232
            &sync_client,
233
            JoinedRoomBuilder::new(room_id)
234
                .add_timeline_event(EventFactory::new().text_msg("before reset").sender(&ALICE)),
235
        )
236
        .await;
237
    receive_until(&mut events, |event| {
238
        event_messages(event).into_iter().any(|event| {
239
            support::message(event).is_some_and(|message| message.body == "before reset")
240
        })
241
    })
242
    .await;
243
244
    let mut items =
245
        resync_and_confirm_reset_first(&handle, &mut events, room_id.to_owned().into()).await;
246
    assert!(items.iter().any(|item| matches!(item,
247
        TimelineItem::Event(event) if support::message(event).is_some_and(|message| message.body == "before reset")
248
    )));
249
250
    server
251
        .sync_room(
252
            &sync_client,
253
            JoinedRoomBuilder::new(room_id)
254
                .add_timeline_event(EventFactory::new().text_msg("after reset").sender(&ALICE)),
255
        )
256
        .await;
257
    tokio::time::timeout(Duration::from_secs(2), async {
258
        loop {
259
            if let Event::TimelineChanged { timeline_id, diffs } =
260
                events.recv().await.expect("event stream closed")
261
                && timeline_id.room_id() == room_id
262
            {
263
                for diff in diffs {
264
                    diff.try_apply(&mut items)
265
                        .expect("resynced snapshot accepts following diffs");
266
                }
267
                if items.iter().any(|item| {
268
                    matches!(item,
269
                        TimelineItem::Event(event) if support::message(event).is_some_and(|message| message.body == "after reset")
270
                    )
271
                }) {
272
                    break;
273
                }
274
            }
275
        }
276
    })
277
    .await
278
    .expect("resynced timeline did not update");
279
280
    shut_down(&handle, &mut events, running).await;
281
}
282
283
#[tokio::test]
284
async fn closes_and_reopens_a_room_in_command_order() {
285
    let server = MatrixMockServer::new().await;
286
    server
287
        .mock_sliding_sync()
288
        .ok(v5::Response::new("1".to_owned()))
289
        .mount()
290
        .await;
291
    let client = server.client_builder().build().await;
292
    let room_id = room_id!("!reopen:localhost");
293
    server.mock_room_state_encryption().plain().mount().await;
294
    server.sync_joined_room(&client, room_id).await;
295
    let config = EngineConfig::default().with_initial_history_limit(0);
296
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
297
    let running = tokio::spawn(engine.run());
298
299
    handle.open_room(room_id.to_owned()).await.unwrap();
300
    receive_until(
301
        &mut events,
302
        |event| matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id),
303
    )
304
    .await;
305
306
    let mut close = handle
307
        .try_send(Command::Room(RoomCommand::Close(room_id.to_owned())))
308
        .unwrap();
309
    let mut replacement_open = handle
310
        .try_send(Command::Room(RoomCommand::Open(room_id.to_owned())))
311
        .unwrap();
312
    let mut second_close = handle
313
        .try_send(Command::Room(RoomCommand::Close(room_id.to_owned())))
314
        .unwrap();
315
    let mut final_open = handle
316
        .try_send(Command::Room(RoomCommand::Open(room_id.to_owned())))
317
        .unwrap();
318
319
    // biased select drains buffered events before it reports ready replies
320
    let mut resets = 0;
321
    let mut recent_active = false;
322
    let mut order = Vec::new();
323
    let (mut close_done, mut replacement_open_done, mut second_close_done, mut final_open_done) =
324
        (false, false, false, false);
325
    tokio::time::timeout(Duration::from_secs(3), async {
326
        while order.len() < 4 {
327
            tokio::select! {
328
                biased;
329
                event = events.recv() => {
330
                    match event.expect("event stream closed") {
331
                        Event::RecentHistoryChanged { room_id: changed, state } if changed == room_id => {
332
                            recent_active = matches!(state, RecentHistoryState::Loading);
333
                        }
334
                        Event::TimelineClosed { timeline_id } if timeline_id.room_id() == room_id => {
335
                            recent_active = false;
336
                        }
337
                        Event::TimelineReset { timeline_id, .. }
338
                            if timeline_id.room_id() == room_id && !recent_active =>
339
                        {
340
                            resets += 1;
341
                            assert!(resets <= 2, "a room generation reset more than once");
342
                        }
343
                        _ => {}
344
                    }
345
                }
346
                result = &mut close, if !close_done => {
347
                    result.expect("close failed");
348
                    close_done = true;
349
                    order.push("close");
350
                }
351
                result = &mut replacement_open, if !replacement_open_done => {
352
                    result.expect("replacement open failed");
353
                    replacement_open_done = true;
354
                    order.push("replacement_open");
355
                }
356
                result = &mut second_close, if !second_close_done => {
357
                    result.expect("second close failed");
358
                    second_close_done = true;
359
                    order.push("second_close");
360
                }
361
                result = &mut final_open, if !final_open_done => {
362
                    result.expect("final open failed");
363
                    final_open_done = true;
364
                    order.push("final_open");
365
                }
366
            }
367
        }
368
    })
369
    .await
370
    .expect("room lifecycle commands did not complete");
371
    assert_eq!(
372
        order,
373
        ["close", "replacement_open", "second_close", "final_open"]
374
    );
375
    assert_eq!(resets, 2, "each open should have reset the timeline once");
376
377
    shut_down(&handle, &mut events, running).await;
378
}
379
380
#[tokio::test]
381
async fn thread_command_waits_for_opening_timeline() {
382
    let server = MatrixMockServer::new().await;
383
    server
384
        .mock_sliding_sync()
385
        .ok(v5::Response::new("1".to_owned()))
386
        .mount()
387
        .await;
388
    let client = server.client_builder().build().await;
389
    let room_id = room_id!("!thread:localhost");
390
    let root = event_id!("$thread-root:localhost");
391
    server.mock_room_state_encryption().plain().mount().await;
392
    server.sync_joined_room(&client, room_id).await;
393
    let (engine, handle, mut events) = Engine::from_client(
394
        client,
395
        EngineConfig::default().with_initial_history_limit(0),
396
    )
397
    .await
398
    .unwrap();
399
    let running = tokio::spawn(engine.run());
400
    handle.open_room(room_id.to_owned()).await.unwrap();
401
402
    let mut open = handle
403
        .try_send(Command::Timeline(TimelineCommand::OpenThread {
404
            room_id: room_id.to_owned(),
405
            root_event_id: root.to_owned(),
406
        }))
407
        .unwrap();
408
    let mut send = handle
409
        .try_send(Command::Timeline(TimelineCommand::Send {
410
            timeline_id: TimelineId::thread(room_id.to_owned(), root.to_owned()),
411
            content: MessageContent::text(""),
412
        }))
413
        .unwrap();
414
    let mut open_succeeded = false;
415
    let mut send_done = false;
416
    tokio::time::timeout(Duration::from_secs(3), async {
417
        while !send_done {
418
            tokio::select! {
419
                biased;
420
                result = &mut open, if !open_succeeded => {
421
                    result.expect("thread open failed");
422
                    open_succeeded = true;
423
                }
424
                result = &mut send, if !send_done => {
425
                    assert!(open_succeeded, "thread command completed before opening");
426
                    let error = result.expect_err("empty body send unexpectedly succeeded");
427
                    assert_eq!(error.kind, ErrorKind::EmptyBody);
428
                    send_done = true;
429
                }
430
            }
431
        }
432
    })
433
    .await
434
    .expect("thread commands did not complete");
435
436
    resync_and_confirm_reset_first(
437
        &handle,
438
        &mut events,
439
        TimelineId::thread(room_id.to_owned(), root.to_owned()),
440
    )
441
    .await;
442
443
    shut_down(&handle, &mut events, running).await;
444
}
445
446
#[tokio::test]
447
async fn pagination_reports_and_remembers_start_of_history() {
448
    let server = MatrixMockServer::new().await;
449
    server
450
        .mock_sliding_sync()
451
        .ok(v5::Response::new("1".to_owned()))
452
        .mount()
453
        .await;
454
    let client = server.client_builder().build().await;
455
    let sync_client = client.clone();
456
    let room_id = room_id!("!history:localhost");
457
    server.mock_room_state_encryption().plain().mount().await;
458
    server.sync_joined_room(&client, room_id).await;
459
    let config = EngineConfig::default().with_initial_history_limit(0);
460
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
461
    let running = tokio::spawn(engine.run());
462
463
    handle.open_room(room_id.to_owned()).await.unwrap();
464
    receive_until(
465
        &mut events,
466
        |event| matches!(event, Event::TimelineReset { timeline_id: reset, .. } if reset.room_id() == room_id),
467
    )
468
    .await;
469
470
    server
471
        .sync_room(
472
            &sync_client,
473
            JoinedRoomBuilder::new(room_id)
474
                .add_timeline_event(EventFactory::new().text_msg("latest").sender(&ALICE))
475
                .set_timeline_limited()
476
                .set_timeline_prev_batch("before-latest"),
477
        )
478
        .await;
479
    receive_until(&mut events, |event| {
480
        matches!(event, Event::TimelineChanged { timeline_id: changed, .. } if changed.room_id() == room_id)
481
            && event_messages(event).into_iter().any(|event| {
482
                support::message(event).is_some_and(|message| message.body == "latest")
483
            })
484
    })
485
    .await;
486
487
    server
488
        .mock_room_messages()
489
        .match_from("before-latest")
490
        .ok(RoomMessagesResponseTemplate::default())
491
        .mock_once()
492
        .mount()
493
        .await;
494
    let mut reached_start = false;
495
    for _ in 0..3 {
496
        handle
497
            .paginate(room_id.to_owned().into(), 10)
498
            .await
499
            .unwrap();
500
        receive_history(&mut events, room_id, HistoryState::Loading).await;
501
        match receive_history_terminal(&mut events, room_id).await {
502
            HistoryState::StartReached => {
503
                reached_start = true;
504
                break;
505
            }
506
            HistoryState::Idle => {}
507
            state => panic!("unexpected pagination state {state:?}"),
508
        }
509
    }
510
    assert!(
511
        reached_start,
512
        "pagination did not reach the mocked room start"
513
    );
514
515
    handle
516
        .paginate(room_id.to_owned().into(), 10)
517
        .await
518
        .unwrap();
519
    receive_history(&mut events, room_id, HistoryState::StartReached).await;
520
521
    shut_down(&handle, &mut events, running).await;
522
}
523
524
/// resyncs `timeline_id` and confirms its `TimelineReset` was already on the
525
/// stream by the time the resync command answered, racing the two so we
526
/// don't have to drain the whole stream first
527
async fn resync_and_confirm_reset_first(
528
    handle: &ClientHandle,
529
    events: &mut EventStream,
530
    timeline_id: TimelineId,
531
) -> macltok::imbl::Vector<TimelineItem> {
532
    let mut pending = handle
533
        .send(Command::Timeline(TimelineCommand::Resync {
534
            timeline_id: timeline_id.clone(),
535
        }))
536
        .await
537
        .unwrap();
538
    let mut reset_items = None;
539
    tokio::time::timeout(Duration::from_secs(3), async {
540
        loop {
541
            tokio::select! {
542
                biased;
543
                event = events.recv() => {
544
                    if let Event::TimelineReset { timeline_id: reset_id, items } = event.expect("event stream closed")
545
                        && reset_id == timeline_id
546
                    {
547
                        reset_items = Some(items);
548
                    }
549
                }
550
                result = &mut pending => {
551
                    result.expect("resync failed");
552
                    break;
553
                }
554
            }
555
        }
556
    })
557
    .await
558
    .expect("resync did not complete");
559
    reset_items.expect("resync completed before its reset")
560
}
561
562
async fn receive_history(
563
    events: &mut EventStream,
564
    room_id: &matrix_sdk::ruma::RoomId,
565
    expected: HistoryState,
566
) {
567
    receive_until(events, |event| {
568
        matches!(event, Event::HistoryChanged { timeline_id: changed, state } if changed.room_id() == room_id && state == &expected)
569
    })
570
    .await;
571
}
572
573
async fn receive_history_terminal(
574
    events: &mut EventStream,
575
    room_id: &matrix_sdk::ruma::RoomId,
576
) -> HistoryState {
577
    match receive_until(events, |event| {
578
        matches!(event, Event::HistoryChanged { timeline_id: changed, state } if changed.room_id() == room_id && !matches!(state, HistoryState::Loading))
579
    })
580
    .await
581
    {
582
        Event::HistoryChanged { state, .. } => state,
583
        _ => unreachable!(),
584
    }
585
}
586
587
async fn wait_for_room_subscriptions(
588
    server: &MatrixMockServer,
589
    room_ids: &[&matrix_sdk::ruma::RoomId],
590
) {
591
    tokio::time::timeout(Duration::from_secs(2), async {
592
        loop {
593
            let found = server
594
                .received_requests()
595
                .await
596
                .unwrap()
597
                .iter()
598
                .filter_map(|request| {
599
                    serde_json::from_slice::<serde_json::Value>(&request.body).ok()
600
                })
601
                .filter_map(|body| {
602
                    body.get("room_subscriptions")
603
                        .and_then(|value| value.as_object())
604
                        .cloned()
605
                })
606
                .any(|subscriptions| {
607
                    room_ids
608
                        .iter()
609
                        .all(|room_id| subscriptions.contains_key(room_id.as_str()))
610
                });
611
            if found {
612
                break;
613
            }
614
            tokio::task::yield_now().await;
615
        }
616
    })
617
    .await
618
    .expect("Sliding Sync did not retain every open room subscription");
619
}