atagen

macltok

public
Star0
Watch0
7c7832147a2ef1974fca8cc6abea628f157a4649
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/mentions_inbox.rsRust882 lines29.0 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, 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::{EventMatchConditionData, NewConditionalPushRule, PushCondition, Ruleset};
22
use serde_json::{Value, json};
23
#[cfg(feature = "sqlite")]
24
use support::receive_until;
25
use support::shut_down;
26
use wiremock::{
27
    Mock, Request, Respond, ResponseTemplate,
28
    matchers::{method, path, path_regex, query_param},
29
};
30
31
#[cfg(feature = "serde")]
32
use macltok::{MentionCoverage, MentionsBoundary, MentionsCommand};
33
34
const SLIDING_SYNC_PATH: &str = "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync";
35
36
struct SlidingFeed {
37
    rooms: Arc<Mutex<BTreeMap<OwnedRoomId, Vec<Value>>>>,
38
    required_state: Arc<Mutex<BTreeMap<OwnedRoomId, Vec<Value>>>>,
39
    requests: Arc<AtomicUsize>,
40
}
41
42
impl SlidingFeed {
43
    fn new(room_id: OwnedRoomId, events: Vec<Value>) -> Self {
44
        Self {
45
            rooms: Arc::new(Mutex::new(BTreeMap::from([(room_id, events)]))),
46
            required_state: Arc::new(Mutex::new(BTreeMap::new())),
47
            requests: Arc::new(AtomicUsize::new(0)),
48
        }
49
    }
50
51
    fn replace(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
52
        self.rooms
53
            .lock()
54
            .unwrap()
55
            .insert(room_id.to_owned(), events);
56
    }
57
58
    fn add_room(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
59
        self.rooms
60
            .lock()
61
            .unwrap()
62
            .insert(room_id.to_owned(), events);
63
    }
64
65
    fn set_required_state(&self, room_id: &ruma::RoomId, events: Vec<Value>) {
66
        self.required_state
67
            .lock()
68
            .unwrap()
69
            .insert(room_id.to_owned(), events);
70
    }
71
72
    async fn mount(&self, server: &MatrixMockServer) {
73
        Mock::given(method("POST"))
74
            .and(path(SLIDING_SYNC_PATH))
75
            .respond_with(self.clone())
76
            .mount(server.server())
77
            .await;
78
    }
79
}
80
81
impl Clone for SlidingFeed {
82
    fn clone(&self) -> Self {
83
        Self {
84
            rooms: self.rooms.clone(),
85
            required_state: self.required_state.clone(),
86
            requests: self.requests.clone(),
87
        }
88
    }
89
}
90
91
impl Respond for SlidingFeed {
92
    fn respond(&self, _request: &Request) -> ResponseTemplate {
93
        let request = self.requests.fetch_add(1, Ordering::AcqRel);
94
        let required_state = self.required_state.lock().unwrap().clone();
95
        let rooms: serde_json::Map<String, Value> = self
96
            .rooms
97
            .lock()
98
            .unwrap()
99
            .iter()
100
            .map(|(room_id, events)| {
101
                (
102
                    room_id.to_string(),
103
                    json!({
104
                        "initial": request == 0,
105
                        "required_state": required_state
106
                            .get(room_id)
107
                            .cloned()
108
                            .unwrap_or_default(),
109
                        "timeline": events,
110
                    }),
111
                )
112
            })
113
            .collect();
114
        ResponseTemplate::new(200)
115
            .set_body_json(json!({
116
                "txn_id": null,
117
                "pos": format!("mentions-{request}"),
118
                "lists": {},
119
                "rooms": rooms,
120
                "extensions": {},
121
            }))
122
            .set_delay(Duration::from_millis(20))
123
    }
124
}
125
126
async fn joined_client(server: &MatrixMockServer, room_id: &ruma::RoomId) -> (Client, OwnedUserId) {
127
    let client = server.client_builder().build().await;
128
    let own_user = client.user_id().unwrap().to_owned();
129
    let room_id = room_id.to_owned();
130
    server
131
        .mock_sync()
132
        .ok_and_run(&client, {
133
            let own_user = own_user.clone();
134
            move |builder| {
135
                let factory = EventFactory::new().room(&room_id);
136
                builder.add_global_account_data(
137
                    factory.push_rules(Ruleset::server_default(&own_user)),
138
                );
139
                builder.add_joined_room(
140
                    JoinedRoomBuilder::new(&room_id).add_state_event(factory.member(&own_user)),
141
                );
142
            }
143
        })
144
        .await;
145
    (client, own_user)
146
}
147
148
fn message(
149
    event_id: &str,
150
    sender: &str,
151
    timestamp: u64,
152
    body: &str,
153
    mentioned: Option<&str>,
154
) -> Value {
155
    let mut content = json!({ "msgtype": "m.text", "body": body });
156
    if let Some(mentioned) = mentioned {
157
        content["m.mentions"] = json!({ "user_ids": [mentioned] });
158
    }
159
    json!({
160
        "type": "m.room.message",
161
        "event_id": event_id,
162
        "sender": sender,
163
        "origin_server_ts": timestamp,
164
        "content": content,
165
    })
166
}
167
168
fn room_mention(event_id: &str, sender: &str, timestamp: u64) -> Value {
169
    let mut event = message(event_id, sender, timestamp, "room mention", None);
170
    event["content"]["m.mentions"] = json!({ "room": true });
171
    event
172
}
173
174
async fn mentions_page(
175
    events: &mut macltok::EventStream,
176
    room_id: &ruma::RoomId,
177
    predicate: impl Fn(&MentionsPage) -> bool,
178
) -> MentionsPage {
179
    mentions_page_with_diagnostics(events, room_id, predicate, None).await
180
}
181
182
async fn mentions_page_with_diagnostics(
183
    events: &mut macltok::EventStream,
184
    room_id: &ruma::RoomId,
185
    predicate: impl Fn(&MentionsPage) -> bool,
186
    diagnostic_server: Option<&MatrixMockServer>,
187
) -> MentionsPage {
188
    let mut last_page = None;
189
    let result = tokio::time::timeout(Duration::from_secs(5), async {
190
        while let Some(event) = events.recv().await {
191
            match event {
192
                Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id => {
193
                    panic!("the unopened room produced a timeline reset");
194
                }
195
                Event::MentionsChanged(update) => {
196
                    let snapshot = update.snapshot;
197
                    let matched = predicate(&snapshot);
198
                    last_page = Some(snapshot.clone());
199
                    if matched {
200
                        return snapshot;
201
                    }
202
                }
203
                _ => {}
204
            }
205
        }
206
        panic!("event stream closed before the expected mentions page");
207
    })
208
    .await;
209
210
    match result {
211
        Ok(page) => page,
212
        Err(_) => {
213
            let last_state = last_page.map(|page: MentionsPage| {
214
                (
215
                    page.revision,
216
                    page.unread_count,
217
                    page.entries
218
                        .iter()
219
                        .map(|entry| {
220
                            (
221
                                entry.key.room_id.to_string(),
222
                                entry.key.event_id.to_string(),
223
                                entry.capture.clone(),
224
                                entry.revision,
225
                            )
226
                        })
227
                        .collect::<Vec<_>>(),
228
                )
229
            });
230
            let requests = if let Some(server) = diagnostic_server {
231
                server.server().received_requests().await.map(|requests| {
232
                    requests
233
                        .iter()
234
                        .filter(|request| {
235
                            let path = request.url.path();
236
                            path.contains(room_id.as_str())
237
                                && (path.contains("/context/") || path.ends_with("/messages"))
238
                        })
239
                        .map(|request| format!("{} {}", request.method, request.url))
240
                        .collect::<Vec<_>>()
241
                })
242
            } else {
243
                None
244
            };
245
            panic!(
246
                "expected mentions update did not arrive, last state {last_state:?}, requests {requests:?}"
247
            );
248
        }
249
    }
250
}
251
252
fn entry_ids(page: &MentionsPage) -> HashSet<String> {
253
    page.entries
254
        .iter()
255
        .map(|entry| entry.key.event_id.to_string())
256
        .collect()
257
}
258
259
async fn wait_for_requests(feed: &SlidingFeed, minimum: usize) {
260
    tokio::time::timeout(Duration::from_secs(5), async {
261
        while feed.requests.load(Ordering::Acquire) < minimum {
262
            tokio::task::yield_now().await;
263
        }
264
    })
265
    .await
266
    .expect("the sliding sync feed did not receive another request");
267
}
268
269
async fn mount_messages_page(
270
    server: &MatrixMockServer,
271
    room_id: &ruma::RoomId,
272
    direction: &str,
273
    token: &str,
274
    limit: u16,
275
) {
276
    Mock::given(method("GET"))
277
        .and(path_regex(format!(
278
            r"^/_matrix/client/v3/rooms/{room_id}/messages$"
279
        )))
280
        .and(query_param("dir", direction))
281
        .and(query_param("from", token))
282
        .and(move |request: &Request| {
283
            request
284
                .url
285
                .query_pairs()
286
                .find(|(key, _)| key == "limit")
287
                .map_or(limit == 10, |(_, value)| value == limit.to_string())
288
        })
289
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
290
            "start": token,
291
            "end": format!("{token}-next"),
292
            "chunk": [],
293
            "state": [],
294
        })))
295
        .expect(1)
296
        .mount(server.server())
297
        .await;
298
}
299
300
#[tokio::test]
301
async fn unopened_room_mentions_are_deduplicated_and_read_through_uses_a_revision_boundary() {
302
    let server = MatrixMockServer::new().await;
303
    let room_id = room_id!("!mentions-inbox:localhost");
304
    let (client, own_user) = joined_client(&server, room_id).await;
305
    let sender = user_id!("@alice:localhost");
306
    let initial = vec![
307
        message(
308
            "$mention",
309
            sender.as_str(),
310
            2_000,
311
            "direct mention",
312
            Some(own_user.as_str()),
313
        ),
314
        message(
315
            "$own-mention",
316
            own_user.as_str(),
317
            2_100,
318
            "own event",
319
            Some(own_user.as_str()),
320
        ),
321
        message(
322
            "$unrelated",
323
            sender.as_str(),
324
            2_200,
325
            "ordinary message",
326
            None,
327
        ),
328
    ];
329
    let feed = SlidingFeed::new(room_id.to_owned(), initial.clone());
330
    feed.mount(&server).await;
331
332
    let (engine, handle, mut events) = Engine::from_client(
333
        client,
334
        EngineConfig::default().with_initial_history_limit(0),
335
    )
336
    .await
337
    .unwrap();
338
    let running = tokio::spawn(engine.run());
339
340
    let first = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
341
    assert_eq!(entry_ids(&first), HashSet::from(["$mention".to_owned()]));
342
    assert_eq!(first.unread_count, 1);
343
    let first_boundary = first.boundary.clone();
344
345
    wait_for_requests(&feed, 3).await;
346
    let replayed = handle.mentions_page(None, 50).await.unwrap();
347
    assert_eq!(entry_ids(&replayed), HashSet::from(["$mention".to_owned()]));
348
349
    let key = replayed.entries[0].key.clone();
350
    handle
351
        .set_mentions_read(vec![key.clone()], true)
352
        .await
353
        .unwrap();
354
    let read = handle.mentions_page(None, 50).await.unwrap();
355
    assert_eq!(read.unread_count, 0);
356
357
    feed.replace(
358
        room_id,
359
        [
360
            initial,
361
            vec![message(
362
                "$older-mention",
363
                sender.as_str(),
364
                1_000,
365
                "older direct mention",
366
                Some(own_user.as_str()),
367
            )],
368
        ]
369
        .concat(),
370
    );
371
    let updated = mentions_page(&mut events, room_id, |page| page.entries.len() == 2).await;
372
    assert_eq!(
373
        entry_ids(&updated),
374
        HashSet::from(["$mention".to_owned(), "$older-mention".to_owned(),])
375
    );
376
377
    handle
378
        .mark_mentions_read_through(first_boundary)
379
        .await
380
        .unwrap();
381
    let after_boundary = handle.mentions_page(None, 50).await.unwrap();
382
    let read_states: Vec<_> = after_boundary
383
        .entries
384
        .iter()
385
        .map(|entry| (entry.key.event_id.to_string(), entry.read))
386
        .collect();
387
    assert!(read_states.contains(&("$mention".to_owned(), true)));
388
    assert!(read_states.contains(&("$older-mention".to_owned(), false)));
389
    assert_eq!(after_boundary.unread_count, 1);
390
391
    shut_down(&handle, &mut events, running).await;
392
}
393
394
#[tokio::test]
395
async fn edits_can_remove_and_restore_a_mention_until_redaction_removes_its_target() {
396
    let server = MatrixMockServer::new().await;
397
    let room_id = room_id!("!mentions-edits:localhost");
398
    let (client, own_user) = joined_client(&server, room_id).await;
399
    let sender = user_id!("@alice:localhost");
400
    let original = message(
401
        "$edited-target",
402
        sender.as_str(),
403
        1_000,
404
        "original mention",
405
        Some(own_user.as_str()),
406
    );
407
    let removal = json!({
408
        "type": "m.room.message",
409
        "event_id": "$remove-edit",
410
        "sender": sender,
411
        "origin_server_ts": 2_000,
412
        "content": {
413
            "msgtype": "m.text",
414
            "body": "* edited without a mention",
415
            "m.relates_to": { "rel_type": "m.replace", "event_id": "$edited-target" },
416
            "m.new_content": { "msgtype": "m.text", "body": "edited without a mention" },
417
        },
418
    });
419
    let addition = json!({
420
        "type": "m.room.message",
421
        "event_id": "$add-edit",
422
        "sender": sender,
423
        "origin_server_ts": 3_000,
424
        "content": {
425
            "msgtype": "m.text",
426
            "body": "* edited to mention you",
427
            "m.relates_to": { "rel_type": "m.replace", "event_id": "$edited-target" },
428
            "m.new_content": {
429
                "msgtype": "m.text",
430
                "body": "edited to mention you",
431
                "m.mentions": { "user_ids": [own_user.as_str()] },
432
            },
433
        },
434
    });
435
    let redaction = json!({
436
        "type": "m.room.redaction",
437
        "event_id": "$redaction",
438
        "sender": sender,
439
        "origin_server_ts": 4_000,
440
        "redacts": "$edited-target",
441
        "content": {},
442
    });
443
    let feed = SlidingFeed::new(room_id.to_owned(), vec![original.clone()]);
444
    feed.mount(&server).await;
445
446
    let (engine, handle, mut events) = Engine::from_client(
447
        client,
448
        EngineConfig::default().with_initial_history_limit(0),
449
    )
450
    .await
451
    .unwrap();
452
    let running = tokio::spawn(engine.run());
453
454
    let discovered = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
455
    assert_eq!(
456
        entry_ids(&discovered),
457
        HashSet::from(["$edited-target".to_owned()])
458
    );
459
460
    feed.replace(room_id, vec![original.clone(), removal.clone()]);
461
    let removed = mentions_page(&mut events, room_id, |page| {
462
        page.revision > discovered.revision && page.entries.is_empty()
463
    })
464
    .await;
465
    assert!(removed.entries.is_empty());
466
    assert_eq!(
467
        handle
468
            .mention_context(room_id, ruma::event_id!("$edited-target"))
469
            .await
470
            .unwrap_err()
471
            .kind,
472
        macltok::ErrorKind::EventNotFound
473
    );
474
475
    feed.replace(room_id, vec![original.clone(), removal, addition]);
476
    let restored = mentions_page(&mut events, room_id, |page| {
477
        page.revision > removed.revision && entry_ids(page).contains("$edited-target")
478
    })
479
    .await;
480
    assert_eq!(
481
        entry_ids(&restored),
482
        HashSet::from(["$edited-target".to_owned()])
483
    );
484
485
    feed.replace(room_id, vec![original, redaction]);
486
    let redacted = mentions_page(&mut events, room_id, |page| {
487
        page.revision > restored.revision && page.entries.is_empty()
488
    })
489
    .await;
490
    assert!(redacted.entries.is_empty());
491
    assert_eq!(
492
        handle
493
            .mention_context(room_id, ruma::event_id!("$edited-target"))
494
            .await
495
            .unwrap_err()
496
            .kind,
497
        macltok::ErrorKind::EventNotFound
498
    );
499
500
    shut_down(&handle, &mut events, running).await;
501
}
502
503
#[tokio::test]
504
async fn room_mentions_require_power_while_muted_direct_mentions_remain_and_ignored_senders_drop() {
505
    let server = MatrixMockServer::new().await;
506
    let client = server.client_builder().build().await;
507
    let own_user = client.user_id().unwrap().to_owned();
508
    let room_mention_room = room_id!("!room-mention-policy:localhost");
509
    let muted_room = room_id!("!muted-mention-policy:localhost");
510
    let ignored_sender = user_id!("@ignored:localhost");
511
    let sender = user_id!("@alice:localhost");
512
    let mut rules = Ruleset::server_default(&own_user);
513
    rules.override_.shift_insert(
514
        1,
515
        NewConditionalPushRule::new(
516
            "mute-inbox-room".to_owned(),
517
            vec![PushCondition::EventMatch(EventMatchConditionData::new(
518
                "room_id".to_owned(),
519
                muted_room.to_string(),
520
            ))],
521
            Vec::new(),
522
        )
523
        .into(),
524
    );
525
    server
526
        .mock_sync()
527
        .ok_and_run(&client, {
528
            let own_user = own_user.clone();
529
            let ignored_sender = ignored_sender.to_owned();
530
            move |builder| {
531
                let factory = EventFactory::new();
532
                builder.add_global_account_data(factory.push_rules(rules));
533
                builder.add_global_account_data(factory.ignored_user_list([ignored_sender]));
534
                builder.add_joined_room(
535
                    JoinedRoomBuilder::new(room_mention_room)
536
                        .add_state_event(
537
                            EventFactory::new()
538
                                .room(room_mention_room)
539
                                .member(&own_user),
540
                        )
541
                        .add_state_event(
542
                            EventFactory::new()
543
                                .room(room_mention_room)
544
                                .default_power_levels()
545
                                .sender(&own_user),
546
                        ),
547
                );
548
                builder.add_joined_room(
549
                    JoinedRoomBuilder::new(muted_room)
550
                        .add_state_event(EventFactory::new().room(muted_room).member(&own_user))
551
                        .add_state_event(
552
                            EventFactory::new()
553
                                .room(muted_room)
554
                                .default_power_levels()
555
                                .sender(&own_user),
556
                        ),
557
                );
558
            }
559
        })
560
        .await;
561
562
    let feed = SlidingFeed::new(
563
        room_mention_room.to_owned(),
564
        vec![room_mention(
565
            "$unauthorized-room-mention",
566
            sender.as_str(),
567
            3_000,
568
        )],
569
    );
570
    feed.mount(&server).await;
571
    let muted_events = vec![message(
572
        "$muted-direct-mention",
573
        sender.as_str(),
574
        2_000,
575
        "muted direct mention",
576
        Some(own_user.as_str()),
577
    )];
578
    let ignored = message(
579
        "$ignored-direct-mention",
580
        ignored_sender.as_str(),
581
        1_000,
582
        "ignored direct mention",
583
        Some(own_user.as_str()),
584
    );
585
    let mut muted_events = muted_events;
586
    muted_events.push(ignored);
587
    feed.add_room(muted_room, muted_events);
588
589
    let (engine, handle, mut events) = Engine::from_client(
590
        client,
591
        EngineConfig::default().with_initial_history_limit(0),
592
    )
593
    .await
594
    .unwrap();
595
    let running = tokio::spawn(engine.run());
596
597
    let page = mentions_page(&mut events, room_mention_room, |page| {
598
        page.entries
599
            .iter()
600
            .any(|entry| entry.key.event_id.as_str() == "$muted-direct-mention")
601
    })
602
    .await;
603
    assert_eq!(
604
        entry_ids(&page),
605
        HashSet::from(["$muted-direct-mention".to_owned()])
606
    );
607
    assert!(page.entries[0].notification_suppressed);
608
    assert!(!entry_ids(&page).contains("$unauthorized-room-mention"));
609
    assert!(!entry_ids(&page).contains("$ignored-direct-mention"));
610
    let room_ids: HashSet<_> = page
611
        .entries
612
        .iter()
613
        .map(|entry| entry.key.room_id.clone())
614
        .collect();
615
    assert!(room_ids.contains(&muted_room.to_owned()));
616
617
    shut_down(&handle, &mut events, running).await;
618
}
619
620
#[tokio::test]
621
async fn event_context_uses_independent_direction_limits_and_saved_mention_context_is_offline() {
622
    let server = MatrixMockServer::new().await;
623
    let room_id = room_id!("!mentions-context:localhost");
624
    let (client, own_user) = joined_client(&server, room_id).await;
625
    let mention_id = event_id!("$mention-context");
626
    let context_id = event_id!("$manual-context");
627
    let mention_event = EventFactory::new()
628
        .room(room_id)
629
        .text_msg("mention with saved context")
630
        .mentions(Mentions::with_user_ids([own_user.clone()]))
631
        .event_id(mention_id)
632
        .sender(&ALICE)
633
        .into_event();
634
    let context_event = EventFactory::new()
635
        .room(room_id)
636
        .text_msg("manually fetched context")
637
        .event_id(context_id)
638
        .sender(&ALICE)
639
        .into_event();
640
641
    server
642
        .mock_room_event_context()
643
        .room(room_id.to_owned())
644
        .match_event_id()
645
        .ok(RoomContextResponseTemplate::new(mention_event.clone())
646
            .start("mention-before")
647
            .end("mention-after"))
648
        .expect(1)
649
        .mount()
650
        .await;
651
    server
652
        .mock_room_event_context()
653
        .room(room_id.to_owned())
654
        .match_event_id()
655
        .ok(RoomContextResponseTemplate::new(context_event)
656
            .start("manual-before")
657
            .end("manual-after"))
658
        .expect(1)
659
        .mount()
660
        .await;
661
    mount_messages_page(&server, room_id, "b", "mention-before", 10).await;
662
    mount_messages_page(&server, room_id, "f", "mention-after", 10).await;
663
    mount_messages_page(&server, room_id, "b", "manual-before", 2).await;
664
    mount_messages_page(&server, room_id, "f", "manual-after", 3).await;
665
666
    let feed = SlidingFeed::new(
667
        room_id.to_owned(),
668
        vec![message(
669
            mention_id.as_str(),
670
            ALICE.as_str(),
671
            5_000,
672
            "mention with saved context",
673
            Some(own_user.as_str()),
674
        )],
675
    );
676
    feed.mount(&server).await;
677
    let (engine, handle, mut events) = Engine::from_client(
678
        client,
679
        EngineConfig::default().with_initial_history_limit(0),
680
    )
681
    .await
682
    .unwrap();
683
    let running = tokio::spawn(engine.run());
684
685
    let captured = mentions_page_with_diagnostics(
686
        &mut events,
687
        room_id,
688
        |page| {
689
            page.entries.iter().any(|entry| {
690
                entry.key.event_id == mention_id
691
                    && matches!(&entry.capture, macltok::MentionCaptureState::Partial)
692
            })
693
        },
694
        Some(&server),
695
    )
696
    .await;
697
    let saved = handle.mention_context(room_id, mention_id).await.unwrap();
698
    assert!(saved.context.is_some());
699
    assert!(matches!(saved.state, macltok::MentionCaptureState::Partial));
700
701
    let before_saved_request_count = server
702
        .server()
703
        .received_requests()
704
        .await
705
        .unwrap()
706
        .iter()
707
        .filter(|request| request.url.path().contains("/context/$mention-context"))
708
        .count();
709
    let _ = handle.mention_context(room_id, mention_id).await.unwrap();
710
    let after_saved_request_count = server
711
        .server()
712
        .received_requests()
713
        .await
714
        .unwrap()
715
        .iter()
716
        .filter(|request| request.url.path().contains("/context/$mention-context"))
717
        .count();
718
    assert_eq!(after_saved_request_count, before_saved_request_count);
719
720
    let context = handle
721
        .event_context(room_id, context_id, 2, 3)
722
        .await
723
        .unwrap();
724
    assert_eq!(context.start_token.as_deref(), Some("manual-before-next"));
725
    assert_eq!(context.end_token.as_deref(), Some("manual-after-next"));
726
727
    let requests = server.server().received_requests().await.unwrap();
728
    let context_limits: Vec<_> = requests
729
        .iter()
730
        .filter(|request| request.url.path().contains("/context/"))
731
        .map(|request| {
732
            request
733
                .url
734
                .query_pairs()
735
                .find(|(key, _)| key == "limit")
736
                .map(|(_, value)| value.into_owned())
737
                .unwrap()
738
        })
739
        .collect();
740
    assert_eq!(context_limits, ["0", "0"]);
741
    let queries: Vec<_> = requests
742
        .iter()
743
        .filter(|request| request.url.path().ends_with("/messages"))
744
        .map(|request| {
745
            let query: std::collections::HashMap<_, _> =
746
                request.url.query_pairs().into_owned().collect();
747
            (
748
                query.get("dir").cloned().unwrap(),
749
                query.get("from").cloned().unwrap(),
750
                query
751
                    .get("limit")
752
                    .cloned()
753
                    .unwrap_or_else(|| "10".to_owned()),
754
            )
755
        })
756
        .collect();
757
    assert!(queries.contains(&("b".to_owned(), "mention-before".to_owned(), "10".to_owned())));
758
    assert!(queries.contains(&("f".to_owned(), "mention-after".to_owned(), "10".to_owned())));
759
    assert!(queries.contains(&("b".to_owned(), "manual-before".to_owned(), "2".to_owned())));
760
    assert!(queries.contains(&("f".to_owned(), "manual-after".to_owned(), "3".to_owned())));
761
    assert_eq!(captured.unread_count, 1);
762
763
    shut_down(&handle, &mut events, running).await;
764
    server.server().verify().await;
765
}
766
767
#[cfg(feature = "sqlite")]
768
#[tokio::test]
769
async fn sqlite_mentions_survive_engine_shutdown_and_session_resume() {
770
    static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);
771
772
    let server = MatrixMockServer::new().await;
773
    server
774
        .mock_versions()
775
        .with_simplified_sliding_sync()
776
        .ok()
777
        .mount()
778
        .await;
779
    server.mock_login().ok().mount().await;
780
781
    let root = std::env::temp_dir().join(format!(
782
        "macltok-mentions-inbox-{}-{}",
783
        std::process::id(),
784
        NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)
785
    ));
786
    let store_path = root.join("store");
787
    let sessions_path = root.join("records").join("session");
788
    let sessions = Arc::new(EncryptedSessionStore::file(
789
        &sessions_path,
790
        StoreConfig::sqlite(&store_path, "mentions inbox test passphrase"),
791
    ));
792
    let room_id = room_id!("!mentions-resume:matrix.org");
793
    let own_user = user_id!("@cheeky_monkey:matrix.org");
794
    let feed = SlidingFeed::new(
795
        room_id.to_owned(),
796
        vec![message(
797
            "$durable-mention",
798
            "@alice:matrix.org",
799
            7_000,
800
            "persist this mention",
801
            Some(own_user.as_str()),
802
        )],
803
    );
804
    feed.set_required_state(
805
        room_id,
806
        vec![json!({
807
            "type": "m.room.member",
808
            "event_id": "$joined-member",
809
            "sender": own_user,
810
            "state_key": own_user,
811
            "origin_server_ts": 1,
812
            "content": { "membership": "join" },
813
        })],
814
    );
815
    feed.mount(&server).await;
816
817
    let config = EngineConfig::default().with_initial_history_limit(0);
818
    let session = Session::login_password(
819
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
820
        sessions.clone(),
821
    )
822
    .await
823
    .unwrap();
824
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
825
    let running = tokio::spawn(engine.run());
826
    let first = mentions_page(&mut events, room_id, |page| !page.entries.is_empty()).await;
827
    assert_eq!(
828
        entry_ids(&first),
829
        HashSet::from(["$durable-mention".to_owned()])
830
    );
831
    shut_down(&handle, &mut events, running).await;
832
    drop(handle);
833
834
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
835
        panic!("the saved session did not resume");
836
    };
837
    let (engine, handle, mut events) = session.build(config).await.unwrap();
838
    let running = tokio::spawn(engine.run());
839
    receive_until(&mut events, |event| {
840
        matches!(event, Event::Authenticated { .. })
841
    })
842
    .await;
843
    let resumed = handle.mentions_page(None, 50).await.unwrap();
844
    assert_eq!(
845
        entry_ids(&resumed),
846
        HashSet::from(["$durable-mention".to_owned()])
847
    );
848
    assert_eq!(resumed.unread_count, 1);
849
    shut_down(&handle, &mut events, running).await;
850
    drop(handle);
851
852
    sessions.discard().await.unwrap();
853
    std::fs::remove_dir_all(root).unwrap();
854
}
855
856
#[cfg(feature = "serde")]
857
#[test]
858
fn mentions_command_and_page_round_trip_through_serde() {
859
    let command = MentionsCommand::ReadThrough(MentionsBoundary {
860
        account: user_id!("@alice:example.org").to_owned(),
861
        revision: 7,
862
    });
863
    let encoded_command = serde_json::to_value(&command).unwrap();
864
    let decoded_command: MentionsCommand = serde_json::from_value(encoded_command).unwrap();
865
    assert_eq!(decoded_command, command);
866
867
    let page = MentionsPage {
868
        entries: Vec::new(),
869
        next_cursor: Some("opaque cursor".to_owned()),
870
        end_reached: false,
871
        coverage: MentionCoverage::default(),
872
        unread_count: 0,
873
        revision: 7,
874
        boundary: MentionsBoundary {
875
            account: user_id!("@alice:example.org").to_owned(),
876
            revision: 7,
877
        },
878
    };
879
    let encoded_page = serde_json::to_value(&page).unwrap();
880
    let decoded_page: MentionsPage = serde_json::from_value(encoded_page).unwrap();
881
    assert_eq!(decoded_page, page);
882
}