atagen

macltok

public
Star0
Watch0
ae941fa10a10b5ece2098aef89e79951fb7785c3
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/history_repair.rsRust612 lines19.9 KB
1
mod support;
2
3
use std::time::Duration;
4
5
#[cfg(feature = "mirror")]
6
use macltok::Mirror;
7
use macltok::{
8
    ClientHandle, Engine, EngineConfig, EngineError, Event, EventStream, HistoryGap,
9
    HistoryRepairPhase, HistoryRepairReport, HistoryRepairStop, RecentHistoryState, Termination,
10
    TimelineDiff, TimelineId, TimelineItem,
11
};
12
use matrix_sdk::{
13
    Client,
14
    ruma::{EventId, RoomId, api::client::sync::sync_events::v5, event_id, room_id},
15
    test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
16
};
17
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
18
use support::shut_down;
19
use wiremock::ResponseTemplate;
20
21
struct TestRoom {
22
    server: MatrixMockServer,
23
    sync_client: Client,
24
    handle: ClientHandle,
25
    events: EventStream,
26
    running: tokio::task::JoinHandle<Result<Termination, EngineError>>,
27
    /// the open room's timeline, kept current by every helper that reads events
28
    items: imbl::Vector<TimelineItem>,
29
    opened: bool,
30
}
31
32
async fn start_room(room_id: &RoomId) -> TestRoom {
33
    let server = MatrixMockServer::new().await;
34
    server
35
        .mock_sliding_sync()
36
        .ok(v5::Response::new("1".to_owned()))
37
        .mount()
38
        .await;
39
    let client = server.client_builder().build().await;
40
    server.mock_room_state_encryption().plain().mount().await;
41
    server.sync_joined_room(&client, room_id).await;
42
    let sync_client = client.clone();
43
    let config = EngineConfig::default().with_initial_history_limit(0);
44
    let (engine, handle, events) = Engine::from_client(client, config).await.unwrap();
45
    let running = tokio::spawn(engine.run());
46
    TestRoom {
47
        server,
48
        sync_client,
49
        handle,
50
        events,
51
        running,
52
        items: imbl::Vector::new(),
53
        opened: false,
54
    }
55
}
56
57
/// the next event that satisfies `predicate`. only the open itself may reset the room
58
/// timeline, history after it has to arrive as diffs
59
async fn next_until(room: &mut TestRoom, mut predicate: impl FnMut(&Event) -> bool) -> Event {
60
    tokio::time::timeout(Duration::from_secs(5), async {
61
        loop {
62
            let event = room.events.recv().await.expect("event stream closed");
63
            match &event {
64
                Event::TimelineReset {
65
                    timeline_id: TimelineId::Room(_),
66
                    items,
67
                } => {
68
                    assert!(!room.opened, "history reset the room timeline");
69
                    room.opened = true;
70
                    room.items = items.clone();
71
                }
72
                Event::TimelineChanged {
73
                    timeline_id: TimelineId::Room(_),
74
                    diffs,
75
                } => {
76
                    for diff in diffs.clone() {
77
                        assert!(
78
                            !matches!(diff, TimelineDiff::Reset { .. }),
79
                            "history replaced the room timeline"
80
                        );
81
                        diff.try_apply(&mut room.items)
82
                            .expect("diff did not fit the timeline");
83
                    }
84
                }
85
                _ => {}
86
            }
87
            if predicate(&event) {
88
                return event;
89
            }
90
        }
91
    })
92
    .await
93
    .expect("expected event did not arrive")
94
}
95
96
async fn timeline_until(
97
    room: &mut TestRoom,
98
    predicate: impl Fn(&imbl::Vector<TimelineItem>) -> bool,
99
) {
100
    tokio::time::timeout(Duration::from_secs(5), async {
101
        while !predicate(&room.items) {
102
            next_until(room, |_| true).await;
103
        }
104
    })
105
    .await
106
    .expect("the timeline never got there");
107
}
108
109
async fn open_room(room: &mut TestRoom, room_id: &RoomId) {
110
    room.handle.open_room(room_id.to_owned()).await.unwrap();
111
    next_until(room, |event| {
112
        matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)
113
    })
114
    .await;
115
}
116
117
async fn cache_event_before_open(room: &TestRoom, room_id: &RoomId, id: &EventId, body: &str) {
118
    room.server
119
        .sync_room(
120
            &room.sync_client,
121
            JoinedRoomBuilder::new(room_id).add_timeline_event(
122
                EventFactory::new()
123
                    .text_msg(body)
124
                    .event_id(id)
125
                    .sender(&ALICE),
126
            ),
127
        )
128
        .await;
129
}
130
131
fn history_event(
132
    room_id: &RoomId,
133
    id: &EventId,
134
    body: &str,
135
) -> matrix_sdk::ruma::serde::Raw<matrix_sdk::ruma::events::AnyTimelineEvent> {
136
    EventFactory::new()
137
        .room(room_id)
138
        .text_msg(body)
139
        .event_id(id)
140
        .sender(&ALICE)
141
        .into_raw_timeline()
142
}
143
144
async fn wait_for_recent(room: &mut TestRoom, room_id: &RoomId) -> RecentHistoryState {
145
    let event = next_until(room, |event| {
146
        matches!(event, Event::RecentHistoryChanged { room_id: id, state, .. }
147
            if id == room_id && !matches!(state, RecentHistoryState::Loading))
148
    })
149
    .await;
150
    match event {
151
        Event::RecentHistoryChanged { state, .. } => state,
152
        _ => unreachable!(),
153
    }
154
}
155
156
async fn wait_for_settled_fill(room: &mut TestRoom) -> HistoryRepairReport {
157
    match next_until(room, |event| {
158
        matches!(event, Event::HistoryRepairChanged(report)
159
            if report.phase != HistoryRepairPhase::Bridging)
160
    })
161
    .await
162
    {
163
        Event::HistoryRepairChanged(report) => report,
164
        _ => unreachable!(),
165
    }
166
}
167
168
fn has_event(items: &imbl::Vector<TimelineItem>, id: &EventId) -> bool {
169
    items.iter().any(
170
        |item| matches!(item, TimelineItem::Event(event) if event.event_id.as_deref() == Some(id)),
171
    )
172
}
173
174
fn gap_in(items: &imbl::Vector<TimelineItem>) -> Option<HistoryGap> {
175
    items.iter().find_map(|item| match item {
176
        TimelineItem::HistoryGap(gap) => Some(gap.clone()),
177
        _ => None,
178
    })
179
}
180
181
#[tokio::test]
182
async fn automatic_recent_load_is_bounded_and_keeps_cached_history_behind_a_gap() {
183
    let room_id = room_id!("!recent-repair:localhost");
184
    let old_id = event_id!("$old:localhost");
185
    let new_ids = [
186
        event_id!("$fresh-a:localhost"),
187
        event_id!("$fresh-b:localhost"),
188
        event_id!("$fresh-c:localhost"),
189
        event_id!("$fresh-d:localhost"),
190
    ];
191
    let mut room = start_room(room_id).await;
192
    for (index, id) in new_ids.iter().enumerate() {
193
        let token = format!("recent-{index}");
194
        let response = RoomMessagesResponseTemplate::default()
195
            .end_token(token.clone())
196
            .events(vec![history_event(room_id, id, "recent event")]);
197
        if index > 0 {
198
            let previous = format!("recent-{}", index - 1);
199
            room.server
200
                .mock_room_messages()
201
                .match_from(&previous)
202
                .ok(response)
203
                .mock_once()
204
                .mount()
205
                .await;
206
        } else {
207
            room.server
208
                .mock_room_messages()
209
                .ok(response)
210
                .mock_once()
211
                .mount()
212
                .await;
213
        }
214
    }
215
    cache_event_before_open(&room, room_id, old_id, "retained old event").await;
216
    open_room(&mut room, room_id).await;
217
    timeline_until(&mut room, |items| {
218
        gap_in(items).is_some()
219
            && has_event(items, old_id)
220
            && new_ids.iter().all(|id| has_event(items, id))
221
    })
222
    .await;
223
    assert_eq!(
224
        wait_for_recent(&mut room, room_id).await,
225
        RecentHistoryState::Loaded
226
    );
227
    let requests = room.server.received_requests().await.unwrap();
228
    let messages = requests
229
        .iter()
230
        .filter(|request| request.url.path().ends_with("/messages"))
231
        .collect::<Vec<_>>();
232
    assert_eq!(messages.len(), 4);
233
    assert!(!messages[0].url.query_pairs().any(|(key, _)| key == "from"));
234
    shut_down(&room.handle, &mut room.events, room.running).await;
235
}
236
237
#[tokio::test]
238
async fn explicit_recent_start_coalesces_with_the_automatic_load() {
239
    let room_id = room_id!("!recent-coalesce:localhost");
240
    let fresh_id = event_id!("$coalesced:localhost");
241
    let mut room = start_room(room_id).await;
242
    room.server
243
        .mock_room_messages()
244
        .ok(RoomMessagesResponseTemplate::default()
245
            .with_delay(Duration::from_millis(300))
246
            .events(vec![history_event(room_id, fresh_id, "shared result")]))
247
        .mock_once()
248
        .mount()
249
        .await;
250
    open_room(&mut room, room_id).await;
251
    tokio::time::timeout(Duration::from_secs(5), async {
252
        loop {
253
            let requested = room
254
                .server
255
                .received_requests()
256
                .await
257
                .unwrap()
258
                .iter()
259
                .any(|request| request.url.path().ends_with("/messages"));
260
            if requested {
261
                break;
262
            }
263
            tokio::task::yield_now().await;
264
        }
265
    })
266
    .await
267
    .expect("automatic recent request did not start");
268
269
    room.handle
270
        .retry_recent_history(room_id.to_owned())
271
        .await
272
        .unwrap();
273
    assert_eq!(
274
        wait_for_recent(&mut room, room_id).await,
275
        RecentHistoryState::Loaded
276
    );
277
    let requests = room.server.received_requests().await.unwrap();
278
    assert_eq!(
279
        requests
280
            .iter()
281
            .filter(|request| request.url.path().ends_with("/messages"))
282
            .count(),
283
        1
284
    );
285
    shut_down(&room.handle, &mut room.events, room.running).await;
286
}
287
288
#[cfg(feature = "mirror")]
289
#[tokio::test]
290
async fn fill_gap_returns_after_the_mirror_contains_the_committed_rows() {
291
    let server = MatrixMockServer::new().await;
292
    server
293
        .mock_sliding_sync()
294
        .ok(v5::Response::new("1".to_owned()))
295
        .mount()
296
        .await;
297
    let client = server.client_builder().build().await;
298
    let room_id = room_id!("!fill-gap-mirror:localhost");
299
    server.mock_room_state_encryption().plain().mount().await;
300
    server.sync_joined_room(&client, room_id).await;
301
    let sync_client = client.clone();
302
    let old_id = event_id!("$gap-old:localhost");
303
    let fresh_id = event_id!("$gap-fresh:localhost");
304
    let middle_id = event_id!("$gap-middle:localhost");
305
    let (engine, handle, events) = Engine::from_client(
306
        client,
307
        EngineConfig::default().with_initial_history_limit(0),
308
    )
309
    .await
310
    .unwrap();
311
    let running = tokio::spawn(engine.run());
312
    let (mirror, mirror_handle) = Mirror::new(handle.clone(), events);
313
    let mirror_running = tokio::spawn(mirror.run());
314
    server
315
        .mock_room_messages()
316
        .ok(RoomMessagesResponseTemplate::default()
317
            .end_token("recent-gap")
318
            .events(vec![history_event(room_id, fresh_id, "new window")]))
319
        .mock_once()
320
        .mount()
321
        .await;
322
    server
323
        .mock_room_messages()
324
        .match_from("recent-gap")
325
        .ok(RoomMessagesResponseTemplate::default().end_token("recent-gap"))
326
        .mock_once()
327
        .mount()
328
        .await;
329
    server
330
        .sync_room(
331
            &sync_client,
332
            JoinedRoomBuilder::new(room_id).add_timeline_event(
333
                EventFactory::new()
334
                    .text_msg("old retained")
335
                    .event_id(old_id)
336
                    .sender(&ALICE),
337
            ),
338
        )
339
        .await;
340
    handle.open_room(room_id.to_owned()).await.unwrap();
341
    let timeline_id = TimelineId::from(room_id.to_owned());
342
    let gap = tokio::time::timeout(Duration::from_secs(5), async {
343
        loop {
344
            if let Some(gap) = mirror_handle
345
                .snapshot()
346
                .timelines
347
                .get(&timeline_id)
348
                .and_then(|timeline| gap_in(&timeline.items))
349
            {
350
                break gap;
351
            }
352
            tokio::task::yield_now().await;
353
        }
354
    })
355
    .await
356
    .expect("the recent history gap did not reach the mirror");
357
    tokio::time::timeout(Duration::from_secs(5), async {
358
        loop {
359
            if mirror_handle.snapshot().recent_history.get(room_id)
360
                == Some(&RecentHistoryState::Loaded)
361
            {
362
                break;
363
            }
364
            tokio::task::yield_now().await;
365
        }
366
    })
367
    .await
368
    .expect("automatic recent loading did not finish");
369
    server
370
        .mock_room_messages()
371
        .match_from("recent-gap")
372
        .ok(RoomMessagesResponseTemplate::default()
373
            .with_delay(Duration::from_millis(300))
374
            .events(vec![
375
                history_event(room_id, middle_id, "middle page"),
376
                history_event(room_id, old_id, "old retained overlap"),
377
            ]))
378
        .mock_once()
379
        .mount()
380
        .await;
381
382
    let fill_handle = handle.clone();
383
    let target_gap_id = gap.id.clone();
384
    let fill_gap_id = target_gap_id.clone();
385
    let fill = tokio::spawn(async move {
386
        fill_handle
387
            .fill_history_gap(room_id.to_owned(), fill_gap_id)
388
            .await
389
    });
390
    tokio::time::timeout(Duration::from_secs(5), async {
391
        loop {
392
            let snapshot = mirror_handle.snapshot();
393
            let active_gap = snapshot.repairs.get(room_id).is_some_and(|report| {
394
                report.gap_id == target_gap_id && report.phase == HistoryRepairPhase::Bridging
395
            });
396
            if active_gap {
397
                break;
398
            }
399
            tokio::task::yield_now().await;
400
        }
401
    })
402
    .await
403
    .expect("the active repair report did not identify its target gap");
404
    fill.await.unwrap().unwrap();
405
    let snapshot = mirror_handle.snapshot();
406
    let timeline = snapshot
407
        .timelines
408
        .get(&timeline_id)
409
        .expect("the room timeline disappeared");
410
    let event_ids = timeline
411
        .items
412
        .iter()
413
        .filter_map(|item| match item {
414
            TimelineItem::Event(event) => event.event_id.as_deref(),
415
            _ => None,
416
        })
417
        .collect::<Vec<_>>();
418
    assert!(event_ids.contains(&fresh_id));
419
    assert!(event_ids.contains(&middle_id));
420
    assert!(event_ids.contains(&old_id));
421
422
    handle.shutdown().await.unwrap();
423
    assert_eq!(running.await.unwrap().unwrap(), Termination::ShutDown);
424
    mirror_running.await.unwrap();
425
}
426
427
#[tokio::test]
428
async fn cancelling_a_gap_fill_keeps_committed_progress_for_retry() {
429
    let room_id = room_id!("!cancel-gap-fill:localhost");
430
    let old_id = event_id!("$cancel-gap-old:localhost");
431
    let fresh_id = event_id!("$cancel-gap-fresh:localhost");
432
    let middle_id = event_id!("$cancel-gap-middle:localhost");
433
    let mut room = start_room(room_id).await;
434
    room.server
435
        .mock_room_messages()
436
        .ok(RoomMessagesResponseTemplate::default()
437
            .end_token("gap-cursor-1")
438
            .events(vec![history_event(room_id, fresh_id, "recent")]))
439
        .mock_once()
440
        .mount()
441
        .await;
442
    room.server
443
        .mock_room_messages()
444
        .match_from("gap-cursor-1")
445
        .ok(RoomMessagesResponseTemplate::default().end_token("gap-cursor-1"))
446
        .mock_once()
447
        .mount()
448
        .await;
449
    cache_event_before_open(&room, room_id, old_id, "old cached").await;
450
    open_room(&mut room, room_id).await;
451
    timeline_until(&mut room, |items| gap_in(items).is_some()).await;
452
    let gap = gap_in(&room.items).unwrap();
453
    assert_eq!(
454
        wait_for_recent(&mut room, room_id).await,
455
        RecentHistoryState::Loaded
456
    );
457
    room.server
458
        .mock_room_messages()
459
        .match_from("gap-cursor-1")
460
        .ok(RoomMessagesResponseTemplate::default()
461
            .end_token("gap-cursor-2")
462
            .events(vec![history_event(
463
                room_id,
464
                middle_id,
465
                "committed progress",
466
            )]))
467
        .mock_once()
468
        .mount()
469
        .await;
470
    room.server
471
        .mock_room_messages()
472
        .match_from("gap-cursor-2")
473
        .ok(RoomMessagesResponseTemplate::default()
474
            .with_delay(Duration::from_secs(5))
475
            .events(vec![history_event(room_id, old_id, "old overlap")]))
476
        .mock_once()
477
        .mount()
478
        .await;
479
    let fill_handle = room.handle.clone();
480
    let gap_id = gap.id.clone();
481
    let fill = tokio::spawn(async move {
482
        fill_handle
483
            .fill_history_gap(room_id.to_owned(), gap_id)
484
            .await
485
    });
486
    tokio::time::timeout(Duration::from_secs(5), async {
487
        loop {
488
            let requested = room
489
                .server
490
                .received_requests()
491
                .await
492
                .unwrap()
493
                .iter()
494
                .any(|request| {
495
                    request.url.path().ends_with("/messages")
496
                        && request
497
                            .url
498
                            .query_pairs()
499
                            .any(|(key, value)| key == "from" && value == "gap-cursor-2")
500
                });
501
            if requested {
502
                break;
503
            }
504
            tokio::task::yield_now().await;
505
        }
506
    })
507
    .await
508
    .expect("the second gap page did not start");
509
    room.handle
510
        .cancel_history_repair(room_id.to_owned())
511
        .await
512
        .unwrap();
513
    let error = fill.await.unwrap().unwrap_err();
514
    assert_eq!(error.kind, macltok::ErrorKind::Interrupted);
515
    let report = wait_for_settled_fill(&mut room).await;
516
    assert_eq!(report.stop, Some(HistoryRepairStop::Cancelled));
517
    assert!(report.failure.is_none());
518
519
    room.server
520
        .mock_room_messages()
521
        .match_from("gap-cursor-2")
522
        .ok(
523
            RoomMessagesResponseTemplate::default().events(vec![history_event(
524
                room_id,
525
                old_id,
526
                "old overlap",
527
            )]),
528
        )
529
        .mock_once()
530
        .mount()
531
        .await;
532
    room.handle
533
        .fill_history_gap(room_id.to_owned(), gap.id)
534
        .await
535
        .unwrap();
536
    timeline_until(&mut room, |items| {
537
        [fresh_id, middle_id, old_id]
538
            .iter()
539
            .all(|id| has_event(items, id))
540
            && gap_in(items).is_none()
541
    })
542
    .await;
543
    shut_down(&room.handle, &mut room.events, room.running).await;
544
}
545
546
#[tokio::test]
547
async fn failed_automatic_recent_load_is_public_and_keeps_the_cached_timeline() {
548
    let room_id = room_id!("!failed-recent:localhost");
549
    let old_id = event_id!("$failed-old:localhost");
550
    let mut room = start_room(room_id).await;
551
    room.server
552
        .mock_room_messages()
553
        .respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({
554
            "errcode": "M_FORBIDDEN",
555
            "error": "history is unavailable",
556
        })))
557
        .mock_once()
558
        .mount()
559
        .await;
560
    cache_event_before_open(&room, room_id, old_id, "kept event").await;
561
    open_room(&mut room, room_id).await;
562
    let state = wait_for_recent(&mut room, room_id).await;
563
    assert!(matches!(state, RecentHistoryState::Failed(_)));
564
    shut_down(&room.handle, &mut room.events, room.running).await;
565
}
566
567
#[tokio::test]
568
async fn closing_during_a_recent_load_settles_idle_not_failed() {
569
    let room_id = room_id!("!close-recent:localhost");
570
    let mut room = start_room(room_id).await;
571
    room.server
572
        .mock_room_messages()
573
        .ok(RoomMessagesResponseTemplate::default()
574
            .with_delay(Duration::from_secs(5))
575
            .events(vec![history_event(
576
                room_id,
577
                event_id!("$never:localhost"),
578
                "too late",
579
            )]))
580
        .mock_once()
581
        .mount()
582
        .await;
583
    open_room(&mut room, room_id).await;
584
    tokio::time::timeout(Duration::from_secs(5), async {
585
        while !room
586
            .server
587
            .received_requests()
588
            .await
589
            .unwrap()
590
            .iter()
591
            .any(|request| request.url.path().ends_with("/messages"))
592
        {
593
            tokio::task::yield_now().await;
594
        }
595
    })
596
    .await
597
    .expect("the recent load never asked for messages");
598
    room.handle.close_room(room_id.to_owned()).await.unwrap();
599
    let mut states = Vec::new();
600
    next_until(&mut room, |event| {
601
        if let Event::RecentHistoryChanged { state, .. } = event {
602
            states.push(state.clone());
603
        }
604
        matches!(event, Event::TimelineClosed { timeline_id } if timeline_id.room_id() == room_id)
605
    })
606
    .await;
607
    assert_eq!(
608
        states,
609
        [RecentHistoryState::Loading, RecentHistoryState::Idle]
610
    );
611
    shut_down(&room.handle, &mut room.events, room.running).await;
612
}