atagen

macltok

public
Star0
Watch0
7cdaa86b9489a6130b716572fa5d91650681196d
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/mentions/service_tests.rsRust770 lines25.5 KB
1
use std::{
2
    sync::{
3
        Arc,
4
        atomic::{AtomicUsize, Ordering},
5
    },
6
    time::Duration,
7
};
8
9
use matrix_sdk::ruma::events::room::MediaSource;
10
use matrix_sdk::{
11
    ruma::{event_id, events::Mentions, push::Ruleset, room_id, user_id},
12
    test_utils::mocks::MatrixMockServer,
13
};
14
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
15
use rusqlite::Connection;
16
use serde_json::json;
17
use wiremock::{
18
    Mock, ResponseTemplate,
19
    matchers::{method, path, query_param, query_param_is_missing},
20
};
21
22
use super::super::store::{Account, MentionsStore};
23
use super::*;
24
use crate::{
25
    MediaId, MediaKind, StoreConfig, TimelineContent, TimelineItem,
26
    normalise::{MediaRegistration, MediaSourceInfo},
27
};
28
29
static STORES: AtomicUsize = AtomicUsize::new(0);
30
const MAX_BYTES: usize = 64 * 1024 * 1024;
31
32
fn config() -> StoreConfig {
33
    let number = STORES.fetch_add(1, Ordering::Relaxed);
34
    StoreConfig::sqlite(
35
        std::env::temp_dir().join(format!(
36
            "macltok-mentions-service-{}-{number}",
37
            std::process::id()
38
        )),
39
        "service commit test passphrase",
40
    )
41
}
42
43
fn account(client: &Client) -> Account {
44
    Account {
45
        homeserver: client.homeserver().to_string(),
46
        user_id: client.user_id().unwrap().to_owned(),
47
    }
48
}
49
50
async fn joined_client(
51
    server: &MatrixMockServer,
52
    room_id: &ruma::RoomId,
53
) -> (Client, ruma::OwnedUserId) {
54
    let client = server.client_builder().build().await;
55
    let own_user = client.user_id().unwrap().to_owned();
56
    let room_id = room_id.to_owned();
57
    server
58
        .mock_sync()
59
        .ok_and_run(&client, {
60
            let own_user = own_user.clone();
61
            move |builder| {
62
                let factory = EventFactory::new().room(&room_id);
63
                builder.add_global_account_data(
64
                    factory.push_rules(Ruleset::server_default(&own_user)),
65
                );
66
                builder.add_joined_room(
67
                    JoinedRoomBuilder::new(&room_id).add_state_event(factory.member(&own_user)),
68
                );
69
            }
70
        })
71
        .await;
72
    (client, own_user)
73
}
74
75
fn mention_batch(
76
    room_id: &ruma::RoomId,
77
    own_user: &ruma::UserId,
78
    event: &str,
79
    previous_pos: Option<&str>,
80
    next_pos: &str,
81
) -> ResponseCommitBatch {
82
    let event = EventFactory::new()
83
        .room(room_id)
84
        .text_msg(event)
85
        .mentions(Mentions::with_user_ids([own_user.to_owned()]))
86
        .event_id(&ruma::EventId::parse(format!("${event}:localhost")).unwrap())
87
        .sender(user_id!("@bob:localhost"))
88
        .into_raw_sync();
89
    let raw_room = serde_json::from_value(json!({
90
        "timeline": [serde_json::to_value(event).unwrap()],
91
    }))
92
    .unwrap();
93
    ResponseCommitBatch {
94
        connection_id: "mentions-connection".to_owned(),
95
        previous_pos: previous_pos.map(str::to_owned),
96
        next_pos: next_pos.to_owned(),
97
        response: Default::default(),
98
        raw_rooms: [(room_id.to_owned(), raw_room)].into(),
99
    }
100
}
101
102
fn quiet_batch(previous_pos: &str, next_pos: &str) -> ResponseCommitBatch {
103
    ResponseCommitBatch {
104
        connection_id: "mentions-connection".to_owned(),
105
        previous_pos: Some(previous_pos.to_owned()),
106
        next_pos: next_pos.to_owned(),
107
        response: Default::default(),
108
        raw_rooms: Default::default(),
109
    }
110
}
111
112
async fn service(client: &Client, store: Option<Result<MentionsStore, Error>>) -> Service {
113
    let media = Arc::new(MediaService::new(
114
        client.clone(),
115
        EngineConfig::default().max_media_bytes,
116
        EngineConfig::default().max_media_sources,
117
    ));
118
    let (_, mut service, _) = Service::new(client.clone(), media, store, EngineConfig::default())
119
        .await
120
        .unwrap();
121
    service.recovery_budget = 0;
122
    service.latest_scan = false;
123
    service
124
}
125
126
fn rows(connection: &Connection) -> BTreeMap<String, Vec<u8>> {
127
    let mut statement = connection
128
        .prepare("SELECT key, value FROM records WHERE key NOT IN ('!cipher', '!head')")
129
        .unwrap();
130
    statement
131
        .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
132
        .unwrap()
133
        .map(Result::unwrap)
134
        .collect()
135
}
136
137
fn discontinuities(service: &Service) -> usize {
138
    service
139
        .data
140
        .coverage
141
        .gaps
142
        .iter()
143
        .filter(|gap| gap.reason == MentionGapReason::SyncDiscontinuity)
144
        .count()
145
}
146
147
#[tokio::test]
148
async fn failed_write_acks_and_keeps_the_inbox_in_memory_until_a_later_write() {
149
    let server = MatrixMockServer::new().await;
150
    let room_id = room_id!("!mentions:localhost");
151
    let (client, own_user) = joined_client(&server, room_id).await;
152
    let config = config();
153
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
154
        .await
155
        .unwrap();
156
    let mut service = service(&client, Some(Ok(store))).await;
157
158
    let connection = Connection::open(config.mentions_location()).unwrap();
159
    connection
160
        .execute_batch(
161
            "CREATE TRIGGER fail_mentions_inbox_insert BEFORE INSERT ON records WHEN NEW.key NOT IN ('!cipher', '!head') BEGIN SELECT RAISE(ABORT, 'injected inbox write failure'); END;
162
             CREATE TRIGGER fail_mentions_inbox_update BEFORE UPDATE ON records WHEN OLD.key NOT IN ('!cipher', '!head') BEGIN SELECT RAISE(ABORT, 'injected inbox write failure'); END;",
163
        )
164
        .unwrap();
165
166
    service
167
        .batch(mention_batch(
168
            room_id,
169
            &own_user,
170
            "first",
171
            None,
172
            "position-1",
173
        ))
174
        .await;
175
    assert!(service.dirty);
176
    assert_eq!(service.data.entries.len(), 1);
177
    assert!(
178
        service
179
            .data
180
            .coverage
181
            .gaps
182
            .iter()
183
            .any(|gap| gap.reason == MentionGapReason::StoreFailed)
184
    );
185
    assert_eq!(service.updates.borrow().entries.len(), 1);
186
    assert!(rows(&connection).is_empty());
187
188
    connection
189
        .execute_batch(
190
            "DROP TRIGGER fail_mentions_inbox_insert;
191
             DROP TRIGGER fail_mentions_inbox_update;",
192
        )
193
        .unwrap();
194
    service.flush_after = 0;
195
    service.batch(quiet_batch("position-1", "position-2")).await;
196
    assert!(!service.dirty);
197
    drop(service);
198
199
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
200
        .await
201
        .unwrap();
202
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
203
    let sync: SyncState = reopened.read("sync").unwrap().unwrap();
204
    assert_eq!(persisted.entries.len(), 1);
205
    assert_eq!(sync.inbox_revision, persisted.revision);
206
    assert_eq!(sync.checkpoints["mentions-connection"].pos, "position-2");
207
    drop(reopened);
208
    drop(connection);
209
    config.delete().await.unwrap();
210
}
211
212
#[tokio::test]
213
async fn a_quiet_response_writes_only_the_checkpoint() {
214
    let server = MatrixMockServer::new().await;
215
    let room_id = room_id!("!mentions:localhost");
216
    let (client, own_user) = joined_client(&server, room_id).await;
217
    let config = config();
218
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
219
        .await
220
        .unwrap();
221
    let mut service = service(&client, Some(Ok(store))).await;
222
    service
223
        .batch(mention_batch(
224
            room_id,
225
            &own_user,
226
            "first",
227
            None,
228
            "position-1",
229
        ))
230
        .await;
231
    let connection = Connection::open(config.mentions_location()).unwrap();
232
    let before = rows(&connection);
233
    assert_eq!(before.len(), 2);
234
235
    service.batch(quiet_batch("position-1", "position-2")).await;
236
    let after = rows(&connection);
237
    assert_eq!(after.len(), 2);
238
    assert_eq!(
239
        before
240
            .iter()
241
            .filter(|(key, value)| after.get(*key) != Some(value))
242
            .count(),
243
        1
244
    );
245
    assert_eq!(service.data.revision, 1);
246
    assert_eq!(
247
        service.sync.checkpoints["mentions-connection"].pos,
248
        "position-2"
249
    );
250
    drop(service);
251
    drop(connection);
252
    config.delete().await.unwrap();
253
}
254
255
#[tokio::test]
256
async fn a_replay_from_the_previous_checkpoint_is_not_a_discontinuity() {
257
    let server = MatrixMockServer::new().await;
258
    let room_id = room_id!("!mentions:localhost");
259
    let (client, own_user) = joined_client(&server, room_id).await;
260
    let config = config();
261
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
262
        .await
263
        .unwrap();
264
    let mut first = service(&client, Some(Ok(store))).await;
265
    first
266
        .batch(mention_batch(room_id, &own_user, "one", None, "position-1"))
267
        .await;
268
    first
269
        .batch(mention_batch(
270
            room_id,
271
            &own_user,
272
            "two",
273
            Some("position-1"),
274
            "position-2",
275
        ))
276
        .await;
277
    drop(first);
278
279
    // the sdk never saved position-2, so it asks again from position-1
280
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
281
        .await
282
        .unwrap();
283
    let mut restarted = service(&client, Some(Ok(store))).await;
284
    restarted
285
        .batch(mention_batch(
286
            room_id,
287
            &own_user,
288
            "three",
289
            Some("position-1"),
290
            "position-3",
291
        ))
292
        .await;
293
    assert_eq!(discontinuities(&restarted), 0);
294
    assert_eq!(restarted.data.entries.len(), 3);
295
    assert_eq!(
296
        restarted.sync.checkpoints["mentions-connection"],
297
        Checkpoint {
298
            pos: "position-3".to_owned(),
299
            previous: Some("position-1".to_owned()),
300
        }
301
    );
302
303
    restarted
304
        .batch(quiet_batch("position-3", "position-4"))
305
        .await;
306
    assert_eq!(discontinuities(&restarted), 0);
307
    restarted
308
        .batch(quiet_batch("position-1", "position-5"))
309
        .await;
310
    assert_eq!(discontinuities(&restarted), 1);
311
    drop(restarted);
312
    config.delete().await.unwrap();
313
}
314
315
#[tokio::test]
316
async fn an_unreadable_store_resets_behind_a_gap() {
317
    let server = MatrixMockServer::new().await;
318
    let room_id = room_id!("!mentions:localhost");
319
    let (client, own_user) = joined_client(&server, room_id).await;
320
    let config = config();
321
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
322
        .await
323
        .unwrap();
324
    let mut first = service(&client, Some(Ok(store))).await;
325
    first
326
        .batch(mention_batch(room_id, &own_user, "one", None, "position-1"))
327
        .await;
328
    drop(first);
329
    let connection = Connection::open(config.mentions_location()).unwrap();
330
    connection
331
        .execute(
332
            "UPDATE records SET value = x'00' WHERE key NOT IN ('!cipher', '!head')",
333
            [],
334
        )
335
        .unwrap();
336
    drop(connection);
337
338
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
339
        .await
340
        .unwrap();
341
    let restarted = service(&client, Some(Ok(store))).await;
342
    assert!(restarted.data.entries.is_empty());
343
    assert!(
344
        restarted
345
            .data
346
            .coverage
347
            .gaps
348
            .iter()
349
            .any(|gap| gap.reason == MentionGapReason::StoreReset)
350
    );
351
    drop(restarted);
352
    config.delete().await.unwrap();
353
}
354
355
#[tokio::test]
356
async fn repair_policy_and_coverage_roundtrip_through_the_inbox() {
357
    let server = MatrixMockServer::new().await;
358
    let room_id = room_id!("!repair-coverage:localhost");
359
    let (client, _) = joined_client(&server, room_id).await;
360
    let config = config();
361
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
362
        .await
363
        .unwrap();
364
    let media = Arc::new(MediaService::new(
365
        client.clone(),
366
        EngineConfig::default().max_media_bytes,
367
        EngineConfig::default().max_media_sources,
368
    ));
369
    let (handle, mut service, _) = Service::new(
370
        client.clone(),
371
        media,
372
        Some(Ok(store)),
373
        EngineConfig::default(),
374
    )
375
    .await
376
    .unwrap();
377
    service.recovery_budget = 0;
378
    service.latest_scan = false;
379
    let service_task = tokio::spawn(service.run());
380
381
    let default_policy = HistoryRepairPolicy::default();
382
    assert_eq!(handle.repair_policy().await.unwrap(), default_policy);
383
    let stored_policy = HistoryRepairPolicy::Bridge;
384
    handle
385
        .set_repair_policy(stored_policy.clone())
386
        .await
387
        .unwrap();
388
    handle.repair_resolved(room_id.to_owned()).await.unwrap();
389
    let coverage = handle.repair_coverage(room_id.to_owned()).await.unwrap();
390
    assert!(coverage.last_confirmed_ms.is_some());
391
    assert!(!coverage.reliable);
392
    assert_eq!(coverage.duration_ms(), None);
393
394
    handle.stop().await;
395
    service_task.await.unwrap();
396
    drop(handle);
397
398
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
399
        .await
400
        .unwrap();
401
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
402
    assert_eq!(persisted.repair_policy, stored_policy);
403
    let sync: SyncState = reopened.read("sync").unwrap().unwrap();
404
    let coverage = sync.repair_coverage.get(room_id).unwrap();
405
    assert!(coverage.last_confirmed_ms.is_some());
406
    assert!(coverage.boundary_ms.is_none());
407
    assert!(!coverage.unresolved);
408
    assert!(!coverage.reliable);
409
    drop(reopened);
410
    config.delete().await.unwrap();
411
}
412
413
#[tokio::test]
414
async fn repair_limited_sync_freezes_the_last_room_checkpoint() {
415
    let server = MatrixMockServer::new().await;
416
    let room_id = room_id!("!repair-boundary:localhost");
417
    let (client, _) = joined_client(&server, room_id).await;
418
    let media = Arc::new(MediaService::new(
419
        client.clone(),
420
        EngineConfig::default().max_media_bytes,
421
        EngineConfig::default().max_media_sources,
422
    ));
423
    let (_handle, mut service, _) = Service::new(client, media, None, EngineConfig::default())
424
        .await
425
        .unwrap();
426
    service.sync.checkpoints.insert(
427
        "mentions-connection".to_owned(),
428
        Checkpoint {
429
            pos: "position-before".to_owned(),
430
            previous: None,
431
        },
432
    );
433
    service.sync.observed_rooms.insert(room_id.to_owned());
434
    service
435
        .sync
436
        .repair_coverage
437
        .entry(room_id.to_owned())
438
        .or_default()
439
        .last_confirmed_ms = Some(1_000);
440
441
    let events = [
442
        EventFactory::new()
443
            .room(room_id)
444
            .text_msg("older event")
445
            .sender(user_id!("@sender:localhost"))
446
            .event_id(event_id!("$repair-older:localhost"))
447
            .into_raw_sync(),
448
        EventFactory::new()
449
            .room(room_id)
450
            .text_msg("latest event")
451
            .sender(user_id!("@sender:localhost"))
452
            .event_id(event_id!("$repair-latest:localhost"))
453
            .into_raw_sync(),
454
    ];
455
    let timeline: Vec<_> = events
456
        .iter()
457
        .map(|event| serde_json::to_value(event).unwrap())
458
        .collect();
459
    let raw_room = serde_json::from_value(json!({
460
        "limited": true,
461
        "prev_batch": "repair-gap-token",
462
        "timeline": timeline,
463
    }))
464
    .unwrap();
465
    let batch = ResponseCommitBatch {
466
        connection_id: "mentions-connection".to_owned(),
467
        previous_pos: Some("position-before".to_owned()),
468
        next_pos: "position-after".to_owned(),
469
        response: Default::default(),
470
        raw_rooms: [(room_id.to_owned(), raw_room)].into(),
471
    };
472
473
    service.batch(batch).await;
474
475
    let coverage = service.sync.repair_coverage.get(room_id).unwrap();
476
    assert_eq!(coverage.last_confirmed_ms, Some(1_000));
477
    assert!(
478
        coverage
479
            .boundary_ms
480
            .is_some_and(|boundary| boundary > 1_000)
481
    );
482
    assert!(coverage.unresolved);
483
    assert!(!coverage.reliable);
484
    assert_eq!(coverage.duration_ms(), None);
485
    assert_eq!(
486
        coverage.latest_anchor.as_deref(),
487
        Some(event_id!("$repair-latest:localhost"))
488
    );
489
}
490
491
#[tokio::test]
492
async fn service_recovers_notification_history_into_durable_unread_inbox() {
493
    let server = MatrixMockServer::new().await;
494
    let room_id = room_id!("!mentions:localhost");
495
    let (client, own_user) = joined_client(&server, room_id).await;
496
    let first = EventFactory::new()
497
        .room(room_id)
498
        .text_msg("history mention one")
499
        .mentions(Mentions::with_user_ids([own_user.clone()]))
500
        .event_id(event_id!("$history-one:localhost"))
501
        .sender(user_id!("@bob:localhost"))
502
        .into_raw_sync();
503
    let second = EventFactory::new()
504
        .room(room_id)
505
        .text_msg("history mention two")
506
        .mentions(Mentions::with_user_ids([own_user]))
507
        .event_id(event_id!("$history-two:localhost"))
508
        .sender(user_id!("@bob:localhost"))
509
        .into_raw_sync();
510
    let first = serde_json::from_str::<serde_json::Value>(first.json().get()).unwrap();
511
    let second = serde_json::from_str::<serde_json::Value>(second.json().get()).unwrap();
512
    Mock::given(method("GET"))
513
        .and(path("/_matrix/client/v3/notifications"))
514
        .and(query_param_is_missing("from"))
515
        .and(query_param("limit", "50"))
516
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
517
            "notifications": [{
518
                "actions": ["notify"],
519
                "event": first,
520
                "read": true,
521
                "room_id": room_id.as_str(),
522
                "ts": 1,
523
            }],
524
            "next_token": "history-page-two",
525
        })))
526
        .mount(server.server())
527
        .await;
528
    Mock::given(method("GET"))
529
        .and(path("/_matrix/client/v3/notifications"))
530
        .and(query_param("from", "history-page-two"))
531
        .and(query_param("limit", "50"))
532
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
533
            "notifications": [{
534
                "actions": ["notify"],
535
                "event": second,
536
                "read": true,
537
                "room_id": room_id.as_str(),
538
                "ts": 2,
539
            }],
540
        })))
541
        .mount(server.server())
542
        .await;
543
544
    let config = config();
545
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
546
        .await
547
        .unwrap();
548
    let media = Arc::new(MediaService::new(
549
        client.clone(),
550
        EngineConfig::default().max_media_bytes,
551
        EngineConfig::default().max_media_sources,
552
    ));
553
    let (handle, service, mut updates) = Service::new(
554
        client.clone(),
555
        media,
556
        Some(Ok(store)),
557
        EngineConfig::default(),
558
    )
559
    .await
560
    .unwrap();
561
    let service_task = tokio::spawn(service.run());
562
563
    let discovered = tokio::time::timeout(Duration::from_secs(10), async {
564
        loop {
565
            let page = updates.borrow_and_update().clone();
566
            if page.entries.len() == 2 && !page.coverage.recovering {
567
                break page;
568
            }
569
            updates.changed().await.unwrap();
570
        }
571
    })
572
    .await;
573
    handle.stop().await;
574
    service_task.await.unwrap();
575
    let discovered = discovered.expect("notification history should be recovered");
576
    assert_eq!(discovered.entries.len(), 2);
577
    assert!(discovered.entries.iter().all(|entry| !entry.read));
578
579
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
580
        .await
581
        .unwrap();
582
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
583
    let sync: SyncState = reopened.read("sync").unwrap().unwrap();
584
    assert_eq!(sync.inbox_revision, persisted.revision);
585
    assert_eq!(persisted.entries.len(), 2);
586
    assert!(persisted.entries.values().all(|entry| !entry.read));
587
    assert!(persisted.latest_cursor.is_none());
588
    assert!(persisted.history_cursor.is_none());
589
    drop(reopened);
590
591
    config.delete().await.unwrap();
592
}
593
594
fn message_body(item: &TimelineItem) -> Option<&str> {
595
    match item {
596
        TimelineItem::Event(event) => match &event.content {
597
            TimelineContent::Message(message) => Some(&message.body),
598
            _ => None,
599
        },
600
        _ => None,
601
    }
602
}
603
604
fn media_registration() -> MediaRegistration {
605
    MediaRegistration {
606
        id: MediaId::new("test-scope#image".to_owned()),
607
        kind: MediaKind::Image,
608
        full: MediaSourceInfo {
609
            source: MediaSource::Plain(ruma::mxc_uri!("mxc://mentions.example/image").to_owned()),
610
            mimetype: Some("image/png".to_owned()),
611
            declared_size: Some(1234),
612
        },
613
        thumbnail: None,
614
    }
615
}
616
617
#[tokio::test]
618
async fn live_context_survives_capture_then_edit_and_redaction() {
619
    let server = MatrixMockServer::new().await;
620
    let room_id = room_id!("!mentions:localhost");
621
    let (client, own_user) = joined_client(&server, room_id).await;
622
    let mention = EventFactory::new()
623
        .room(room_id)
624
        .text_msg("direct mention")
625
        .mentions(Mentions::with_user_ids([own_user]))
626
        .event_id(event_id!("$live-mention:localhost"))
627
        .sender(user_id!("@bob:localhost"))
628
        .into_raw_sync();
629
    let mut mention: serde_json::Value = serde_json::from_str(mention.json().get()).unwrap();
630
    mention["origin_server_ts"] = json!(10);
631
    let following = EventFactory::new()
632
        .room(room_id)
633
        .text_msg("following")
634
        .event_id(event_id!("$live-following:localhost"))
635
        .sender(user_id!("@bob:localhost"))
636
        .into_raw_sync();
637
    let mut following: serde_json::Value = serde_json::from_str(following.json().get()).unwrap();
638
    following["origin_server_ts"] = json!(11);
639
640
    let media = Arc::new(MediaService::new(
641
        client.clone(),
642
        EngineConfig::default().max_media_bytes,
643
        EngineConfig::default().max_media_sources,
644
    ));
645
    let (_handle, mut service, _updates) =
646
        Service::new(client, media, None, EngineConfig::default())
647
            .await
648
            .unwrap();
649
    let room_id = room_id.to_owned();
650
    let mention_key = MentionKey {
651
        room_id: room_id.clone(),
652
        event_id: event_id!("$live-mention:localhost").to_owned(),
653
    };
654
    let following_id = format!("{room_id}\n$live-following:localhost");
655
    let mut data = Data::default();
656
657
    service.observe(&mut data, &room_id, mention).await;
658
    service.observe(&mut data, &room_id, following).await;
659
660
    let mention_identity = id(&mention_key);
661
    let entry = data.entries.get(&mention_identity).unwrap();
662
    let revision = entry.revision;
663
    let target = entry.event.clone();
664
    service.captured(
665
        &mut data,
666
        mention_identity.clone(),
667
        revision,
668
        Ok((
669
            crate::EventContext {
670
                events_before: Vec::new(),
671
                event: Some(target),
672
                events_after: Vec::new(),
673
                start_token: None,
674
                end_token: None,
675
            },
676
            Vec::new(),
677
            Vec::new(),
678
        )),
679
    );
680
681
    assert_eq!(
682
        data.entries[&mention_identity].capture,
683
        MentionCaptureState::Partial
684
    );
685
    assert!(data.events.contains_key(&following_id));
686
    let (saved, _) = data.context(&mention_key).unwrap();
687
    let context = saved.context.unwrap();
688
    assert_eq!(context.events_after.len(), 1);
689
    assert_eq!(message_body(&context.events_after[0]), Some("following"));
690
691
    let edit = json!({
692
        "content": {
693
            "body": "* edited following",
694
            "m.new_content": {"body": "edited following", "msgtype": "m.text"},
695
            "m.relates_to": {"event_id": "$live-following:localhost", "rel_type": "m.replace"},
696
            "msgtype": "m.text"
697
        },
698
        "event_id": "$live-following-edit:localhost",
699
        "origin_server_ts": 12,
700
        "room_id": room_id.as_str(),
701
        "sender": "@bob:localhost",
702
        "type": "m.room.message"
703
    });
704
    service.observe(&mut data, &room_id, edit).await;
705
    let (saved, _) = data.context(&mention_key).unwrap();
706
    let context = saved.context.unwrap();
707
    assert_eq!(
708
        message_body(&context.events_after[0]),
709
        Some("edited following")
710
    );
711
712
    let saved_following = data.events.get_mut(&following_id).unwrap();
713
    assert!(saved_following.raw.is_some());
714
    saved_following.media.push(media_registration());
715
    assert_eq!(saved_following.media.len(), 1);
716
    let redaction = json!({
717
        "content": {},
718
        "event_id": "$live-following-redaction:localhost",
719
        "origin_server_ts": 13,
720
        "redacts": "$live-following:localhost",
721
        "room_id": room_id.as_str(),
722
        "sender": "@bob:localhost",
723
        "type": "m.room.redaction"
724
    });
725
    service.observe(&mut data, &room_id, redaction).await;
726
727
    let saved_following = data.events.get(&following_id).unwrap();
728
    assert!(matches!(
729
        &saved_following.item,
730
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
731
    ));
732
    assert!(saved_following.raw.is_none());
733
    assert!(saved_following.media.is_empty());
734
    let (saved, _) = data.context(&mention_key).unwrap();
735
    let context = saved.context.unwrap();
736
    assert!(matches!(
737
        &context.events_after[0],
738
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
739
    ));
740
    data.windows.remove(&mention_identity);
741
    service.data = data;
742
    let CommandOutput::MentionContext(context) = service
743
        .command(MentionsCommand::RetryContext(mention_key))
744
        .await
745
        .unwrap()
746
    else {
747
        panic!("retry should return saved context");
748
    };
749
    assert_eq!(context.state, MentionCaptureState::Pending);
750
    assert!(context.context.unwrap().event.is_some());
751
}
752
753
#[tokio::test]
754
async fn explicit_recovery_rechecks_server_support() {
755
    let server = MatrixMockServer::new().await;
756
    let (client, _) = joined_client(&server, room_id!("!mentions:localhost")).await;
757
    let config = EngineConfig::default();
758
    let media = Arc::new(MediaService::new(
759
        client.clone(),
760
        config.max_media_bytes,
761
        config.max_media_sources,
762
    ));
763
    let (_, mut service, _) = Service::new(client, media, None, config).await.unwrap();
764
    service.data.coverage.historical_supported = Some(false);
765
    service.recovery_budget = 0;
766
    service.command(MentionsCommand::Recover).await.unwrap();
767
    assert_eq!(service.data.coverage.historical_supported, None);
768
    assert!(service.data.coverage.recovering);
769
    assert_eq!(service.recovery_budget, 20);
770
}