atagen

macltok

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