Skip to main content

matrix_sdk_ui/
unable_to_decrypt_hook.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! This module provides a generic interface to subscribe to unable-to-decrypt
16//! events, and notable updates to such events.
17//!
18//! This provides a general trait that a consumer may implement, as well as
19//! utilities to simplify usage of this trait.
20
21use std::{
22    collections::HashMap,
23    sync::{Arc, Mutex},
24};
25
26use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder};
27use matrix_sdk::{Client, sleep::sleep, task_monitor::BackgroundTaskHandle};
28use matrix_sdk_base::{
29    SendOutsideWasm, StateStoreDataKey, StateStoreDataValue, StoreError, SyncOutsideWasm,
30    crypto::types::events::UtdCause,
31};
32use ruma::{
33    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedServerName, UserId,
34    time::{Duration, Instant},
35};
36use tokio::sync::{Mutex as AsyncMutex, MutexGuard};
37use tracing::{error, trace};
38
39/// A generic interface which methods get called whenever we observe a
40/// unable-to-decrypt (UTD) event.
41pub trait UnableToDecryptHook: std::fmt::Debug + SendOutsideWasm + SyncOutsideWasm {
42    /// Called every time the hook observes an encrypted event that couldn't be
43    /// decrypted.
44    ///
45    /// If the hook manager was configured with a max delay, this could also
46    /// contain extra information for late-decrypted events. See details in
47    /// [`UnableToDecryptInfo::time_to_decrypt`].
48    fn on_utd(&self, info: UnableToDecryptInfo);
49}
50
51/// Information about an event we were unable to decrypt (UTD).
52#[derive(Clone, Debug, Hash, PartialEq, Eq)]
53pub struct UnableToDecryptInfo {
54    /// The identifier of the event that couldn't get decrypted.
55    pub event_id: OwnedEventId,
56
57    /// If the event could be decrypted late (that is, the event was encrypted
58    /// at first, but could be decrypted later on), then this indicates the time
59    /// it took to decrypt the event. If it is not set, this is considered a
60    /// definite UTD.
61    pub time_to_decrypt: Option<Duration>,
62
63    /// What we know about what caused this UTD. E.g. was this event sent when
64    /// we were not a member of this room?
65    pub cause: UtdCause,
66
67    /// The difference between the event creation time (`origin_server_ts`) and
68    /// the time our device was created. If negative, this event was sent
69    /// _before_ our device was created.
70    pub event_local_age_millis: i64,
71
72    /// Whether the user had verified their own identity at the point they
73    /// received the UTD event.
74    pub user_trusts_own_identity: bool,
75
76    /// The homeserver of the user that sent the undecryptable event.
77    pub sender_homeserver: OwnedServerName,
78
79    /// Our local user's own homeserver, or `None` if the client is not logged
80    /// in.
81    pub own_homeserver: Option<OwnedServerName>,
82}
83
84/// Data about a UTD event which we are waiting to report to the parent hook.
85#[derive(Debug)]
86struct PendingUtdReport {
87    /// The time that we received the UTD report from the timeline code.
88    marked_utd_at: Instant,
89
90    /// The task that will report this UTD to the parent hook.
91    report_task: BackgroundTaskHandle,
92
93    /// The UnableToDecryptInfo structure for this UTD event.
94    utd_info: UnableToDecryptInfo,
95}
96
97/// A manager over an existing [`UnableToDecryptHook`] that deduplicates UTDs on
98/// similar events, and adds basic consistency checks.
99///
100/// It can also implement a grace period before reporting an event as a UTD, if
101/// configured with [`Self::with_max_delay`]. Instead of immediately reporting
102/// the UTD, the reporting will be delayed by the max delay at most; if the
103/// event could eventually get decrypted, it may be reported before the end of
104/// that delay.
105#[derive(Debug)]
106pub struct UtdHookManager {
107    /// A Client associated with the UTD hook. This is used to access the store
108    /// which we persist our data to.
109    client: Client,
110
111    /// The parent hook we'll call, when we have found a unique UTD.
112    parent: Arc<dyn UnableToDecryptHook>,
113
114    /// An optional delay before marking the event as UTD ("grace period").
115    max_delay: Option<Duration>,
116
117    /// A mapping of events we're going to report as UTDs, to the tasks to do
118    /// so.
119    ///
120    /// Note: this is empty if no [`Self::max_delay`] is set.
121    ///
122    /// Note: this is theoretically unbounded in size, although this set of
123    /// tasks will degrow over time, as tasks expire after the max delay.
124    pending_delayed: Arc<Mutex<HashMap<OwnedEventId, PendingUtdReport>>>,
125
126    /// Bloom filter containing the event IDs of events which have been reported
127    /// as UTDs
128    reported_utds: Arc<AsyncMutex<GrowableBloom>>,
129}
130
131impl UtdHookManager {
132    /// Create a new [`UtdHookManager`] for the given hook.
133    ///
134    /// A [`Client`] must also be provided; this provides a link to the
135    /// [`matrix_sdk_base::StateStore`] which is used to load and store the
136    /// persistent data.
137    pub fn new(parent: Arc<dyn UnableToDecryptHook>, client: Client) -> Self {
138        let bloom_filter =
139            // Some slightly arbitrarily-chosen parameters here. We specify
140            // that, after 1000 UTDs, we want to have a false-positive rate of
141            // 1%.
142            //
143            // The `GrowableBloomFilter` is based on a series of (partitioned)
144            // Bloom filters; once the first starts getting full (the expected
145            // false-positive rate gets too high), it adds another Bloom filter.
146            // Each new entry is recorded in the most recent Bloom filter; when
147            // querying, if _any_ of the component filters show a match, that
148            // shows an overall match.
149            //
150            // The first component filter is created based on the parameters we
151            // give. For reasons derived in the paper [1], a partitioned Bloom
152            // filter with target false-positive rate `P` after `n` insertions
153            // requires a number of slices `k` given by:
154            //
155            // ```latex
156            // k = log2(1/P) = -ln(P) / ln(2)
157            // ```
158            //
159            // ... where each slice has a number of bits `m` given by
160            //
161            // ```latex
162            // m = n / ln(2)
163            // ```
164            //
165            // We have to have a whole number of slices and bits, so the total
166            // number of bits M is:
167            //
168            // ```latex
169            // M = ceil(k) * ceil(m)
170            //   = ceil(-ln(P) / ln(2)) * ceil(n / ln(2))
171            // ```
172            //
173            // In other words, our FP rate of 1% after 1000 insertions requires:
174            //
175            // ```latex
176            // M = ceil(-ln(0.01) / ln(2)) * ceil(1000 / ln(2))
177            //   = 7 * 1443 = 10101 bits
178            // ```
179            //
180            // So our filter starts off with 1263 bytes of data (plus a little
181            // overhead). Once we hit 1000 UTDs, we add a second component
182            // filter with a capacity double that of the original and target
183            // error rate 85% of the original (another 2526 bytes), which then
184            // lasts us until a total of 3000 UTDs.
185            //
186            // [1]: https://gsd.di.uminho.pt/members/cbm/ps/dbloom.pdf
187            GrowableBloomBuilder::new().estimated_insertions(1000).desired_error_ratio(0.01).build();
188
189        Self {
190            client,
191            parent,
192            max_delay: None,
193            pending_delayed: Default::default(),
194            reported_utds: Arc::new(AsyncMutex::new(bloom_filter)),
195        }
196    }
197
198    /// Reports UTDs with the given max delay.
199    ///
200    /// Note: late decryptions are always reported, even if there was a grace
201    /// period set for the reporting of the UTD.
202    pub fn with_max_delay(mut self, delay: Duration) -> Self {
203        self.max_delay = Some(delay);
204        self
205    }
206
207    /// Load the persistent data for the UTD hook from the store.
208    ///
209    /// If the client previously used a UtdHookManager, and UTDs were
210    /// encountered, the data on the reported UTDs is loaded from the store.
211    /// Otherwise, there is no effect.
212    pub async fn reload_from_store(&mut self) -> Result<(), StoreError> {
213        let existing_data =
214            self.client.state_store().get_kv_data(StateStoreDataKey::UtdHookManagerData).await?;
215
216        if let Some(existing_data) = existing_data {
217            let bloom_filter = existing_data
218                .into_utd_hook_manager_data()
219                .expect("StateStore::get_kv_data should return data of the right type");
220            self.reported_utds = Arc::new(AsyncMutex::new(bloom_filter));
221        }
222        Ok(())
223    }
224
225    /// The function to call whenever a UTD is seen for the first time.
226    ///
227    /// Pipe in any information that needs to be included in the final report.
228    ///
229    /// # Arguments
230    ///
231    /// - `event_id` - The ID of the event that could not be decrypted.
232    /// - `cause` - Our best guess at the reason why the event can't be
233    ///   decrypted.
234    /// - `event_timestamp` - The event's `origin_server_ts` field (or creation
235    ///   time for local echo).
236    /// - `sender_user_id` - The Matrix user ID of the user that sent the
237    ///   undecryptable message.
238    pub(crate) async fn on_utd(
239        &self,
240        event_id: &EventId,
241        cause: UtdCause,
242        event_timestamp: MilliSecondsSinceUnixEpoch,
243        sender_user_id: &UserId,
244    ) {
245        trace!(%event_id, "UtdHookManager: Observed UTD");
246        // Hold the lock on `reported_utds` throughout, to avoid races with
247        // other threads.
248        let mut reported_utds_lock = self.reported_utds.lock().await;
249
250        // Check if this, or a previous instance of UtdHookManager, has already
251        // reported this UTD, and bail out if not.
252        if reported_utds_lock.contains(event_id) {
253            return;
254        }
255
256        // Otherwise, check if we already have a task to handle this UTD.
257        if self.pending_delayed.lock().unwrap().contains_key(event_id) {
258            return;
259        }
260
261        let event_local_age_millis = i64::from(event_timestamp.get()).saturating_sub_unsigned(
262            self.client.encryption().device_creation_timestamp().await.get().into(),
263        );
264
265        let own_user_id = self.client.user_id();
266        let user_trusts_own_identity = if let Some(own_user_id) = own_user_id {
267            if let Ok(Some(own_id)) = self.client.encryption().get_user_identity(own_user_id).await
268            {
269                own_id.is_verified()
270            } else {
271                false
272            }
273        } else {
274            false
275        };
276
277        let own_homeserver = own_user_id.map(|id| id.server_name().to_owned());
278        let sender_homeserver = sender_user_id.server_name().to_owned();
279
280        let info = UnableToDecryptInfo {
281            event_id: event_id.to_owned(),
282            time_to_decrypt: None,
283            cause,
284            event_local_age_millis,
285            user_trusts_own_identity,
286            own_homeserver,
287            sender_homeserver,
288        };
289
290        let Some(max_delay) = self.max_delay else {
291            // No delay: immediately report the event to the parent hook.
292            Self::report_utd(info, &self.parent, &self.client, &mut reported_utds_lock).await;
293            return;
294        };
295
296        // Clone data shared with the task below.
297        let pending_delayed = self.pending_delayed.clone();
298        let reported_utds = self.reported_utds.clone();
299        let parent = self.parent.clone();
300        let client = self.client.clone();
301        let owned_event_id = event_id.to_owned();
302
303        // Spawn a task that will wait for the given delay, and maybe call the
304        // parent hook then.
305        let handle = self.client.task_monitor().spawn_finite_task("utd_hook", async move {
306            // Wait for the given delay.
307            sleep(max_delay).await;
308
309            // Make sure we take out the lock on `reported_utds` before removing
310            // the entry from `pending_delayed`, to ensure we don't race against
311            // another call to `on_utd` (which could otherwise see that the
312            // entry has been removed from `pending_delayed` but not yet added
313            // to `reported_utds`).
314            let mut reported_utds_lock = reported_utds.lock().await;
315
316            // Remove the task from the outstanding set. But if it's already
317            // been removed, it's been decrypted since the task was added!
318            let pending_report = pending_delayed.lock().unwrap().remove(&owned_event_id);
319            if let Some(pending_report) = pending_report {
320                Self::report_utd(
321                    pending_report.utd_info,
322                    &parent,
323                    &client,
324                    &mut reported_utds_lock,
325                )
326                .await;
327            }
328        });
329
330        // Add the task to the set of pending tasks.
331        self.pending_delayed.lock().unwrap().insert(
332            event_id.to_owned(),
333            PendingUtdReport { marked_utd_at: Instant::now(), report_task: handle, utd_info: info },
334        );
335    }
336
337    /// The function to call whenever an event that was marked as a UTD has
338    /// eventually been decrypted.
339    ///
340    /// Note: if this is called for an event that was never marked as a UTD
341    /// before, it has no effect.
342    pub(crate) async fn on_late_decrypt(&self, event_id: &EventId) {
343        trace!(%event_id, "UtdHookManager: On late decrypt");
344        // Hold the lock on `reported_utds` throughout, to avoid races with
345        // other threads.
346        let mut reported_utds_lock = self.reported_utds.lock().await;
347
348        // Only let the parent hook know about the late decryption if the event
349        // is a pending UTD. If so, remove the event from the pending list —
350        // doing so will cause the reporting task to no-op if it runs.
351        let Some(pending_utd_report) = self.pending_delayed.lock().unwrap().remove(event_id) else {
352            trace!(%event_id, "UtdHookManager: received a late decrypt report for an unknown utd");
353            return;
354        };
355
356        // We can also cancel the reporting task.
357        pending_utd_report.report_task.abort();
358
359        // Update the UTD Info struct with new data, then report it
360        let mut info = pending_utd_report.utd_info;
361        info.time_to_decrypt = Some(pending_utd_report.marked_utd_at.elapsed());
362        Self::report_utd(info, &self.parent, &self.client, &mut reported_utds_lock).await;
363    }
364
365    /// Helper for [`UtdHookManager::on_utd`] and
366    /// [`UtdHookManager.on_late_decrypt`]: reports the UTD to the parent,
367    /// and records the event as reported.
368    ///
369    /// Must be called with the lock held on [`UtdHookManager::reported_utds`],
370    /// and takes a `MutexGuard` to enforce that.
371    async fn report_utd(
372        info: UnableToDecryptInfo,
373        parent_hook: &Arc<dyn UnableToDecryptHook>,
374        client: &Client,
375        reported_utds_lock: &mut MutexGuard<'_, GrowableBloom>,
376    ) {
377        let event_id = info.event_id.clone();
378        parent_hook.on_utd(info);
379        reported_utds_lock.insert(event_id);
380        if let Err(e) = client
381            .state_store()
382            .set_kv_data(
383                StateStoreDataKey::UtdHookManagerData,
384                StateStoreDataValue::UtdHookManagerData(reported_utds_lock.clone()),
385            )
386            .await
387        {
388            error!("Unable to persist UTD report data: {}", e);
389        }
390    }
391}
392
393impl Drop for UtdHookManager {
394    fn drop(&mut self) {
395        // Cancel all the outstanding delayed tasks to report UTDs.
396        //
397        // Here, we don't take the lock on `reported_utd`s (indeed, we can't,
398        // since `reported_utds` has an async mutex, and `drop` has to be sync),
399        // but that's ok. We can't race against `on_utd` or `on_late_decrypt`,
400        // since they both have `&self` references which mean `drop` can't be
401        // called. We _could_ race against one of the actual tasks to report
402        // UTDs, but that's ok too: either the report task will bail out when it
403        // sees the entry has been removed from `pending_delayed` (which is
404        // fine), or the report task will successfully report the UTD (which is
405        // fine).
406        let mut pending_delayed = self.pending_delayed.lock().unwrap();
407        for (_, pending_utd_report) in pending_delayed.drain() {
408            pending_utd_report.report_task.abort();
409        }
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use matrix_sdk::test_utils::{logged_in_client, no_retry_test_client};
416    use matrix_sdk_test::async_test;
417    use ruma::{event_id, owned_server_name, user_id};
418
419    use super::*;
420
421    #[derive(Debug, Default)]
422    struct Dummy {
423        utds: Mutex<Vec<UnableToDecryptInfo>>,
424    }
425
426    impl UnableToDecryptHook for Dummy {
427        fn on_utd(&self, info: UnableToDecryptInfo) {
428            self.utds.lock().unwrap().push(info);
429        }
430    }
431
432    #[async_test]
433    async fn test_deduplicates_utds() {
434        // If I create a dummy hook,
435        let hook = Arc::new(Dummy::default());
436
437        // And I wrap with the UtdHookManager,
438        let wrapper = UtdHookManager::new(hook.clone(), logged_in_client(None).await);
439
440        // And I call the `on_utd` method multiple times, sometimes on the same
441        // event,
442        let event_timestamp = MilliSecondsSinceUnixEpoch::now();
443        let sender_user = user_id!("@example2:localhost");
444        let federated_user = user_id!("@example2:example.com");
445        wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp, sender_user).await;
446        wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp, sender_user).await;
447        wrapper.on_utd(event_id!("$2"), UtdCause::Unknown, event_timestamp, federated_user).await;
448        wrapper.on_utd(event_id!("$1"), UtdCause::Unknown, event_timestamp, sender_user).await;
449        wrapper.on_utd(event_id!("$2"), UtdCause::Unknown, event_timestamp, federated_user).await;
450        wrapper.on_utd(event_id!("$3"), UtdCause::Unknown, event_timestamp, sender_user).await;
451
452        // Then the event ids have been deduplicated,
453        {
454            let utds = hook.utds.lock().unwrap();
455            assert_eq!(utds.len(), 3);
456            assert_eq!(utds[0].event_id, "$1");
457            assert_eq!(utds[1].event_id, "$2");
458            assert_eq!(utds[2].event_id, "$3");
459
460            // No event is a late-decryption event.
461            assert!(utds[0].time_to_decrypt.is_none());
462            assert!(utds[1].time_to_decrypt.is_none());
463            assert!(utds[2].time_to_decrypt.is_none());
464
465            // event_local_age_millis should be a small positive number, because
466            // the timestamp we used was after we created the device
467            let utd_local_age = utds[0].event_local_age_millis;
468            assert!(utd_local_age >= 0);
469            assert!(utd_local_age <= 1000);
470
471            assert_eq!(utds[0].sender_homeserver, "localhost");
472            assert_eq!(utds[0].own_homeserver, Some(owned_server_name!("localhost")));
473
474            assert_eq!(utds[1].sender_homeserver, "example.com");
475            assert_eq!(utds[1].own_homeserver, Some(owned_server_name!("localhost")));
476        }
477    }
478
479    #[async_test]
480    async fn test_deduplicates_utds_from_previous_session() {
481        // Use a single client for both hooks, so that both hooks are backed by
482        // the same memorystore.
483        let client = no_retry_test_client(None).await;
484
485        // Dummy hook 1, with the first UtdHookManager
486        {
487            let hook = Arc::new(Dummy::default());
488            let wrapper = UtdHookManager::new(hook.clone(), client.clone());
489
490            // I call it a couple of times with different events
491            wrapper
492                .on_utd(
493                    event_id!("$1"),
494                    UtdCause::Unknown,
495                    MilliSecondsSinceUnixEpoch::now(),
496                    user_id!("@a:b"),
497                )
498                .await;
499            wrapper
500                .on_utd(
501                    event_id!("$2"),
502                    UtdCause::Unknown,
503                    MilliSecondsSinceUnixEpoch::now(),
504                    user_id!("@a:b"),
505                )
506                .await;
507
508            // Sanity-check the reported event IDs
509            {
510                let utds = hook.utds.lock().unwrap();
511                assert_eq!(utds.len(), 2);
512                assert_eq!(utds[0].event_id, "$1");
513                assert!(utds[0].time_to_decrypt.is_none());
514                assert_eq!(utds[1].event_id, "$2");
515                assert!(utds[1].time_to_decrypt.is_none());
516            }
517        }
518
519        // Now, create a *new* hook, with a *new* UtdHookManager
520        {
521            let hook = Arc::new(Dummy::default());
522            let mut wrapper = UtdHookManager::new(hook.clone(), client.clone());
523            wrapper.reload_from_store().await.unwrap();
524
525            // Call it with more events, some of which match the previous
526            // instance
527            wrapper
528                .on_utd(
529                    event_id!("$1"),
530                    UtdCause::Unknown,
531                    MilliSecondsSinceUnixEpoch::now(),
532                    user_id!("@a:b"),
533                )
534                .await;
535            wrapper
536                .on_utd(
537                    event_id!("$3"),
538                    UtdCause::Unknown,
539                    MilliSecondsSinceUnixEpoch::now(),
540                    user_id!("@a:b"),
541                )
542                .await;
543
544            // Only the *new* ones should be reported
545            let utds = hook.utds.lock().unwrap();
546            assert_eq!(utds.len(), 1);
547            assert_eq!(utds[0].event_id, "$3");
548        }
549    }
550
551    /// Test that UTD events which had not yet been reported in a previous
552    /// session, are reported in the next session.
553    #[async_test]
554    async fn test_does_not_deduplicate_late_utds_from_previous_session() {
555        // Use a single client for both hooks, so that both hooks are backed by
556        // the same memorystore.
557        let client = no_retry_test_client(None).await;
558
559        // Dummy hook 1, with the first UtdHookManager
560        {
561            let hook = Arc::new(Dummy::default());
562            let wrapper = UtdHookManager::new(hook.clone(), client.clone())
563                .with_max_delay(Duration::from_secs(2));
564
565            // a UTD event
566            wrapper
567                .on_utd(
568                    event_id!("$1"),
569                    UtdCause::Unknown,
570                    MilliSecondsSinceUnixEpoch::now(),
571                    user_id!("@a:b"),
572                )
573                .await;
574
575            // The event ID should not yet have been reported.
576            {
577                let utds = hook.utds.lock().unwrap();
578                assert_eq!(utds.len(), 0);
579            }
580        }
581
582        // Now, create a *new* hook, with a *new* UtdHookManager
583        {
584            let hook = Arc::new(Dummy::default());
585            let mut wrapper = UtdHookManager::new(hook.clone(), client.clone());
586            wrapper.reload_from_store().await.unwrap();
587
588            // Call the new hook with the same event
589            wrapper
590                .on_utd(
591                    event_id!("$1"),
592                    UtdCause::Unknown,
593                    MilliSecondsSinceUnixEpoch::now(),
594                    user_id!("@a:b"),
595                )
596                .await;
597
598            // And it should be reported.
599            sleep(Duration::from_millis(2500)).await;
600
601            let utds = hook.utds.lock().unwrap();
602            assert_eq!(utds.len(), 1);
603            assert_eq!(utds[0].event_id, "$1");
604        }
605    }
606
607    #[async_test]
608    async fn test_on_late_decrypted_no_effect() {
609        // If I create a dummy hook,
610        let hook = Arc::new(Dummy::default());
611
612        // And I wrap with the UtdHookManager,
613        let wrapper = UtdHookManager::new(hook.clone(), no_retry_test_client(None).await);
614
615        // And I call the `on_late_decrypt` method before the event had been
616        // marked as utd,
617        wrapper.on_late_decrypt(event_id!("$1")).await;
618
619        // Then nothing is registered in the parent hook.
620        assert!(hook.utds.lock().unwrap().is_empty());
621    }
622
623    #[async_test]
624    async fn test_on_late_decrypted_after_utd_no_grace_period() {
625        // If I create a dummy hook,
626        let hook = Arc::new(Dummy::default());
627
628        // And I wrap with the UtdHookManager,
629        let wrapper = UtdHookManager::new(hook.clone(), no_retry_test_client(None).await);
630
631        // And I call the `on_utd` method for an event,
632        wrapper
633            .on_utd(
634                event_id!("$1"),
635                UtdCause::Unknown,
636                MilliSecondsSinceUnixEpoch::now(),
637                user_id!("@a:b"),
638            )
639            .await;
640
641        // Then the UTD has been notified, but not as late-decrypted event.
642        {
643            let utds = hook.utds.lock().unwrap();
644            assert_eq!(utds.len(), 1);
645            assert_eq!(utds[0].event_id, "$1");
646            assert!(utds[0].time_to_decrypt.is_none());
647        }
648
649        // And when I call the `on_late_decrypt` method,
650        wrapper.on_late_decrypt(event_id!("$1")).await;
651
652        // Then the event is not reported again as a late-decryption.
653        {
654            let utds = hook.utds.lock().unwrap();
655            assert_eq!(utds.len(), 1);
656
657            // The previous report is still there. (There was no grace period.)
658            assert_eq!(utds[0].event_id, "$1");
659            assert!(utds[0].time_to_decrypt.is_none());
660        }
661    }
662
663    #[cfg(not(target_family = "wasm"))] // wasm32 has no time for that
664    #[async_test]
665    async fn test_delayed_utd() {
666        // If I create a dummy hook,
667        let hook = Arc::new(Dummy::default());
668
669        // And I wrap with the UtdHookManager, configured to delay reporting
670        // after 2 seconds.
671        let wrapper = UtdHookManager::new(hook.clone(), no_retry_test_client(None).await)
672            .with_max_delay(Duration::from_secs(2));
673
674        // And I call the `on_utd` method for an event,
675        wrapper
676            .on_utd(
677                event_id!("$1"),
678                UtdCause::Unknown,
679                MilliSecondsSinceUnixEpoch::now(),
680                user_id!("@a:b"),
681            )
682            .await;
683
684        // Then the UTD is not being reported immediately.
685        assert!(hook.utds.lock().unwrap().is_empty());
686        assert_eq!(wrapper.pending_delayed.lock().unwrap().len(), 1);
687
688        // If I wait for 1 second, then it's still not been notified yet.
689        sleep(Duration::from_secs(1)).await;
690
691        assert!(hook.utds.lock().unwrap().is_empty());
692        assert_eq!(wrapper.pending_delayed.lock().unwrap().len(), 1);
693
694        // But if I wait just a bit more, then it's getting notified as a
695        // definite UTD.
696        sleep(Duration::from_millis(1500)).await;
697
698        {
699            let utds = hook.utds.lock().unwrap();
700            assert_eq!(utds.len(), 1);
701            assert_eq!(utds[0].event_id, "$1");
702            assert!(utds[0].time_to_decrypt.is_none());
703        }
704
705        assert!(wrapper.pending_delayed.lock().unwrap().is_empty());
706    }
707
708    #[cfg(not(target_family = "wasm"))] // wasm32 has no time for that
709    #[async_test]
710    async fn test_delayed_late_decryption() {
711        // If I create a dummy hook,
712        let hook = Arc::new(Dummy::default());
713
714        // And I wrap with the UtdHookManager, configured to delay reporting
715        // after 2 seconds.
716        let wrapper = UtdHookManager::new(hook.clone(), no_retry_test_client(None).await)
717            .with_max_delay(Duration::from_secs(2));
718
719        // And I call the `on_utd` method for an event,
720        wrapper
721            .on_utd(
722                event_id!("$1"),
723                UtdCause::Unknown,
724                MilliSecondsSinceUnixEpoch::now(),
725                user_id!("@a:b"),
726            )
727            .await;
728
729        // Then the UTD has not been notified quite yet.
730        assert!(hook.utds.lock().unwrap().is_empty());
731        assert_eq!(wrapper.pending_delayed.lock().unwrap().len(), 1);
732
733        // If I wait for 1 second, and mark the event as late-decrypted,
734        sleep(Duration::from_secs(1)).await;
735
736        wrapper.on_late_decrypt(event_id!("$1")).await;
737
738        // Then it's being immediately reported as a late-decryption UTD.
739        {
740            let utds = hook.utds.lock().unwrap();
741            assert_eq!(utds.len(), 1);
742            assert_eq!(utds[0].event_id, "$1");
743            assert!(utds[0].time_to_decrypt.is_some());
744        }
745
746        // And there aren't any pending delayed reports anymore.
747        assert!(wrapper.pending_delayed.lock().unwrap().is_empty());
748    }
749}