atagen

macltok

public
Star0
Watch0
1fe480c16a3c6db3270dce9e11f0afe67ebc17fc
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/engine.rsRust605 lines21.2 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(|event| support::media_of(event).is_some())
95
    })
96
    .await;
97
    let media_id = event_messages(&media_event)
98
        .into_iter()
99
        .find_map(|event| support::media_of(event).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).into_iter().any(|event| {
168
                        support::message(event)
169
                            .is_some_and(|message| message.body == "first update")
170
                    });
171
                second_updated |= timeline_id.room_id() == second_id
172
                    && event_messages(&event).into_iter().any(|event| {
173
                        support::message(event)
174
                            .is_some_and(|message| message.body == "second update")
175
                    });
176
            }
177
        }
178
    })
179
    .await
180
    .expect("both retained rooms did not update");
181
182
    handle.close_room(first_id.to_owned()).await.unwrap();
183
    server
184
        .sync_room(
185
            &sync_client,
186
            JoinedRoomBuilder::new(second_id)
187
                .add_timeline_event(factory.text_msg("still live").sender(&ALICE)),
188
        )
189
        .await;
190
    receive_until(&mut events, |event| {
191
        matches!(event, Event::TimelineChanged { timeline_id, .. } if timeline_id.room_id() == second_id)
192
            && event_messages(event).into_iter().any(|event| {
193
                support::message(event).is_some_and(|message| message.body == "still live")
194
            })
195
    })
196
    .await;
197
198
    shut_down(&handle, &mut events, running).await;
199
}
200
201
#[tokio::test]
202
async fn resync_replaces_timeline_before_ack_and_preserves_following_diffs() {
203
    let server = MatrixMockServer::new().await;
204
    server
205
        .mock_sliding_sync()
206
        .ok(v5::Response::new("1".to_owned()))
207
        .mount()
208
        .await;
209
    let client = server.client_builder().build().await;
210
    let sync_client = client.clone();
211
    let room_id = room_id!("!resync:localhost");
212
    server.mock_room_state_encryption().plain().mount().await;
213
    server.sync_joined_room(&client, room_id).await;
214
    let config = EngineConfig::default().with_initial_history_limit(0);
215
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
216
    let running = tokio::spawn(engine.run());
217
218
    handle.open_room(room_id.to_owned()).await.unwrap();
219
    receive_until(&mut events, |event| {
220
        matches!(event,
221
            Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id
222
        )
223
    })
224
    .await;
225
    server
226
        .sync_room(
227
            &sync_client,
228
            JoinedRoomBuilder::new(room_id)
229
                .add_timeline_event(EventFactory::new().text_msg("before reset").sender(&ALICE)),
230
        )
231
        .await;
232
    receive_until(&mut events, |event| {
233
        event_messages(event).into_iter().any(|event| {
234
            support::message(event).is_some_and(|message| message.body == "before reset")
235
        })
236
    })
237
    .await;
238
239
    let mut items =
240
        resync_and_confirm_reset_first(&handle, &mut events, room_id.to_owned().into()).await;
241
    assert!(items.iter().any(|item| matches!(item,
242
        TimelineItem::Event(event) if support::message(event).is_some_and(|message| message.body == "before reset")
243
    )));
244
245
    server
246
        .sync_room(
247
            &sync_client,
248
            JoinedRoomBuilder::new(room_id)
249
                .add_timeline_event(EventFactory::new().text_msg("after reset").sender(&ALICE)),
250
        )
251
        .await;
252
    tokio::time::timeout(Duration::from_secs(2), async {
253
        loop {
254
            if let Event::TimelineChanged { timeline_id, diffs } =
255
                events.recv().await.expect("event stream closed")
256
                && timeline_id.room_id() == room_id
257
            {
258
                for diff in diffs {
259
                    diff.try_apply(&mut items)
260
                        .expect("resynced snapshot accepts following diffs");
261
                }
262
                if items.iter().any(|item| {
263
                    matches!(item,
264
                        TimelineItem::Event(event) if support::message(event).is_some_and(|message| message.body == "after reset")
265
                    )
266
                }) {
267
                    break;
268
                }
269
            }
270
        }
271
    })
272
    .await
273
    .expect("resynced timeline did not update");
274
275
    shut_down(&handle, &mut events, running).await;
276
}
277
278
#[tokio::test]
279
async fn closes_and_reopens_a_room_in_command_order() {
280
    let server = MatrixMockServer::new().await;
281
    server
282
        .mock_sliding_sync()
283
        .ok(v5::Response::new("1".to_owned()))
284
        .mount()
285
        .await;
286
    let client = server.client_builder().build().await;
287
    let room_id = room_id!("!reopen:localhost");
288
    server.mock_room_state_encryption().plain().mount().await;
289
    server.sync_joined_room(&client, room_id).await;
290
    let config = EngineConfig::default().with_initial_history_limit(0);
291
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
292
    let running = tokio::spawn(engine.run());
293
294
    handle.open_room(room_id.to_owned()).await.unwrap();
295
    receive_until(
296
        &mut events,
297
        |event| matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id),
298
    )
299
    .await;
300
301
    let mut close = handle
302
        .try_send(Command::Room(RoomCommand::Close(room_id.to_owned())))
303
        .unwrap();
304
    let mut replacement_open = handle
305
        .try_send(Command::Room(RoomCommand::Open(room_id.to_owned())))
306
        .unwrap();
307
    let mut second_close = handle
308
        .try_send(Command::Room(RoomCommand::Close(room_id.to_owned())))
309
        .unwrap();
310
    let mut final_open = handle
311
        .try_send(Command::Room(RoomCommand::Open(room_id.to_owned())))
312
        .unwrap();
313
314
    // biased select drains every buffered reset before it ever reports a ready reply,
315
    // so the engine can race ahead and land both resets before either open is observed
316
    let mut resets = 0;
317
    let mut order = Vec::new();
318
    let (mut close_done, mut replacement_open_done, mut second_close_done, mut final_open_done) =
319
        (false, false, false, false);
320
    tokio::time::timeout(Duration::from_secs(3), async {
321
        while order.len() < 4 {
322
            tokio::select! {
323
                biased;
324
                event = events.recv() => {
325
                    if let Event::TimelineReset { timeline_id, .. } = event.expect("event stream closed")
326
                        && timeline_id.room_id() == room_id
327
                    {
328
                        resets += 1;
329
                        assert!(resets <= 2, "a room generation reset more than once");
330
                    }
331
                }
332
                result = &mut close, if !close_done => {
333
                    result.expect("close failed");
334
                    close_done = true;
335
                    order.push("close");
336
                }
337
                result = &mut replacement_open, if !replacement_open_done => {
338
                    result.expect("replacement open failed");
339
                    replacement_open_done = true;
340
                    order.push("replacement_open");
341
                }
342
                result = &mut second_close, if !second_close_done => {
343
                    result.expect("second close failed");
344
                    second_close_done = true;
345
                    order.push("second_close");
346
                }
347
                result = &mut final_open, if !final_open_done => {
348
                    result.expect("final open failed");
349
                    final_open_done = true;
350
                    order.push("final_open");
351
                }
352
            }
353
        }
354
    })
355
    .await
356
    .expect("room lifecycle commands did not complete");
357
    assert_eq!(
358
        order,
359
        ["close", "replacement_open", "second_close", "final_open"]
360
    );
361
    assert_eq!(resets, 2, "each open should have reset the timeline once");
362
363
    shut_down(&handle, &mut events, running).await;
364
}
365
366
#[tokio::test]
367
async fn thread_command_waits_for_opening_timeline() {
368
    let server = MatrixMockServer::new().await;
369
    server
370
        .mock_sliding_sync()
371
        .ok(v5::Response::new("1".to_owned()))
372
        .mount()
373
        .await;
374
    let client = server.client_builder().build().await;
375
    let room_id = room_id!("!thread:localhost");
376
    let root = event_id!("$thread-root:localhost");
377
    server.mock_room_state_encryption().plain().mount().await;
378
    server.sync_joined_room(&client, room_id).await;
379
    let (engine, handle, mut events) = Engine::from_client(
380
        client,
381
        EngineConfig::default().with_initial_history_limit(0),
382
    )
383
    .await
384
    .unwrap();
385
    let running = tokio::spawn(engine.run());
386
    handle.open_room(room_id.to_owned()).await.unwrap();
387
388
    let mut open = handle
389
        .try_send(Command::Timeline(TimelineCommand::OpenThread {
390
            room_id: room_id.to_owned(),
391
            root_event_id: root.to_owned(),
392
        }))
393
        .unwrap();
394
    let mut send = handle
395
        .try_send(Command::Timeline(TimelineCommand::Send {
396
            timeline_id: TimelineId::thread(room_id.to_owned(), root.to_owned()),
397
            content: MessageContent::text(""),
398
        }))
399
        .unwrap();
400
    let mut open_succeeded = false;
401
    let mut send_done = false;
402
    tokio::time::timeout(Duration::from_secs(3), async {
403
        while !send_done {
404
            tokio::select! {
405
                biased;
406
                result = &mut open, if !open_succeeded => {
407
                    result.expect("thread open failed");
408
                    open_succeeded = true;
409
                }
410
                result = &mut send, if !send_done => {
411
                    assert!(open_succeeded, "thread command completed before opening");
412
                    let error = result.expect_err("empty body send unexpectedly succeeded");
413
                    assert_eq!(error.kind, ErrorKind::EmptyBody);
414
                    send_done = true;
415
                }
416
            }
417
        }
418
    })
419
    .await
420
    .expect("thread commands did not complete");
421
422
    resync_and_confirm_reset_first(
423
        &handle,
424
        &mut events,
425
        TimelineId::thread(room_id.to_owned(), root.to_owned()),
426
    )
427
    .await;
428
429
    shut_down(&handle, &mut events, running).await;
430
}
431
432
#[tokio::test]
433
async fn pagination_reports_and_remembers_start_of_history() {
434
    let server = MatrixMockServer::new().await;
435
    server
436
        .mock_sliding_sync()
437
        .ok(v5::Response::new("1".to_owned()))
438
        .mount()
439
        .await;
440
    let client = server.client_builder().build().await;
441
    let sync_client = client.clone();
442
    let room_id = room_id!("!history:localhost");
443
    server.mock_room_state_encryption().plain().mount().await;
444
    server.sync_joined_room(&client, room_id).await;
445
    let config = EngineConfig::default().with_initial_history_limit(0);
446
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
447
    let running = tokio::spawn(engine.run());
448
449
    handle.open_room(room_id.to_owned()).await.unwrap();
450
    receive_until(
451
        &mut events,
452
        |event| matches!(event, Event::TimelineReset { timeline_id: reset, .. } if reset.room_id() == room_id),
453
    )
454
    .await;
455
456
    server
457
        .sync_room(
458
            &sync_client,
459
            JoinedRoomBuilder::new(room_id)
460
                .add_timeline_event(EventFactory::new().text_msg("latest").sender(&ALICE))
461
                .set_timeline_limited()
462
                .set_timeline_prev_batch("before-latest"),
463
        )
464
        .await;
465
    receive_until(&mut events, |event| {
466
        matches!(event, Event::TimelineChanged { timeline_id: changed, .. } if changed.room_id() == room_id)
467
            && event_messages(event).into_iter().any(|event| {
468
                support::message(event).is_some_and(|message| message.body == "latest")
469
            })
470
    })
471
    .await;
472
473
    server
474
        .mock_room_messages()
475
        .match_from("before-latest")
476
        .ok(RoomMessagesResponseTemplate::default())
477
        .mock_once()
478
        .mount()
479
        .await;
480
    let mut reached_start = false;
481
    for _ in 0..3 {
482
        handle
483
            .paginate(room_id.to_owned().into(), 10)
484
            .await
485
            .unwrap();
486
        receive_history(&mut events, room_id, HistoryState::Loading).await;
487
        match receive_history_terminal(&mut events, room_id).await {
488
            HistoryState::StartReached => {
489
                reached_start = true;
490
                break;
491
            }
492
            HistoryState::Idle => {}
493
            state => panic!("unexpected pagination state {state:?}"),
494
        }
495
    }
496
    assert!(
497
        reached_start,
498
        "pagination did not reach the mocked room start"
499
    );
500
501
    handle
502
        .paginate(room_id.to_owned().into(), 10)
503
        .await
504
        .unwrap();
505
    receive_history(&mut events, room_id, HistoryState::StartReached).await;
506
507
    shut_down(&handle, &mut events, running).await;
508
}
509
510
/// resyncs `timeline_id` and confirms its `TimelineReset` was already on the
511
/// stream by the time the resync command answered, racing the two so we
512
/// don't have to drain the whole stream first
513
async fn resync_and_confirm_reset_first(
514
    handle: &ClientHandle,
515
    events: &mut EventStream,
516
    timeline_id: TimelineId,
517
) -> Vec<TimelineItem> {
518
    let mut pending = handle
519
        .send(Command::Timeline(TimelineCommand::Resync {
520
            timeline_id: timeline_id.clone(),
521
        }))
522
        .await
523
        .unwrap();
524
    let mut reset_items = None;
525
    tokio::time::timeout(Duration::from_secs(3), async {
526
        loop {
527
            tokio::select! {
528
                biased;
529
                event = events.recv() => {
530
                    if let Event::TimelineReset { timeline_id: reset_id, items } = event.expect("event stream closed")
531
                        && reset_id == timeline_id
532
                    {
533
                        reset_items = Some(items);
534
                    }
535
                }
536
                result = &mut pending => {
537
                    result.expect("resync failed");
538
                    break;
539
                }
540
            }
541
        }
542
    })
543
    .await
544
    .expect("resync did not complete");
545
    reset_items.expect("resync completed before its reset")
546
}
547
548
async fn receive_history(
549
    events: &mut EventStream,
550
    room_id: &matrix_sdk::ruma::RoomId,
551
    expected: HistoryState,
552
) {
553
    receive_until(events, |event| {
554
        matches!(event, Event::HistoryChanged { timeline_id: changed, state } if changed.room_id() == room_id && state == &expected)
555
    })
556
    .await;
557
}
558
559
async fn receive_history_terminal(
560
    events: &mut EventStream,
561
    room_id: &matrix_sdk::ruma::RoomId,
562
) -> HistoryState {
563
    match receive_until(events, |event| {
564
        matches!(event, Event::HistoryChanged { timeline_id: changed, state } if changed.room_id() == room_id && !matches!(state, HistoryState::Loading))
565
    })
566
    .await
567
    {
568
        Event::HistoryChanged { state, .. } => state,
569
        _ => unreachable!(),
570
    }
571
}
572
573
async fn wait_for_room_subscriptions(
574
    server: &MatrixMockServer,
575
    room_ids: &[&matrix_sdk::ruma::RoomId],
576
) {
577
    tokio::time::timeout(Duration::from_secs(2), async {
578
        loop {
579
            let found = server
580
                .received_requests()
581
                .await
582
                .unwrap()
583
                .iter()
584
                .filter_map(|request| {
585
                    serde_json::from_slice::<serde_json::Value>(&request.body).ok()
586
                })
587
                .filter_map(|body| {
588
                    body.get("room_subscriptions")
589
                        .and_then(|value| value.as_object())
590
                        .cloned()
591
                })
592
                .any(|subscriptions| {
593
                    room_ids
594
                        .iter()
595
                        .all(|room_id| subscriptions.contains_key(room_id.as_str()))
596
                });
597
            if found {
598
                break;
599
            }
600
            tokio::task::yield_now().await;
601
        }
602
    })
603
    .await
604
    .expect("Sliding Sync did not retain every open room subscription");
605
}