atagen

macltok

public
Star0
Watch0
0dfdd69718302c1d2503a83a5efbcec10b9da6c2
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/mentions_inbox.rsRust1428 lines47.6 KB
1
mod support;
2
3
use std::{
4
    collections::{BTreeMap, HashSet},
5
    sync::{
6
        Arc, Mutex,
7
        atomic::{AtomicUsize, Ordering},
8
    },
9
    time::Duration,
10
};
11
12
#[cfg(feature = "sqlite")]
13
use macltok::{EncryptedSessionStore, PasswordLogin, Restore, Session, SessionStore, StoreConfig};
14
use macltok::{Engine, EngineConfig, Event, MentionReason, MentionsPage};
15
use matrix_sdk::{
16
    Client,
17
    ruma::{OwnedRoomId, OwnedUserId, event_id, events::Mentions, room_id, user_id},
18
    test_utils::mocks::{MatrixMockServer, RoomContextResponseTemplate},
19
};
20
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
21
use ruma::push::{
22
    Action, EventMatchConditionData, HighlightTweakValue, NewConditionalPushRule,
23
    NewPatternedPushRule, PushCondition, Ruleset, SoundTweakValue, Tweak,
24
};
25
use serde_json::{Value, json};
26
#[cfg(feature = "sqlite")]
27
use support::receive_until;
28
use support::shut_down;
29
use wiremock::{
30
    Mock, Request, Respond, ResponseTemplate,
31
    matchers::{method, path, path_regex, query_param},
32
};
33
34
#[cfg(feature = "serde")]
35
use macltok::{MentionCoverage, MentionsBoundary, MentionsCommand};
36
37
const SLIDING_SYNC_PATH: &str = "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync";
38
39
struct SlidingFeed {
40
    rooms: Arc<Mutex<BTreeMap<OwnedRoomId, Vec<Value>>>>,
41
    required_state: Arc<Mutex<BTreeMap<OwnedRoomId, Vec<Value>>>>,
42
    requests: Arc<AtomicUsize>,
43
}
44
45
impl SlidingFeed {
46
    fn new(room_id: OwnedRoomId, events: Vec<Value>) -> Self {
47
        Self {
48
            rooms: Arc::new(Mutex::new(BTreeMap::from([(room_id, events)]))),
49
            required_state: Arc::new(Mutex::new(BTreeMap::new())),
50
            requests: Arc::new(AtomicUsize::new(0)),
51
        }
52
    }
53
54
    fn replace(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
55
        self.rooms
56
            .lock()
57
            .unwrap()
58
            .insert(room_id.to_owned(), events);
59
    }
60
61
    fn add_room(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
62
        self.rooms
63
            .lock()
64
            .unwrap()
65
            .insert(room_id.to_owned(), events);
66
    }
67
68
    fn set_required_state(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
69
        self.required_state
70
            .lock()
71
            .unwrap()
72
            .insert(room_id.to_owned(), events);
73
    }
74
75
    async fn mount(&self, server: &MatrixMockServer) {
76
        Mock::given(method("POST"))
77
            .and(path(SLIDING_SYNC_PATH))
78
            .respond_with(self.clone())
79
            .mount(server.server())
80
            .await;
81
    }
82
}
83
84
impl Clone for SlidingFeed {
85
    fn clone(&self) -> Self {
86
        Self {
87
            rooms: self.rooms.clone(),
88
            required_state: self.required_state.clone(),
89
            requests: self.requests.clone(),
90
        }
91
    }
92
}
93
94
impl Respond for SlidingFeed {
95
    fn respond(&self, _request: &Request) -> ResponseTemplate {
96
        let request = self.requests.fetch_add(1, Ordering::AcqRel);
97
        let required_state = self.required_state.lock().unwrap().clone();
98
        let rooms: serde_json::Map<String, Value> = self
99
            .rooms
100
            .lock()
101
            .unwrap()
102
            .iter()
103
            .map(|(room_id, events)| {
104
                (
105
                    room_id.to_string(),
106
                    json!({
107
                        "initial": request == 0,
108
                        "required_state": required_state
109
                            .get(room_id)
110
                            .cloned()
111
                            .unwrap_or_default(),
112
                        "timeline": events,
113
                    }),
114
                )
115
            })
116
            .collect();
117
        ResponseTemplate::new(200)
118
            .set_body_json(json!({
119
                "txn_id": null,
120
                "pos": format!("mentions-{request}"),
121
                "lists": {},
122
                "rooms": rooms,
123
                "extensions": {},
124
            }))
125
            .set_delay(Duration::from_millis(20))
126
    }
127
}
128
129
async fn joined_client(server: &MatrixMockServer, room_id: &ruma::RoomId) -> (Client, OwnedUserId) {
130
    let client = server.client_builder().build().await;
131
    let own_user = client.user_id().unwrap().to_owned();
132
    let room_id = room_id.to_owned();
133
    server
134
        .mock_sync()
135
        .ok_and_run(&client, {
136
            let own_user = own_user.clone();
137
            move |builder| {
138
                let factory = EventFactory::new().room(&room_id);
139
                builder.add_global_account_data(
140
                    factory.push_rules(Ruleset::server_default(&own_user)),
141
                );
142
                builder.add_joined_room(
143
                    JoinedRoomBuilder::new(&room_id).add_state_event(factory.member(&own_user)),
144
                );
145
            }
146
        })
147
        .await;
148
    (client, own_user)
149
}
150
151
fn message(
152
    event_id: &str,
153
    sender: &str,
154
    timestamp: u64,
155
    body: &str,
156
    mentioned: Option<&str>,
157
) -> Value {
158
    let mut content = json!({ "msgtype": "m.text", "body": body });
159
    if let Some(mentioned) = mentioned {
160
        content["m.mentions"] = json!({ "user_ids": [mentioned] });
161
    }
162
    json!({
163
        "type": "m.room.message",
164
        "event_id": event_id,
165
        "sender": sender,
166
        "origin_server_ts": timestamp,
167
        "content": content,
168
    })
169
}
170
171
fn room_mention(event_id: &str, sender: &str, timestamp: u64) -> Value {
172
    let mut event = message(event_id, sender, timestamp, "room mention", None);
173
    event["content"]["m.mentions"] = json!({ "room": true });
174
    event
175
}
176
177
async fn mentions_page(
178
    events: &mut macltok::EventStream,
179
    room_id: &ruma::RoomId,
180
    predicate: impl Fn(&MentionsPage) -> bool,
181
) -> MentionsPage {
182
    mentions_page_with_diagnostics(events, room_id, predicate, None).await
183
}
184
185
async fn mentions_page_with_diagnostics(
186
    events: &mut macltok::EventStream,
187
    room_id: &ruma::RoomId,
188
    predicate: impl Fn(&MentionsPage) -> bool,
189
    diagnostic_server: Option<&MatrixMockServer>,
190
) -> MentionsPage {
191
    let mut last_page = None;
192
    let result = tokio::time::timeout(Duration::from_secs(5), async {
193
        while let Some(event) = events.recv().await {
194
            match event {
195
                Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id => {
196
                    panic!("the unopened room produced a timeline reset");
197
                }
198
                Event::MentionsChanged(update) => {
199
                    let snapshot = update.snapshot;
200
                    let matched = predicate(&snapshot);
201
                    last_page = Some(snapshot.clone());
202
                    if matched {
203
                        return snapshot;
204
                    }
205
                }
206
                _ => {}
207
            }
208
        }
209
        panic!("event stream closed before the expected mentions page");
210
    })
211
    .await;
212
213
    match result {
214
        Ok(page) => page,
215
        Err(_) => {
216
            let last_state = last_page.map(|page: MentionsPage| {
217
                (
218
                    page.revision,
219
                    page.unread_count,
220
                    page.entries
221
                        .iter()
222
                        .map(|entry| {
223
                            (
224
                                entry.key.room_id.to_string(),
225
                                entry.key.event_id.to_string(),
226
                                entry.capture.clone(),
227
                                entry.revision,
228
                            )
229
                        })
230
                        .collect::<Vec<_>>(),
231
                )
232
            });
233
            let requests = if let Some(server) = diagnostic_server {
234
                server.server().received_requests().await.map(|requests| {
235
                    requests
236
                        .iter()
237
                        .filter(|request| {
238
                            let path = request.url.path();
239
                            path.contains(room_id.as_str())
240
                                && (path.contains("/context/") || path.ends_with("/messages"))
241
                        })
242
                        .map(|request| format!("{} {}", request.method, request.url))
243
                        .collect::<Vec<_>>()
244
                })
245
            } else {
246
                None
247
            };
248
            panic!(
249
                "expected mentions update did not arrive, last state {last_state:?}, requests {requests:?}"
250
            );
251
        }
252
    }
253
}
254
255
fn entry_ids(page: &MentionsPage) -> HashSet<String> {
256
    page.entries
257
        .iter()
258
        .map(|entry| entry.key.event_id.to_string())
259
        .collect()
260
}
261
262
async fn wait_for_requests(feed: &SlidingFeed, minimum: usize) {
263
    tokio::time::timeout(Duration::from_secs(5), async {
264
        while feed.requests.load(Ordering::Acquire) < minimum {
265
            tokio::task::yield_now().await;
266
        }
267
    })
268
    .await
269
    .expect("the sliding sync feed did not receive another request");
270
}
271
272
async fn mount_messages_page(
273
    server: &MatrixMockServer,
274
    room_id: &ruma::RoomId,
275
    direction: &str,
276
    token: &str,
277
    limit: u16,
278
) {
279
    Mock::given(method("GET"))
280
        .and(path_regex(format!(
281
            r"^/_matrix/client/v3/rooms/{room_id}/messages$"
282
        )))
283
        .and(query_param("dir", direction))
284
        .and(query_param("from", token))
285
        .and(move |request: &Request| {
286
            request
287
                .url
288
                .query_pairs()
289
                .find(|(key, _)| key == "limit")
290
                .map_or(limit == 10, |(_, value)| value == limit.to_string())
291
        })
292
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
293
            "start": token,
294
            "end": format!("{token}-next"),
295
            "chunk": [],
296
            "state": [],
297
        })))
298
        .expect(1)
299
        .mount(server.server())
300
        .await;
301
}
302
303
#[tokio::test]
304
async fn unopened_room_mentions_are_deduplicated_and_read_through_uses_a_revision_boundary() {
305
    let server = MatrixMockServer::new().await;
306
    let room_id = room_id!("!mentions-inbox:localhost");
307
    let (client, own_user) = joined_client(&server, room_id).await;
308
    let sender = user_id!("@alice:localhost");
309
    let initial = vec![
310
        message(
311
            "$mention",
312
            sender.as_str(),
313
            2_000,
314
            "direct mention",
315
            Some(own_user.as_str()),
316
        ),
317
        message(
318
            "$own-mention",
319
            own_user.as_str(),
320
            2_100,
321
            "own event",
322
            Some(own_user.as_str()),
323
        ),
324
        message(
325
            "$unrelated",
326
            sender.as_str(),
327
            2_200,
328
            "ordinary message",
329
            None,
330
        ),
331
    ];
332
    let feed = SlidingFeed::new(room_id.to_owned(), initial.clone());
333
    feed.mount(&server).await;
334
335
    let (engine, handle, mut events) = Engine::from_client(
336
        client,
337
        EngineConfig::default()
338
            .with_initial_history_limit(0)
339
            .with_search_backfill_pages_per_room(0),
340
    )
341
    .await
342
    .unwrap();
343
    let running = tokio::spawn(engine.run());
344
345
    let first = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
346
    assert_eq!(entry_ids(&first), HashSet::from(["$mention".to_owned()]));
347
    assert_eq!(first.unread_count, 1);
348
    let first_boundary = first.boundary.clone();
349
350
    wait_for_requests(&feed, 3).await;
351
    let replayed = handle.mentions_page(None, 50).await.unwrap();
352
    assert_eq!(entry_ids(&replayed), HashSet::from(["$mention".to_owned()]));
353
354
    let key = replayed.entries[0].key.clone();
355
    handle
356
        .set_mentions_read(vec![key.clone()], true)
357
        .await
358
        .unwrap();
359
    let read = handle.mentions_page(None, 50).await.unwrap();
360
    assert_eq!(read.unread_count, 0);
361
362
    feed.replace(
363
        room_id,
364
        [
365
            initial,
366
            vec![message(
367
                "$older-mention",
368
                sender.as_str(),
369
                1_000,
370
                "older direct mention",
371
                Some(own_user.as_str()),
372
            )],
373
        ]
374
        .concat(),
375
    );
376
    let updated = mentions_page(&mut events, room_id, |page| page.entries.len() == 2).await;
377
    assert_eq!(
378
        entry_ids(&updated),
379
        HashSet::from(["$mention".to_owned(), "$older-mention".to_owned(),])
380
    );
381
382
    handle
383
        .mark_mentions_read_through(first_boundary)
384
        .await
385
        .unwrap();
386
    let after_boundary = handle.mentions_page(None, 50).await.unwrap();
387
    let read_states: Vec<_> = after_boundary
388
        .entries
389
        .iter()
390
        .map(|entry| (entry.key.event_id.to_string(), entry.read))
391
        .collect();
392
    assert!(read_states.contains(&("$mention".to_owned(), true)));
393
    assert!(read_states.contains(&("$older-mention".to_owned(), false)));
394
    assert_eq!(after_boundary.unread_count, 1);
395
396
    shut_down(&handle, &mut events, running).await;
397
}
398
399
#[tokio::test]
400
async fn edits_can_remove_and_restore_a_mention_until_redaction_removes_its_target() {
401
    let server = MatrixMockServer::new().await;
402
    let room_id = room_id!("!mentions-edits:localhost");
403
    let (client, own_user) = joined_client(&server, room_id).await;
404
    let sender = user_id!("@alice:localhost");
405
    let original = message(
406
        "$edited-target",
407
        sender.as_str(),
408
        1_000,
409
        "original mention",
410
        Some(own_user.as_str()),
411
    );
412
    let removal = json!({
413
        "type": "m.room.message",
414
        "event_id": "$remove-edit",
415
        "sender": sender,
416
        "origin_server_ts": 2_000,
417
        "content": {
418
            "msgtype": "m.text",
419
            "body": "* edited without a mention",
420
            "m.relates_to": { "rel_type": "m.replace", "event_id": "$edited-target" },
421
            "m.new_content": { "msgtype": "m.text", "body": "edited without a mention" },
422
        },
423
    });
424
    let addition = json!({
425
        "type": "m.room.message",
426
        "event_id": "$add-edit",
427
        "sender": sender,
428
        "origin_server_ts": 3_000,
429
        "content": {
430
            "msgtype": "m.text",
431
            "body": "* edited to mention you",
432
            "m.relates_to": { "rel_type": "m.replace", "event_id": "$edited-target" },
433
            "m.new_content": {
434
                "msgtype": "m.text",
435
                "body": "edited to mention you",
436
                "m.mentions": { "user_ids": [own_user.as_str()] },
437
            },
438
        },
439
    });
440
    let redaction = json!({
441
        "type": "m.room.redaction",
442
        "event_id": "$redaction",
443
        "sender": sender,
444
        "origin_server_ts": 4_000,
445
        "redacts": "$edited-target",
446
        "content": {},
447
    });
448
    let feed = SlidingFeed::new(room_id.to_owned(), vec![original.clone()]);
449
    feed.mount(&server).await;
450
451
    let (engine, handle, mut events) = Engine::from_client(
452
        client,
453
        EngineConfig::default()
454
            .with_initial_history_limit(0)
455
            .with_search_backfill_pages_per_room(0),
456
    )
457
    .await
458
    .unwrap();
459
    let running = tokio::spawn(engine.run());
460
461
    let discovered = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
462
    assert_eq!(
463
        entry_ids(&discovered),
464
        HashSet::from(["$edited-target".to_owned()])
465
    );
466
467
    feed.replace(room_id, vec![original.clone(), removal.clone()]);
468
    let removed = mentions_page(&mut events, room_id, |page| {
469
        page.revision > discovered.revision && page.entries.is_empty()
470
    })
471
    .await;
472
    assert!(removed.entries.is_empty());
473
    assert_eq!(
474
        handle
475
            .mention_context(room_id, ruma::event_id!("$edited-target"))
476
            .await
477
            .unwrap_err()
478
            .kind,
479
        macltok::ErrorKind::EventNotFound
480
    );
481
482
    feed.replace(room_id, vec![original.clone(), removal, addition]);
483
    let restored = mentions_page(&mut events, room_id, |page| {
484
        page.revision > removed.revision && entry_ids(page).contains("$edited-target")
485
    })
486
    .await;
487
    assert_eq!(
488
        entry_ids(&restored),
489
        HashSet::from(["$edited-target".to_owned()])
490
    );
491
492
    feed.replace(room_id, vec![original, redaction]);
493
    let redacted = mentions_page(&mut events, room_id, |page| {
494
        page.revision > restored.revision && page.entries.is_empty()
495
    })
496
    .await;
497
    assert!(redacted.entries.is_empty());
498
    assert_eq!(
499
        handle
500
            .mention_context(room_id, ruma::event_id!("$edited-target"))
501
            .await
502
            .unwrap_err()
503
            .kind,
504
        macltok::ErrorKind::EventNotFound
505
    );
506
507
    shut_down(&handle, &mut events, running).await;
508
}
509
510
#[tokio::test]
511
async fn room_mentions_require_power_while_muted_direct_mentions_remain_and_ignored_senders_drop() {
512
    let server = MatrixMockServer::new().await;
513
    let client = server.client_builder().build().await;
514
    let own_user = client.user_id().unwrap().to_owned();
515
    let room_mention_room = room_id!("!room-mention-policy:localhost");
516
    let muted_room = room_id!("!muted-mention-policy:localhost");
517
    let ignored_sender = user_id!("@ignored:localhost");
518
    let sender = user_id!("@alice:localhost");
519
    let mut rules = Ruleset::server_default(&own_user);
520
    rules.override_.shift_insert(
521
        1,
522
        NewConditionalPushRule::new(
523
            "mute-inbox-room".to_owned(),
524
            vec![PushCondition::EventMatch(EventMatchConditionData::new(
525
                "room_id".to_owned(),
526
                muted_room.to_string(),
527
            ))],
528
            Vec::new(),
529
        )
530
        .into(),
531
    );
532
    server
533
        .mock_sync()
534
        .ok_and_run(&client, {
535
            let own_user = own_user.clone();
536
            let ignored_sender = ignored_sender.to_owned();
537
            move |builder| {
538
                let factory = EventFactory::new();
539
                builder.add_global_account_data(factory.push_rules(rules));
540
                builder.add_global_account_data(factory.ignored_user_list([ignored_sender]));
541
                builder.add_joined_room(
542
                    JoinedRoomBuilder::new(room_mention_room)
543
                        .add_state_event(
544
                            EventFactory::new()
545
                                .room(room_mention_room)
546
                                .member(&own_user),
547
                        )
548
                        .add_state_event(
549
                            EventFactory::new()
550
                                .room(room_mention_room)
551
                                .default_power_levels()
552
                                .sender(&own_user),
553
                        ),
554
                );
555
                builder.add_joined_room(
556
                    JoinedRoomBuilder::new(muted_room)
557
                        .add_state_event(EventFactory::new().room(muted_room).member(&own_user))
558
                        .add_state_event(
559
                            EventFactory::new()
560
                                .room(muted_room)
561
                                .default_power_levels()
562
                                .sender(&own_user),
563
                        ),
564
                );
565
            }
566
        })
567
        .await;
568
569
    let feed = SlidingFeed::new(
570
        room_mention_room.to_owned(),
571
        vec![room_mention(
572
            "$unauthorized-room-mention",
573
            sender.as_str(),
574
            3_000,
575
        )],
576
    );
577
    feed.mount(&server).await;
578
    let muted_events = vec![message(
579
        "$muted-direct-mention",
580
        sender.as_str(),
581
        2_000,
582
        "muted direct mention",
583
        Some(own_user.as_str()),
584
    )];
585
    let ignored = message(
586
        "$ignored-direct-mention",
587
        ignored_sender.as_str(),
588
        1_000,
589
        "ignored direct mention",
590
        Some(own_user.as_str()),
591
    );
592
    let mut muted_events = muted_events;
593
    muted_events.push(ignored);
594
    feed.add_room(muted_room, muted_events);
595
596
    let (engine, handle, mut events) = Engine::from_client(
597
        client,
598
        EngineConfig::default()
599
            .with_initial_history_limit(0)
600
            .with_search_backfill_pages_per_room(0),
601
    )
602
    .await
603
    .unwrap();
604
    let running = tokio::spawn(engine.run());
605
606
    let page = mentions_page(&mut events, room_mention_room, |page| {
607
        page.entries
608
            .iter()
609
            .any(|entry| entry.key.event_id.as_str() == "$muted-direct-mention")
610
    })
611
    .await;
612
    assert_eq!(
613
        entry_ids(&page),
614
        HashSet::from(["$muted-direct-mention".to_owned()])
615
    );
616
    assert!(page.entries[0].notification_suppressed);
617
    assert!(!entry_ids(&page).contains("$unauthorized-room-mention"));
618
    assert!(!entry_ids(&page).contains("$ignored-direct-mention"));
619
    let room_ids: HashSet<_> = page
620
        .entries
621
        .iter()
622
        .map(|entry| entry.key.room_id.clone())
623
        .collect();
624
    assert!(room_ids.contains(&muted_room.to_owned()));
625
626
    shut_down(&handle, &mut events, running).await;
627
}
628
629
#[tokio::test]
630
async fn event_context_uses_independent_direction_limits_and_saved_mention_context_is_offline() {
631
    let server = MatrixMockServer::new().await;
632
    let room_id = room_id!("!mentions-context:localhost");
633
    let (client, own_user) = joined_client(&server, room_id).await;
634
    let mention_id = event_id!("$mention-context");
635
    let context_id = event_id!("$manual-context");
636
    let mention_event = EventFactory::new()
637
        .room(room_id)
638
        .text_msg("mention with saved context")
639
        .mentions(Mentions::with_user_ids([own_user.clone()]))
640
        .event_id(mention_id)
641
        .sender(&ALICE)
642
        .into_event();
643
    let context_event = EventFactory::new()
644
        .room(room_id)
645
        .text_msg("manually fetched context")
646
        .event_id(context_id)
647
        .sender(&ALICE)
648
        .into_event();
649
650
    server
651
        .mock_room_event_context()
652
        .room(room_id.to_owned())
653
        .match_event_id()
654
        .ok(RoomContextResponseTemplate::new(mention_event.clone())
655
            .start("mention-before")
656
            .end("mention-after"))
657
        .expect(1)
658
        .mount()
659
        .await;
660
    server
661
        .mock_room_event_context()
662
        .room(room_id.to_owned())
663
        .match_event_id()
664
        .ok(RoomContextResponseTemplate::new(context_event)
665
            .start("manual-before")
666
            .end("manual-after"))
667
        .expect(1)
668
        .mount()
669
        .await;
670
    mount_messages_page(&server, room_id, "b", "mention-before", 10).await;
671
    mount_messages_page(&server, room_id, "f", "mention-after", 10).await;
672
    mount_messages_page(&server, room_id, "b", "manual-before", 2).await;
673
    mount_messages_page(&server, room_id, "f", "manual-after", 3).await;
674
675
    let feed = SlidingFeed::new(
676
        room_id.to_owned(),
677
        vec![message(
678
            mention_id.as_str(),
679
            ALICE.as_str(),
680
            5_000,
681
            "mention with saved context",
682
            Some(own_user.as_str()),
683
        )],
684
    );
685
    feed.mount(&server).await;
686
    let (engine, handle, mut events) = Engine::from_client(
687
        client,
688
        EngineConfig::default()
689
            .with_initial_history_limit(0)
690
            .with_search_backfill_pages_per_room(0),
691
    )
692
    .await
693
    .unwrap();
694
    let running = tokio::spawn(engine.run());
695
696
    let captured = mentions_page_with_diagnostics(
697
        &mut events,
698
        room_id,
699
        |page| {
700
            page.entries.iter().any(|entry| {
701
                entry.key.event_id == mention_id
702
                    && matches!(&entry.capture, macltok::MentionCaptureState::Partial)
703
            })
704
        },
705
        Some(&server),
706
    )
707
    .await;
708
    let saved = handle.mention_context(room_id, mention_id).await.unwrap();
709
    assert!(saved.context.is_some());
710
    assert!(matches!(saved.state, macltok::MentionCaptureState::Partial));
711
712
    let before_saved_request_count = server
713
        .server()
714
        .received_requests()
715
        .await
716
        .unwrap()
717
        .iter()
718
        .filter(|request| request.url.path().contains("/context/$mention-context"))
719
        .count();
720
    let _ = handle.mention_context(room_id, mention_id).await.unwrap();
721
    let after_saved_request_count = server
722
        .server()
723
        .received_requests()
724
        .await
725
        .unwrap()
726
        .iter()
727
        .filter(|request| request.url.path().contains("/context/$mention-context"))
728
        .count();
729
    assert_eq!(after_saved_request_count, before_saved_request_count);
730
731
    let session = handle
732
        .event_context(room_id, context_id, 2, 3)
733
        .await
734
        .unwrap();
735
    let context = &session.context;
736
    assert_eq!(
737
        context.start_token, None,
738
        "an empty page going back is the start"
739
    );
740
    assert_eq!(context.end_token.as_deref(), Some("manual-after-next"));
741
    handle
742
        .close_event_context(&session.context_id)
743
        .await
744
        .unwrap();
745
    let closed = handle
746
        .event_context_before(&session.context_id, "manual-before-next", 5)
747
        .await
748
        .unwrap_err();
749
    assert_eq!(closed.kind, macltok::ErrorKind::EventContextNotOpen);
750
751
    let requests = server.server().received_requests().await.unwrap();
752
    let context_limits: Vec<_> = requests
753
        .iter()
754
        .filter(|request| request.url.path().contains("/context/"))
755
        .map(|request| {
756
            request
757
                .url
758
                .query_pairs()
759
                .find(|(key, _)| key == "limit")
760
                .map(|(_, value)| value.into_owned())
761
                .unwrap()
762
        })
763
        .collect();
764
    assert_eq!(context_limits, ["0", "0"]);
765
    let queries: Vec<_> = requests
766
        .iter()
767
        .filter(|request| request.url.path().ends_with("/messages"))
768
        .map(|request| {
769
            let query: std::collections::HashMap<_, _> =
770
                request.url.query_pairs().into_owned().collect();
771
            (
772
                query.get("dir").cloned().unwrap(),
773
                query.get("from").cloned().unwrap(),
774
                query
775
                    .get("limit")
776
                    .cloned()
777
                    .unwrap_or_else(|| "10".to_owned()),
778
            )
779
        })
780
        .collect();
781
    assert!(queries.contains(&("b".to_owned(), "mention-before".to_owned(), "10".to_owned())));
782
    assert!(queries.contains(&("f".to_owned(), "mention-after".to_owned(), "10".to_owned())));
783
    assert!(queries.contains(&("b".to_owned(), "manual-before".to_owned(), "2".to_owned())));
784
    assert!(queries.contains(&("f".to_owned(), "manual-after".to_owned(), "3".to_owned())));
785
    assert_eq!(captured.unread_count, 1);
786
787
    shut_down(&handle, &mut events, running).await;
788
    server.server().verify().await;
789
}
790
791
#[tokio::test]
792
async fn highlighting_keywords_are_mentions_and_quiet_ones_are_not() {
793
    let server = MatrixMockServer::new().await;
794
    let client = server.client_builder().build().await;
795
    let own_user = client.user_id().unwrap().to_owned();
796
    let room_id = room_id!("!keyword-policy:localhost");
797
    let sender = user_id!("@alice:localhost");
798
    let mut rules = Ruleset::server_default(&own_user);
799
    let notify = [
800
        Action::Notify,
801
        Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
802
    ];
803
    let mut loud = notify.to_vec();
804
    loud.push(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)));
805
    rules
806
        .content
807
        .insert(NewPatternedPushRule::new("deploy".to_owned(), "deploy".to_owned(), loud).into());
808
    rules.content.insert(
809
        NewPatternedPushRule::new("lunch".to_owned(), "lunch".to_owned(), notify.to_vec()).into(),
810
    );
811
    server
812
        .mock_sync()
813
        .ok_and_run(&client, {
814
            let own_user = own_user.clone();
815
            move |builder| {
816
                let factory = EventFactory::new().room(room_id);
817
                builder.add_global_account_data(factory.push_rules(rules));
818
                builder.add_joined_room(
819
                    JoinedRoomBuilder::new(room_id).add_state_event(factory.member(&own_user)),
820
                );
821
            }
822
        })
823
        .await;
824
    let feed = SlidingFeed::new(
825
        room_id.to_owned(),
826
        vec![
827
            message(
828
                "$keyword",
829
                sender.as_str(),
830
                1_000,
831
                "the deploy is done",
832
                None,
833
            ),
834
            message("$quiet", sender.as_str(), 2_000, "lunch anyone", None),
835
            message("$plain", sender.as_str(), 3_000, "nothing to see", None),
836
            message(
837
                "$keyword-direct",
838
                sender.as_str(),
839
                4_000,
840
                "deploy now",
841
                Some(own_user.as_str()),
842
            ),
843
            message("$own-keyword", own_user.as_str(), 5_000, "my deploy", None),
844
        ],
845
    );
846
    feed.mount(&server).await;
847
848
    let (engine, handle, mut events) = Engine::from_client(
849
        client,
850
        EngineConfig::default()
851
            .with_initial_history_limit(0)
852
            .with_search_backfill_pages_per_room(0),
853
    )
854
    .await
855
    .unwrap();
856
    let running = tokio::spawn(engine.run());
857
858
    let page = mentions_page(&mut events, room_id, |page| page.entries.len() >= 2).await;
859
    assert_eq!(
860
        entry_ids(&page),
861
        HashSet::from(["$keyword".to_owned(), "$keyword-direct".to_owned()])
862
    );
863
    let reasons: BTreeMap<_, _> = page
864
        .entries
865
        .iter()
866
        .map(|entry| (entry.key.event_id.to_string(), entry.reasons.clone()))
867
        .collect();
868
    assert_eq!(reasons["$keyword"], vec![MentionReason::Keyword]);
869
    assert_eq!(reasons["$keyword-direct"], vec![MentionReason::Direct]);
870
    assert_eq!(page.unread_count, 2);
871
    assert!(
872
        page.entries
873
            .iter()
874
            .all(|entry| !entry.notification_suppressed)
875
    );
876
877
    shut_down(&handle, &mut events, running).await;
878
}
879
880
fn actions_are(expected: Value) -> impl Fn(&Request) -> bool + Send + Sync + 'static {
881
    move |request: &Request| {
882
        serde_json::from_slice::<Value>(&request.body).is_ok_and(|body| body["actions"] == expected)
883
    }
884
}
885
886
async fn mount_keyword_rule(server: &MatrixMockServer, actions: Value) {
887
    Mock::given(method("GET"))
888
        .and(path("/_matrix/client/v3/pushrules/global/content/deploy"))
889
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
890
            "rule_id": "deploy",
891
            "pattern": "deploy",
892
            "default": false,
893
            "enabled": true,
894
            "actions": actions,
895
        })))
896
        .mount(server.server())
897
        .await;
898
}
899
900
#[tokio::test]
901
async fn adding_a_keyword_gives_its_rule_a_highlight() {
902
    let server = MatrixMockServer::new().await;
903
    let room_id = room_id!("!keyword-rule:localhost");
904
    let (client, _) = joined_client(&server, room_id).await;
905
    Mock::given(method("PUT"))
906
        .and(path("/_matrix/client/v3/pushrules/global/content/deploy"))
907
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
908
        .expect(1)
909
        .mount(server.server())
910
        .await;
911
    mount_keyword_rule(
912
        &server,
913
        json!(["notify", {"set_tweak": "sound", "value": "default"}]),
914
    )
915
    .await;
916
    Mock::given(method("PUT"))
917
        .and(path(
918
            "/_matrix/client/v3/pushrules/global/content/deploy/actions",
919
        ))
920
        .and(actions_are(json!([
921
            "notify",
922
            {"set_tweak": "sound", "value": "default"},
923
            {"set_tweak": "highlight"},
924
        ])))
925
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
926
        .expect(1)
927
        .mount(server.server())
928
        .await;
929
    SlidingFeed::new(room_id.to_owned(), Vec::new())
930
        .mount(&server)
931
        .await;
932
933
    let (engine, handle, mut events) = Engine::from_client(
934
        client,
935
        EngineConfig::default().with_search_backfill_pages_per_room(0),
936
    )
937
    .await
938
    .unwrap();
939
    let running = tokio::spawn(engine.run());
940
    handle.add_notification_keyword("deploy").await.unwrap();
941
    shut_down(&handle, &mut events, running).await;
942
    server.server().verify().await;
943
}
944
945
#[tokio::test]
946
async fn a_quiet_keyword_rule_only_gains_the_highlight() {
947
    let server = MatrixMockServer::new().await;
948
    let client = server.client_builder().build().await;
949
    let own_user = client.user_id().unwrap().to_owned();
950
    let room_id = room_id!("!quiet-keyword-rule:localhost");
951
    let mut rules = Ruleset::server_default(&own_user);
952
    rules.content.insert(
953
        NewPatternedPushRule::new("deploy".to_owned(), "deploy".to_owned(), Vec::new()).into(),
954
    );
955
    server
956
        .mock_sync()
957
        .ok_and_run(&client, {
958
            let own_user = own_user.clone();
959
            move |builder| {
960
                let factory = EventFactory::new().room(room_id);
961
                builder.add_global_account_data(factory.push_rules(rules));
962
                builder.add_joined_room(
963
                    JoinedRoomBuilder::new(room_id).add_state_event(factory.member(&own_user)),
964
                );
965
            }
966
        })
967
        .await;
968
    mount_keyword_rule(&server, json!([])).await;
969
    Mock::given(method("PUT"))
970
        .and(path(
971
            "/_matrix/client/v3/pushrules/global/content/deploy/actions",
972
        ))
973
        .and(actions_are(json!([{"set_tweak": "highlight"}])))
974
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
975
        .expect(1)
976
        .mount(server.server())
977
        .await;
978
    SlidingFeed::new(room_id.to_owned(), Vec::new())
979
        .mount(&server)
980
        .await;
981
982
    let (engine, handle, mut events) = Engine::from_client(
983
        client,
984
        EngineConfig::default().with_search_backfill_pages_per_room(0),
985
    )
986
    .await
987
    .unwrap();
988
    let running = tokio::spawn(engine.run());
989
    handle.add_notification_keyword("deploy").await.unwrap();
990
    shut_down(&handle, &mut events, running).await;
991
    server.server().verify().await;
992
}
993
994
#[cfg(feature = "sqlite")]
995
#[tokio::test]
996
async fn sqlite_mentions_survive_engine_shutdown_and_session_resume() {
997
    static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);
998
999
    let server = MatrixMockServer::new().await;
1000
    server
1001
        .mock_versions()
1002
        .with_simplified_sliding_sync()
1003
        .ok()
1004
        .mount()
1005
        .await;
1006
    server.mock_login().ok().mount().await;
1007
1008
    let root = std::env::temp_dir().join(format!(
1009
        "macltok-mentions-inbox-{}-{}",
1010
        std::process::id(),
1011
        NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)
1012
    ));
1013
    let store_path = root.join("store");
1014
    let sessions_path = root.join("records").join("session");
1015
    let sessions = Arc::new(EncryptedSessionStore::file(
1016
        &sessions_path,
1017
        StoreConfig::sqlite(&store_path, "mentions inbox test passphrase"),
1018
    ));
1019
    let room_id = room_id!("!mentions-resume:matrix.org");
1020
    let own_user = user_id!("@cheeky_monkey:matrix.org");
1021
    let feed = SlidingFeed::new(
1022
        room_id.to_owned(),
1023
        vec![message(
1024
            "$durable-mention",
1025
            "@alice:matrix.org",
1026
            7_000,
1027
            "persist this mention",
1028
            Some(own_user.as_str()),
1029
        )],
1030
    );
1031
    feed.set_required_state(
1032
        room_id,
1033
        vec![json!({
1034
            "type": "m.room.member",
1035
            "event_id": "$joined-member",
1036
            "sender": own_user,
1037
            "state_key": own_user,
1038
            "origin_server_ts": 1,
1039
            "content": { "membership": "join" },
1040
        })],
1041
    );
1042
    feed.mount(&server).await;
1043
1044
    let config = EngineConfig::default()
1045
        .with_initial_history_limit(0)
1046
        .with_search_backfill_pages_per_room(0);
1047
    let session = Session::login_password(
1048
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
1049
        sessions.clone(),
1050
    )
1051
    .await
1052
    .unwrap();
1053
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
1054
    let running = tokio::spawn(engine.run());
1055
    let first = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
1056
    assert_eq!(
1057
        entry_ids(&first),
1058
        HashSet::from(["$durable-mention".to_owned()])
1059
    );
1060
    shut_down(&handle, &mut events, running).await;
1061
    drop(handle);
1062
1063
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
1064
        panic!("the saved session did not resume");
1065
    };
1066
    let (engine, handle, mut events) = session.build(config).await.unwrap();
1067
    let running = tokio::spawn(engine.run());
1068
    receive_until(&mut events, |event| {
1069
        matches!(event, Event::Authenticated { .. })
1070
    })
1071
    .await;
1072
    let resumed = handle.mentions_page(None, 50).await.unwrap();
1073
    assert_eq!(
1074
        entry_ids(&resumed),
1075
        HashSet::from(["$durable-mention".to_owned()])
1076
    );
1077
    assert_eq!(resumed.unread_count, 1);
1078
    shut_down(&handle, &mut events, running).await;
1079
    drop(handle);
1080
1081
    sessions.discard().await.unwrap();
1082
    std::fs::remove_dir_all(root).unwrap();
1083
}
1084
1085
#[cfg(feature = "serde")]
1086
#[test]
1087
fn mentions_command_and_page_round_trip_through_serde() {
1088
    let command = MentionsCommand::ReadThrough(MentionsBoundary {
1089
        account: user_id!("@alice:example.org").to_owned(),
1090
        revision: 7,
1091
    });
1092
    let encoded_command = serde_json::to_value(&command).unwrap();
1093
    let decoded_command: MentionsCommand = serde_json::from_value(encoded_command).unwrap();
1094
    assert_eq!(decoded_command, command);
1095
1096
    let page = MentionsPage {
1097
        entries: Vec::new(),
1098
        next_cursor: Some("opaque cursor".to_owned()),
1099
        end_reached: false,
1100
        coverage: MentionCoverage::default(),
1101
        unread_count: 0,
1102
        revision: 7,
1103
        boundary: MentionsBoundary {
1104
            account: user_id!("@alice:example.org").to_owned(),
1105
            revision: 7,
1106
        },
1107
    };
1108
    let encoded_page = serde_json::to_value(&page).unwrap();
1109
    let decoded_page: MentionsPage = serde_json::from_value(encoded_page).unwrap();
1110
    assert_eq!(decoded_page, page);
1111
}
1112
1113
#[tokio::test]
1114
async fn inbox_rows_carry_sender_profiles_and_never_leave_replies_pending() {
1115
    let server = MatrixMockServer::new().await;
1116
    let room_id = room_id!("!inbox-profiles:localhost");
1117
    let (client, own_user) = joined_client(&server, room_id).await;
1118
    let sender = user_id!("@alice:localhost");
1119
    let feed = SlidingFeed::new(
1120
        room_id.to_owned(),
1121
        vec![message("$parent", sender.as_str(), 1_000, "parent", None)],
1122
    );
1123
    feed.set_required_state(
1124
        room_id,
1125
        vec![json!({
1126
            "type": "m.room.member",
1127
            "state_key": sender,
1128
            "sender": sender,
1129
            "event_id": "$alice-member",
1130
            "origin_server_ts": 1,
1131
            "content": { "membership": "join", "displayname": "Alice" },
1132
        })],
1133
    );
1134
    feed.mount(&server).await;
1135
    let (engine, handle, mut events) = Engine::from_client(
1136
        client,
1137
        EngineConfig::default()
1138
            .with_initial_history_limit(0)
1139
            .with_search_backfill_pages_per_room(0),
1140
    )
1141
    .await
1142
    .unwrap();
1143
    let running = tokio::spawn(engine.run());
1144
    wait_for_requests(&feed, 2).await;
1145
1146
    let reply = |event_id: &str, target: &str, timestamp| {
1147
        let mut event = message(
1148
            event_id,
1149
            sender.as_str(),
1150
            timestamp,
1151
            "reply",
1152
            Some(own_user.as_str()),
1153
        );
1154
        event["content"]["m.relates_to"] = json!({ "m.in_reply_to": { "event_id": target } });
1155
        event
1156
    };
1157
    let answer = reply("$answer", "$parent", 2_000);
1158
    let orphan = reply("$orphan", "$missing", 2_100);
1159
    for event in [&answer, &orphan] {
1160
        let mut event = event.clone();
1161
        event["room_id"] = json!(room_id);
1162
        server
1163
            .mock_room_event_context()
1164
            .room(room_id.to_owned())
1165
            .match_event_id()
1166
            .ok(RoomContextResponseTemplate::new(
1167
                matrix_sdk::deserialized_responses::TimelineEvent::from_plaintext(
1168
                    ruma::serde::Raw::new(&event).unwrap().cast_unchecked(),
1169
                ),
1170
            ))
1171
            .mount()
1172
            .await;
1173
    }
1174
    feed.replace(room_id, vec![answer, orphan]);
1175
    let settled = |page: &MentionsPage| {
1176
        page.entries.len() == 2
1177
            && page.entries.iter().all(|entry| {
1178
                matches!(&entry.event, macltok::TimelineItem::Event(event)
1179
                if event.sender.display_name.is_some()
1180
                    && event.reply_to.as_ref().is_some_and(|reply| {
1181
                        !matches!(reply.details, macltok::ReplyDetails::Pending)
1182
                    }))
1183
            })
1184
    };
1185
    let page = mentions_page(&mut events, room_id, |page| {
1186
        settled(page)
1187
            && page.entries.iter().any(|entry| {
1188
                matches!(&entry.event, macltok::TimelineItem::Event(event)
1189
                    if matches!(event.reply_to.as_ref().unwrap().details,
1190
                        macltok::ReplyDetails::Ready { .. }))
1191
            })
1192
    })
1193
    .await;
1194
    for entry in &page.entries {
1195
        let macltok::TimelineItem::Event(event) = &entry.event else {
1196
            panic!("a mention should be an event");
1197
        };
1198
        assert_eq!(event.sender.display_name.as_deref(), Some("Alice"));
1199
        let details = &event
1200
            .reply_to
1201
            .as_ref()
1202
            .expect("both mentions reply")
1203
            .details;
1204
        match entry.key.event_id.as_str() {
1205
            "$answer" => assert!(matches!(details, macltok::ReplyDetails::Ready { .. })),
1206
            _ => assert_eq!(details, &macltok::ReplyDetails::Unavailable),
1207
        }
1208
    }
1209
    shut_down(&handle, &mut events, running).await;
1210
}
1211
1212
#[cfg(feature = "sqlite")]
1213
#[tokio::test]
1214
async fn a_resumed_inbox_with_threaded_replies_still_answers_a_page() {
1215
    static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);
1216
1217
    let server = MatrixMockServer::new().await;
1218
    server
1219
        .mock_versions()
1220
        .with_simplified_sliding_sync()
1221
        .ok()
1222
        .mount()
1223
        .await;
1224
    server.mock_login().ok().mount().await;
1225
    let root = std::env::temp_dir().join(format!(
1226
        "macltok-mentions-threads-{}-{}",
1227
        std::process::id(),
1228
        NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)
1229
    ));
1230
    let sessions = Arc::new(EncryptedSessionStore::file(
1231
        root.join("records").join("session"),
1232
        StoreConfig::sqlite(root.join("store"), "mentions thread test passphrase"),
1233
    ));
1234
    let room_id = room_id!("!mentions-threads:matrix.org");
1235
    let own_user = user_id!("@cheeky_monkey:matrix.org");
1236
    let alice = "@alice:matrix.org";
1237
    let threaded = |event_id: &str, timestamp| {
1238
        let mut event = message(
1239
            event_id,
1240
            alice,
1241
            timestamp,
1242
            "threaded mention",
1243
            Some(own_user.as_str()),
1244
        );
1245
        event["content"]["m.relates_to"] = json!({
1246
            "rel_type": "m.thread",
1247
            "event_id": "$root",
1248
            "is_falling_back": true,
1249
            "m.in_reply_to": { "event_id": "$root" },
1250
        });
1251
        event
1252
    };
1253
    let feed = SlidingFeed::new(
1254
        room_id.to_owned(),
1255
        vec![
1256
            message("$root", alice, 6_000, "thread root", None),
1257
            threaded("$thread-a", 7_000),
1258
            threaded("$thread-b", 7_100),
1259
            message(
1260
                "$plain",
1261
                alice,
1262
                7_200,
1263
                "plain mention",
1264
                Some(own_user.as_str()),
1265
            ),
1266
        ],
1267
    );
1268
    feed.set_required_state(
1269
        room_id,
1270
        vec![json!({
1271
            "type": "m.room.member",
1272
            "event_id": "$joined-member",
1273
            "sender": own_user,
1274
            "state_key": own_user,
1275
            "origin_server_ts": 1,
1276
            "content": { "membership": "join" },
1277
        })],
1278
    );
1279
    feed.mount(&server).await;
1280
1281
    let config = EngineConfig::default()
1282
        .with_initial_history_limit(0)
1283
        .with_search_backfill_pages_per_room(0);
1284
    let session = Session::login_password(
1285
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
1286
        sessions.clone(),
1287
    )
1288
    .await
1289
    .unwrap();
1290
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
1291
    let running = tokio::spawn(engine.run());
1292
    mentions_page(&mut events, room_id, |page| page.entries.len() == 3).await;
1293
    shut_down(&handle, &mut events, running).await;
1294
    drop(handle);
1295
1296
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
1297
        panic!("the saved session did not resume");
1298
    };
1299
    let (engine, handle, mut events) = session.build(config).await.unwrap();
1300
    let running = tokio::spawn(engine.run());
1301
    receive_until(&mut events, |event| {
1302
        matches!(event, Event::Authenticated { .. })
1303
    })
1304
    .await;
1305
    wait_for_requests(&feed, 4).await;
1306
    let resumed = tokio::time::timeout(Duration::from_secs(5), handle.mentions_page(None, 50))
1307
        .await
1308
        .expect("the resumed inbox did not answer a page")
1309
        .unwrap();
1310
    assert_eq!(resumed.entries.len(), 3);
1311
    shut_down(&handle, &mut events, running).await;
1312
    drop(handle);
1313
1314
    sessions.discard().await.unwrap();
1315
    std::fs::remove_dir_all(root).unwrap();
1316
}
1317
1318
#[tokio::test]
1319
async fn a_resumed_room_with_only_memberships_still_opens() {
1320
    static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);
1321
1322
    let server = MatrixMockServer::new().await;
1323
    server
1324
        .mock_versions()
1325
        .with_simplified_sliding_sync()
1326
        .ok()
1327
        .mount()
1328
        .await;
1329
    server.mock_login().ok().mount().await;
1330
    server
1331
        .mock_room_messages()
1332
        .ok(matrix_sdk::test_utils::mocks::RoomMessagesResponseTemplate::default())
1333
        .mount()
1334
        .await;
1335
    let root = std::env::temp_dir().join(format!(
1336
        "macltok-membership-room-{}-{}",
1337
        std::process::id(),
1338
        NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)
1339
    ));
1340
    let sessions = Arc::new(EncryptedSessionStore::file(
1341
        root.join("records").join("session"),
1342
        StoreConfig::sqlite(root.join("store"), "membership room test passphrase"),
1343
    ));
1344
    let room_id = room_id!("!memberships-resume:matrix.org");
1345
    let own_user = user_id!("@cheeky_monkey:matrix.org");
1346
    let member = |event_id: &str, user: &str, ts: u64| {
1347
        json!({
1348
            "type": "m.room.member",
1349
            "event_id": event_id,
1350
            "sender": user,
1351
            "state_key": user,
1352
            "origin_server_ts": ts,
1353
            "content": { "membership": "join" },
1354
        })
1355
    };
1356
    let feed = SlidingFeed::new(
1357
        room_id.to_owned(),
1358
        vec![
1359
            json!({
1360
                "type": "m.room.create",
1361
                "event_id": "$created",
1362
                "sender": own_user,
1363
                "state_key": "",
1364
                "origin_server_ts": 1,
1365
                "content": { "room_version": "11" },
1366
            }),
1367
            member("$own-joined", own_user.as_str(), 2),
1368
            member("$alice-joined", "@alice:matrix.org", 3),
1369
        ],
1370
    );
1371
    feed.set_required_state(room_id, vec![member("$own-joined", own_user.as_str(), 2)]);
1372
    feed.mount(&server).await;
1373
1374
    let config = EngineConfig::default()
1375
        .with_initial_history_limit(0)
1376
        .with_search_backfill_pages_per_room(0);
1377
    let session = Session::login_password(
1378
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
1379
        sessions.clone(),
1380
    )
1381
    .await
1382
    .unwrap();
1383
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
1384
    let running = tokio::spawn(engine.run());
1385
    tokio::time::timeout(Duration::from_secs(10), async {
1386
        while !handle
1387
            .rooms()
1388
            .await
1389
            .iter()
1390
            .any(|room| room.room_id == room_id)
1391
        {
1392
            tokio::time::sleep(Duration::from_millis(20)).await;
1393
        }
1394
    })
1395
    .await
1396
    .expect("the room never synced");
1397
    tokio::time::timeout(
1398
        Duration::from_secs(10),
1399
        handle.open_room(room_id.to_owned()),
1400
    )
1401
    .await
1402
    .expect("opening the room never answered before the restart")
1403
    .unwrap();
1404
    shut_down(&handle, &mut events, running).await;
1405
    drop(handle);
1406
1407
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
1408
        panic!("the saved session did not resume");
1409
    };
1410
    let (engine, handle, mut events) = session.build(config).await.unwrap();
1411
    let running = tokio::spawn(engine.run());
1412
    receive_until(&mut events, |event| {
1413
        matches!(event, Event::Authenticated { .. })
1414
    })
1415
    .await;
1416
    tokio::time::timeout(
1417
        Duration::from_secs(10),
1418
        handle.open_room(room_id.to_owned()),
1419
    )
1420
    .await
1421
    .expect("opening the resumed room never answered")
1422
    .unwrap();
1423
    shut_down(&handle, &mut events, running).await;
1424
    drop(handle);
1425
1426
    sessions.discard().await.unwrap();
1427
    std::fs::remove_dir_all(root).unwrap();
1428
}