atagen

macltok

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