atagen

macltok

public
Star0
Watch0
2656c1d26d6b12a7d19e2c8902c8c88ffe20f982
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/history_repair.rsRust611 lines20.0 KB
1
mod support;
2
3
use std::time::Duration;
4
5
use macltok::{
6
    ClientHandle, Engine, EngineConfig, EngineError, Event, EventStream, HistoryContinuity,
7
    HistoryRepairBudget, HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairStop,
8
    HistoryRepairStrategy, Termination, TimelineItem,
9
};
10
#[cfg(feature = "mirror")]
11
use macltok::{Mirror, TimelineId};
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::{event_messages, receive_until, 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
}
28
29
async fn open_room(room_id: &RoomId) -> TestRoom {
30
    let server = MatrixMockServer::new().await;
31
    server
32
        .mock_sliding_sync()
33
        .ok(v5::Response::new("1".to_owned()))
34
        .mount()
35
        .await;
36
    let client = server.client_builder().build().await;
37
    server.mock_room_state_encryption().plain().mount().await;
38
    server.sync_joined_room(&client, room_id).await;
39
    let sync_client = client.clone();
40
    let config = EngineConfig::default().with_initial_history_limit(0);
41
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
42
    let running = tokio::spawn(engine.run());
43
44
    handle.open_room(room_id.to_owned()).await.unwrap();
45
    receive_until(&mut events, |event| {
46
        matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)
47
    })
48
    .await;
49
50
    TestRoom {
51
        server,
52
        sync_client,
53
        handle,
54
        events,
55
        running,
56
    }
57
}
58
59
async fn sync_cached_event(room: &mut 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
    receive_until(&mut room.events, |event| {
72
        event_messages(event)
73
            .into_iter()
74
            .any(|event| event.event_id.as_deref() == Some(id))
75
    })
76
    .await;
77
}
78
79
fn history_event(
80
    room_id: &RoomId,
81
    id: &EventId,
82
    body: &str,
83
) -> matrix_sdk::ruma::serde::Raw<matrix_sdk::ruma::events::AnyTimelineEvent> {
84
    EventFactory::new()
85
        .room(room_id)
86
        .text_msg(body)
87
        .event_id(id)
88
        .sender(&ALICE)
89
        .into_raw_timeline()
90
}
91
92
fn report_budget(max_pages: u16) -> HistoryRepairBudget {
93
    HistoryRepairBudget {
94
        max_pages,
95
        ..HistoryRepairBudget::default()
96
    }
97
}
98
99
#[tokio::test]
100
async fn recent_repair_starts_without_a_cursor_and_keeps_its_end_for_pagination() {
101
    let room_id = room_id!("!recent-repair:localhost");
102
    let stale_id = event_id!("$stale:localhost");
103
    let fresh_id = event_id!("$fresh:localhost");
104
    let older_id = event_id!("$older:localhost");
105
    let mut room = open_room(room_id).await;
106
    sync_cached_event(&mut room, room_id, stale_id, "cached before repair").await;
107
108
    room.server
109
        .mock_room_messages()
110
        .ok(RoomMessagesResponseTemplate::default()
111
            .end_token("recent-prev")
112
            .events(vec![history_event(room_id, fresh_id, "fresh page")]))
113
        .mock_once()
114
        .mount()
115
        .await;
116
    let report = room
117
        .handle
118
        .history_repair(
119
            room_id.to_owned(),
120
            Some(HistoryRepairPolicy::StartRecent),
121
            HistoryRepairBudget::default(),
122
        )
123
        .await
124
        .unwrap();
125
    assert_eq!(report.strategy, HistoryRepairStrategy::Recent);
126
    assert_eq!(report.phase, HistoryRepairPhase::Ready);
127
    assert!(report.recent_available);
128
129
    let reset = receive_until(&mut room.events, |event| {
130
        matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)
131
            && event_messages(event)
132
                .into_iter()
133
                .any(|event| event.event_id.as_deref() == Some(fresh_id))
134
    })
135
    .await;
136
    let reset_ids = event_messages(&reset)
137
        .into_iter()
138
        .filter_map(|event| event.event_id.as_ref().map(ToString::to_string))
139
        .collect::<Vec<_>>();
140
    assert_eq!(reset_ids, vec![fresh_id.to_string()]);
141
142
    let requests = room.server.received_requests().await.unwrap();
143
    let messages = requests
144
        .iter()
145
        .filter(|request| request.url.path().ends_with("/messages"))
146
        .collect::<Vec<_>>();
147
    assert_eq!(messages.len(), 1);
148
    assert!(!messages[0].url.query_pairs().any(|(key, _)| key == "from"));
149
150
    room.server
151
        .mock_room_messages()
152
        .match_from("recent-prev")
153
        .ok(
154
            RoomMessagesResponseTemplate::default().events(vec![history_event(
155
                room_id,
156
                older_id,
157
                "older page",
158
            )]),
159
        )
160
        .mock_once()
161
        .mount()
162
        .await;
163
    room.handle
164
        .paginate(room_id.to_owned().into(), 10)
165
        .await
166
        .unwrap();
167
    receive_until(&mut room.events, |event| {
168
        event_messages(event).into_iter().any(|event| {
169
            support::message(event).is_some_and(|message| message.body == "older page")
170
        })
171
    })
172
    .await;
173
174
    shut_down(&room.handle, &mut room.events, room.running).await;
175
}
176
177
#[cfg(feature = "mirror")]
178
#[tokio::test]
179
async fn repair_ack_waits_until_the_mirror_has_applied_the_reset_and_report() {
180
    let server = MatrixMockServer::new().await;
181
    server
182
        .mock_sliding_sync()
183
        .ok(v5::Response::new("1".to_owned()))
184
        .mount()
185
        .await;
186
    let client = server.client_builder().build().await;
187
    let room_id = room_id!("!repair-mirror:localhost");
188
    server.mock_room_state_encryption().plain().mount().await;
189
    server.sync_joined_room(&client, room_id).await;
190
    let sync_client = client.clone();
191
    let (engine, handle, events) = Engine::from_client(
192
        client,
193
        EngineConfig::default().with_initial_history_limit(0),
194
    )
195
    .await
196
    .unwrap();
197
    let running = tokio::spawn(engine.run());
198
    let (mirror, mirror_handle) = Mirror::new(handle.clone(), events);
199
    let mirror_running = tokio::spawn(mirror.run());
200
201
    handle.open_room(room_id.to_owned()).await.unwrap();
202
    let stale_id = event_id!("$mirror-stale:localhost");
203
    server
204
        .sync_room(
205
            &sync_client,
206
            JoinedRoomBuilder::new(room_id).add_timeline_event(
207
                EventFactory::new()
208
                    .text_msg("cached event")
209
                    .event_id(stale_id)
210
                    .sender(&ALICE),
211
            ),
212
        )
213
        .await;
214
    let timeline_id = TimelineId::from(room_id.to_owned());
215
    tokio::time::timeout(Duration::from_secs(2), async {
216
        loop {
217
            let has_cached_event = mirror_handle
218
                .snapshot()
219
                .timelines
220
                .get(&timeline_id)
221
                .is_some_and(|timeline| {
222
                    timeline.items.iter().any(|item| {
223
                        matches!(item, TimelineItem::Event(event)
224
                            if event.event_id.as_deref() == Some(stale_id))
225
                    })
226
                });
227
            if has_cached_event {
228
                break;
229
            }
230
            tokio::task::yield_now().await;
231
        }
232
    })
233
    .await
234
    .expect("the cached timeline event did not reach the mirror");
235
236
    let fresh_id = event_id!("$mirror-fresh:localhost");
237
    server
238
        .mock_room_messages()
239
        .ok(
240
            RoomMessagesResponseTemplate::default().events(vec![history_event(
241
                room_id,
242
                fresh_id,
243
                "fresh event",
244
            )]),
245
        )
246
        .mock_once()
247
        .mount()
248
        .await;
249
    let report = handle
250
        .history_repair(
251
            room_id.to_owned(),
252
            Some(HistoryRepairPolicy::StartRecent),
253
            HistoryRepairBudget::default(),
254
        )
255
        .await
256
        .unwrap();
257
258
    let state = mirror_handle.snapshot();
259
    assert_eq!(state.repairs.get(room_id), Some(&report));
260
    let timeline = state
261
        .timelines
262
        .get(&timeline_id)
263
        .expect("the repaired timeline was not applied before acknowledgement");
264
    let ids = timeline
265
        .items
266
        .iter()
267
        .filter_map(|item| match item {
268
            TimelineItem::Event(event) => event.event_id.as_ref().map(ToString::to_string),
269
            _ => None,
270
        })
271
        .collect::<Vec<_>>();
272
    assert_eq!(ids, vec![fresh_id.to_string()]);
273
274
    handle.shutdown().await.unwrap();
275
    assert_eq!(running.await.unwrap().unwrap(), Termination::ShutDown);
276
    mirror_running.await.unwrap();
277
}
278
279
#[tokio::test]
280
async fn failed_recent_fetch_leaves_the_existing_timeline_in_place() {
281
    let room_id = room_id!("!failed-repair:localhost");
282
    let stale_id = event_id!("$kept:localhost");
283
    let mut room = open_room(room_id).await;
284
    sync_cached_event(&mut room, room_id, stale_id, "keep this event").await;
285
286
    room.server
287
        .mock_room_messages()
288
        .respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({
289
            "errcode": "M_FORBIDDEN",
290
            "error": "history is unavailable",
291
        })))
292
        .mock_once()
293
        .mount()
294
        .await;
295
    let report = room
296
        .handle
297
        .history_repair(
298
            room_id.to_owned(),
299
            Some(HistoryRepairPolicy::StartRecent),
300
            HistoryRepairBudget::default(),
301
        )
302
        .await
303
        .unwrap();
304
    assert_eq!(report.phase, HistoryRepairPhase::Paused);
305
    assert!(report.failure.is_some());
306
307
    let mut reset_after_failure = false;
308
    while let Ok(event) = room.events.try_recv() {
309
        reset_after_failure |= matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id);
310
    }
311
    assert!(!reset_after_failure);
312
313
    shut_down(&room.handle, &mut room.events, room.running).await;
314
}
315
316
#[tokio::test]
317
async fn cancelling_a_delayed_bridge_page_preserves_the_recent_stage() {
318
    let room_id = room_id!("!cancel-repair:localhost");
319
    let anchor_id = event_id!("$cancel-anchor:localhost");
320
    let recent_id = event_id!("$cancel-recent:localhost");
321
    let mut room = open_room(room_id).await;
322
    sync_cached_event(&mut room, room_id, anchor_id, "cached anchor").await;
323
324
    room.server
325
        .mock_room_messages()
326
        .ok(RoomMessagesResponseTemplate::default()
327
            .end_token("cancel-prev")
328
            .events(vec![history_event(room_id, recent_id, "recent")]))
329
        .mock_once()
330
        .mount()
331
        .await;
332
    let initial = room
333
        .handle
334
        .history_repair(
335
            room_id.to_owned(),
336
            Some(HistoryRepairPolicy::Bridge),
337
            report_budget(1),
338
        )
339
        .await
340
        .unwrap();
341
    assert_eq!(initial.stop, Some(HistoryRepairStop::Budget));
342
    assert!(initial.recent_available);
343
    receive_until(&mut room.events, |event| {
344
        matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)
345
            && event_messages(event)
346
                .into_iter()
347
                .any(|event| event.event_id.as_deref() == Some(recent_id))
348
    })
349
    .await;
350
351
    room.server
352
        .mock_room_messages()
353
        .match_from("cancel-prev")
354
        .ok(RoomMessagesResponseTemplate::default()
355
            .with_delay(Duration::from_secs(5))
356
            .events(vec![history_event(room_id, anchor_id, "cached anchor")]))
357
        .mock_once()
358
        .mount()
359
        .await;
360
    let repair_handle = room.handle.clone();
361
    let repair = tokio::spawn(async move {
362
        repair_handle
363
            .continue_history_repair(room_id.to_owned(), HistoryRepairBudget::default())
364
            .await
365
    });
366
    tokio::time::timeout(Duration::from_secs(5), async {
367
        loop {
368
            let requested = room
369
                .server
370
                .received_requests()
371
                .await
372
                .unwrap()
373
                .iter()
374
                .any(|request| {
375
                    request.url.path().ends_with("/messages")
376
                        && request
377
                            .url
378
                            .query_pairs()
379
                            .any(|(key, value)| key == "from" && value == "cancel-prev")
380
                });
381
            if requested {
382
                break;
383
            }
384
            tokio::task::yield_now().await;
385
        }
386
    })
387
    .await
388
    .expect("bridge request did not start");
389
    room.handle
390
        .cancel_history_repair(room_id.to_owned())
391
        .await
392
        .unwrap();
393
    let report = repair.await.unwrap().unwrap();
394
    assert_eq!(report.phase, HistoryRepairPhase::Paused);
395
    assert_eq!(report.stop, Some(HistoryRepairStop::Cancelled));
396
    assert!(report.recent_available);
397
    assert_eq!(report.pages, 1);
398
399
    let mut reset_after_cancel = false;
400
    while let Ok(event) = room.events.try_recv() {
401
        reset_after_cancel |= matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id);
402
    }
403
    assert!(!reset_after_cancel);
404
405
    shut_down(&room.handle, &mut room.events, room.running).await;
406
}
407
408
#[tokio::test]
409
async fn bridge_reaches_the_cached_anchor_deduplicates_overlap_and_can_continue_after_budget() {
410
    let room_id = room_id!("!bridge-repair:localhost");
411
    let anchor_id = event_id!("$anchor:localhost");
412
    let middle_id = event_id!("$middle:localhost");
413
    let recent_id = event_id!("$recent:localhost");
414
    let latest_id = event_id!("$latest:localhost");
415
    let mut room = open_room(room_id).await;
416
    sync_cached_event(&mut room, room_id, anchor_id, "cached anchor").await;
417
418
    room.server
419
        .mock_room_messages()
420
        .ok(RoomMessagesResponseTemplate::default()
421
            .end_token("recent-prev")
422
            .events(vec![
423
                history_event(room_id, recent_id, "recent"),
424
                history_event(room_id, middle_id, "middle"),
425
            ]))
426
        .mock_once()
427
        .mount()
428
        .await;
429
    let initial = room
430
        .handle
431
        .history_repair(
432
            room_id.to_owned(),
433
            Some(HistoryRepairPolicy::Bridge),
434
            report_budget(1),
435
        )
436
        .await
437
        .unwrap();
438
    assert_eq!(initial.phase, HistoryRepairPhase::Paused);
439
    assert_eq!(initial.stop, Some(HistoryRepairStop::Budget));
440
    assert!(initial.recent_available);
441
442
    wiremock::Mock::given(|request: &wiremock::Request| {
443
        request.url.path().ends_with("/messages")
444
            && !request.url.query_pairs().any(|(key, _)| key == "from")
445
    })
446
    .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
447
        "start": "head-start",
448
        "chunk": [
449
            history_event(room_id, latest_id, "latest"),
450
            history_event(room_id, recent_id, "recent overlap"),
451
            history_event(room_id, middle_id, "middle overlap"),
452
        ],
453
        "state": [],
454
    })))
455
    .up_to_n_times(1)
456
    .expect(1)
457
    .mount(room.server.server())
458
    .await;
459
    room.server
460
        .mock_room_messages()
461
        .match_from("recent-prev")
462
        .ok(RoomMessagesResponseTemplate::default()
463
            .end_token("older-prev")
464
            .events(vec![
465
                history_event(room_id, middle_id, "middle overlap"),
466
                history_event(room_id, anchor_id, "cached anchor overlap"),
467
            ]))
468
        .mock_once()
469
        .mount()
470
        .await;
471
472
    let report = room
473
        .handle
474
        .continue_history_repair(room_id.to_owned(), HistoryRepairBudget::default())
475
        .await
476
        .unwrap();
477
    assert_eq!(report.strategy, HistoryRepairStrategy::Bridge);
478
    assert_eq!(report.phase, HistoryRepairPhase::Ready, "{report:?}");
479
    assert_eq!(report.continuity, HistoryContinuity::Verified);
480
    assert_eq!(report.pages, 3);
481
482
    let reset = receive_until(&mut room.events, |event| {
483
        matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)
484
            && event_messages(event)
485
                .into_iter()
486
                .any(|event| event.event_id.as_deref() == Some(latest_id))
487
    })
488
    .await;
489
    let reset_ids = event_messages(&reset)
490
        .into_iter()
491
        .filter_map(|event| event.event_id.as_ref().map(ToString::to_string))
492
        .collect::<Vec<_>>();
493
    assert_eq!(
494
        reset_ids,
495
        vec![
496
            anchor_id.to_string(),
497
            middle_id.to_string(),
498
            recent_id.to_string(),
499
            latest_id.to_string(),
500
        ]
501
    );
502
503
    shut_down(&room.handle, &mut room.events, room.running).await;
504
}
505
506
#[tokio::test]
507
async fn a_recent_window_containing_the_cached_anchor_overlaps_under_every_policy() {
508
    let cases = [
509
        (
510
            room_id!("!policy-recent:localhost"),
511
            event_id!("$policy-recent-anchor:localhost"),
512
            HistoryRepairPolicy::StartRecent,
513
        ),
514
        (
515
            room_id!("!policy-bridge:localhost"),
516
            event_id!("$policy-bridge-anchor:localhost"),
517
            HistoryRepairPolicy::Bridge,
518
        ),
519
        (
520
            room_id!("!policy-short-gap:localhost"),
521
            event_id!("$policy-short-anchor:localhost"),
522
            HistoryRepairPolicy::BridgeShortGaps { threshold_ms: 0 },
523
        ),
524
    ];
525
526
    for (room_id, anchor_id, policy) in cases {
527
        let mut room = open_room(room_id).await;
528
        sync_cached_event(&mut room, room_id, anchor_id, "cached anchor").await;
529
        room.server
530
            .mock_room_messages()
531
            .ok(
532
                RoomMessagesResponseTemplate::default().events(vec![history_event(
533
                    room_id,
534
                    anchor_id,
535
                    "same anchor in recent history",
536
                )]),
537
            )
538
            .mock_once()
539
            .mount()
540
            .await;
541
542
        let report = room
543
            .handle
544
            .history_repair(
545
                room_id.to_owned(),
546
                Some(policy.clone()),
547
                HistoryRepairBudget::default(),
548
            )
549
            .await
550
            .unwrap();
551
        assert_eq!(report.requested_policy, policy);
552
        assert_eq!(report.strategy, HistoryRepairStrategy::Overlap);
553
        assert_eq!(report.continuity, HistoryContinuity::Verified);
554
        assert_eq!(report.pages, 1);
555
        assert!(report.recent_available);
556
557
        let requests = room.server.received_requests().await.unwrap();
558
        assert_eq!(
559
            requests
560
                .iter()
561
                .filter(|request| request.url.path().ends_with("/messages"))
562
                .count(),
563
            1
564
        );
565
        shut_down(&room.handle, &mut room.events, room.running).await;
566
    }
567
}
568
569
#[tokio::test]
570
async fn short_gap_policy_stays_recent_when_coverage_is_unknown() {
571
    let room_id = room_id!("!unknown-coverage:localhost");
572
    let anchor_id = event_id!("$unknown-anchor:localhost");
573
    let fresh_id = event_id!("$unknown-fresh:localhost");
574
    let mut room = open_room(room_id).await;
575
    sync_cached_event(&mut room, room_id, anchor_id, "cached anchor").await;
576
    room.server
577
        .mock_room_messages()
578
        .ok(RoomMessagesResponseTemplate::default()
579
            .end_token("recent-prev")
580
            .events(vec![history_event(room_id, fresh_id, "recent window")]))
581
        .mock_once()
582
        .mount()
583
        .await;
584
585
    let report = room
586
        .handle
587
        .history_repair(
588
            room_id.to_owned(),
589
            Some(HistoryRepairPolicy::BridgeShortGaps {
590
                threshold_ms: 60_000,
591
            }),
592
            HistoryRepairBudget::default(),
593
        )
594
        .await
595
        .unwrap();
596
    assert_eq!(report.coverage_duration_ms, None);
597
    assert!(!report.threshold_evaluated);
598
    assert_eq!(report.strategy, HistoryRepairStrategy::Recent);
599
    assert_eq!(report.continuity, HistoryContinuity::Unresolved);
600
    assert_eq!(report.pages, 1);
601
602
    let requests = room.server.received_requests().await.unwrap();
603
    assert_eq!(
604
        requests
605
            .iter()
606
            .filter(|request| request.url.path().ends_with("/messages"))
607
            .count(),
608
        1
609
    );
610
    shut_down(&room.handle, &mut room.events, room.running).await;
611
}