atagen

macltok

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