atagen

macltok

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