matrix_sdk_base/event_cache/store/
integration_tests.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//! Trait and macro of integration tests for `EventCacheStore` implementations.
16
17use assert_matches::assert_matches;
18use async_trait::async_trait;
19use matrix_sdk_common::{
20    deserialized_responses::{
21        AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
22        VerificationState,
23    },
24    linked_chunk::{
25        ChunkContent, ChunkIdentifier as CId, LinkedChunk, LinkedChunkBuilder, Position, RawChunk,
26        Update,
27    },
28};
29use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
30use ruma::{
31    api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
32    push::Action, room_id, uint, RoomId,
33};
34
35use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
36use crate::{
37    event_cache::{Event, Gap},
38    media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
39};
40
41/// Create a test event with all data filled, for testing that linked chunk
42/// correctly stores event data.
43///
44/// Keep in sync with [`check_test_event`].
45pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
46    let encryption_info = EncryptionInfo {
47        sender: (*ALICE).into(),
48        sender_device: None,
49        algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
50            curve25519_key: "1337".to_owned(),
51            sender_claimed_keys: Default::default(),
52        },
53        verification_state: VerificationState::Verified,
54    };
55
56    let event = EventFactory::new()
57        .text_msg(content)
58        .room(room_id)
59        .sender(*ALICE)
60        .into_raw_timeline()
61        .cast();
62
63    TimelineEvent {
64        kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
65            event,
66            encryption_info,
67            unsigned_encryption_info: None,
68        }),
69        push_actions: Some(vec![Action::Notify]),
70    }
71}
72
73/// Check that an event created with [`make_test_event`] contains the expected
74/// data.
75///
76/// Keep in sync with [`make_test_event`].
77#[track_caller]
78pub fn check_test_event(event: &TimelineEvent, text: &str) {
79    // Check push actions.
80    let actions = event.push_actions.as_ref().unwrap();
81    assert_eq!(actions.len(), 1);
82    assert_matches!(&actions[0], Action::Notify);
83
84    // Check content.
85    assert_matches!(&event.kind, TimelineEventKind::Decrypted(d) => {
86        // Check encryption fields.
87        assert_eq!(d.encryption_info.sender, *ALICE);
88        assert_matches!(&d.encryption_info.algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => {
89            assert_eq!(curve25519_key, "1337");
90        });
91
92        // Check event.
93        let deserialized = d.event.deserialize().unwrap();
94        assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => {
95            assert_eq!(msg.as_original().unwrap().content.body(), text);
96        });
97    });
98}
99
100/// `EventCacheStore` integration tests.
101///
102/// This trait is not meant to be used directly, but will be used with the
103/// [`event_cache_store_integration_tests!`] macro.
104#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
105#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
106pub trait EventCacheStoreIntegrationTests {
107    /// Test media content storage.
108    async fn test_media_content(&self);
109
110    /// Test replacing a MXID.
111    async fn test_replace_media_key(&self);
112
113    /// Test handling updates to a linked chunk and reloading these updates from
114    /// the store.
115    async fn test_handle_updates_and_rebuild_linked_chunk(&self);
116
117    /// Test that rebuilding a linked chunk from an empty store doesn't return
118    /// anything.
119    async fn test_rebuild_empty_linked_chunk(&self);
120
121    /// Test that clear all the rooms' linked chunks works.
122    async fn test_clear_all_rooms_chunks(&self);
123
124    /// Test that removing a room from storage empties all associated data.
125    async fn test_remove_room(&self);
126}
127
128fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
129    LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
130}
131
132#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
133#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
134impl EventCacheStoreIntegrationTests for DynEventCacheStore {
135    async fn test_media_content(&self) {
136        let uri = mxc_uri!("mxc://localhost/media");
137        let request_file = MediaRequestParameters {
138            source: MediaSource::Plain(uri.to_owned()),
139            format: MediaFormat::File,
140        };
141        let request_thumbnail = MediaRequestParameters {
142            source: MediaSource::Plain(uri.to_owned()),
143            format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
144                Method::Crop,
145                uint!(100),
146                uint!(100),
147            )),
148        };
149
150        let other_uri = mxc_uri!("mxc://localhost/media-other");
151        let request_other_file = MediaRequestParameters {
152            source: MediaSource::Plain(other_uri.to_owned()),
153            format: MediaFormat::File,
154        };
155
156        let content: Vec<u8> = "hello".into();
157        let thumbnail_content: Vec<u8> = "world".into();
158        let other_content: Vec<u8> = "foo".into();
159
160        // Media isn't present in the cache.
161        assert!(
162            self.get_media_content(&request_file).await.unwrap().is_none(),
163            "unexpected media found"
164        );
165        assert!(
166            self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
167            "media not found"
168        );
169
170        // Let's add the media.
171        self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
172            .await
173            .expect("adding media failed");
174
175        // Media is present in the cache.
176        assert_eq!(
177            self.get_media_content(&request_file).await.unwrap().as_ref(),
178            Some(&content),
179            "media not found though added"
180        );
181        assert_eq!(
182            self.get_media_content_for_uri(uri).await.unwrap().as_ref(),
183            Some(&content),
184            "media not found by URI though added"
185        );
186
187        // Let's remove the media.
188        self.remove_media_content(&request_file).await.expect("removing media failed");
189
190        // Media isn't present in the cache.
191        assert!(
192            self.get_media_content(&request_file).await.unwrap().is_none(),
193            "media still there after removing"
194        );
195        assert!(
196            self.get_media_content_for_uri(uri).await.unwrap().is_none(),
197            "media still found by URI after removing"
198        );
199
200        // Let's add the media again.
201        self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
202            .await
203            .expect("adding media again failed");
204
205        assert_eq!(
206            self.get_media_content(&request_file).await.unwrap().as_ref(),
207            Some(&content),
208            "media not found after adding again"
209        );
210
211        // Let's add the thumbnail media.
212        self.add_media_content(
213            &request_thumbnail,
214            thumbnail_content.clone(),
215            IgnoreMediaRetentionPolicy::No,
216        )
217        .await
218        .expect("adding thumbnail failed");
219
220        // Media's thumbnail is present.
221        assert_eq!(
222            self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
223            Some(&thumbnail_content),
224            "thumbnail not found"
225        );
226
227        // We get a file with the URI, we don't know which one.
228        assert!(
229            self.get_media_content_for_uri(uri).await.unwrap().is_some(),
230            "media not found by URI though two where added"
231        );
232
233        // Let's add another media with a different URI.
234        self.add_media_content(
235            &request_other_file,
236            other_content.clone(),
237            IgnoreMediaRetentionPolicy::No,
238        )
239        .await
240        .expect("adding other media failed");
241
242        // Other file is present.
243        assert_eq!(
244            self.get_media_content(&request_other_file).await.unwrap().as_ref(),
245            Some(&other_content),
246            "other file not found"
247        );
248        assert_eq!(
249            self.get_media_content_for_uri(other_uri).await.unwrap().as_ref(),
250            Some(&other_content),
251            "other file not found by URI"
252        );
253
254        // Let's remove media based on URI.
255        self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
256
257        assert!(
258            self.get_media_content(&request_file).await.unwrap().is_none(),
259            "media wasn't removed"
260        );
261        assert!(
262            self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
263            "thumbnail wasn't removed"
264        );
265        assert!(
266            self.get_media_content(&request_other_file).await.unwrap().is_some(),
267            "other media was removed"
268        );
269        assert!(
270            self.get_media_content_for_uri(uri).await.unwrap().is_none(),
271            "media found by URI wasn't removed"
272        );
273        assert!(
274            self.get_media_content_for_uri(other_uri).await.unwrap().is_some(),
275            "other media found by URI was removed"
276        );
277    }
278
279    async fn test_replace_media_key(&self) {
280        let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d");
281        let req = MediaRequestParameters {
282            source: MediaSource::Plain(uri.to_owned()),
283            format: MediaFormat::File,
284        };
285
286        let content = "hello".as_bytes().to_owned();
287
288        // Media isn't present in the cache.
289        assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
290
291        // Add the media.
292        self.add_media_content(&req, content.clone(), IgnoreMediaRetentionPolicy::No)
293            .await
294            .expect("adding media failed");
295
296        // Sanity-check: media is found after adding it.
297        assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
298
299        // Replacing a media request works.
300        let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d");
301        let new_req = MediaRequestParameters {
302            source: MediaSource::Plain(new_uri.to_owned()),
303            format: MediaFormat::File,
304        };
305        self.replace_media_key(&req, &new_req)
306            .await
307            .expect("replacing the media request key failed");
308
309        // Finding with the previous request doesn't work anymore.
310        assert!(
311            self.get_media_content(&req).await.unwrap().is_none(),
312            "unexpected media found with the old key"
313        );
314
315        // Finding with the new request does work.
316        assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
317    }
318
319    async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
320        let room_id = room_id!("!r0:matrix.org");
321
322        self.handle_linked_chunk_updates(
323            room_id,
324            vec![
325                // new chunk
326                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
327                // new items on 0
328                Update::PushItems {
329                    at: Position::new(CId::new(0), 0),
330                    items: vec![
331                        make_test_event(room_id, "hello"),
332                        make_test_event(room_id, "world"),
333                    ],
334                },
335                // a gap chunk
336                Update::NewGapChunk {
337                    previous: Some(CId::new(0)),
338                    new: CId::new(1),
339                    next: None,
340                    gap: Gap { prev_token: "parmesan".to_owned() },
341                },
342                // another items chunk
343                Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
344                // new items on 0
345                Update::PushItems {
346                    at: Position::new(CId::new(2), 0),
347                    items: vec![make_test_event(room_id, "sup")],
348                },
349            ],
350        )
351        .await
352        .unwrap();
353
354        // The linked chunk is correctly reloaded.
355        let raws = self.reload_linked_chunk(room_id).await.unwrap();
356        let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty");
357
358        let mut chunks = lc.chunks();
359
360        {
361            let first = chunks.next().unwrap();
362            // Note: we can't assert the previous/next chunks, as these fields and their
363            // getters are private.
364            assert_eq!(first.identifier(), CId::new(0));
365
366            assert_matches!(first.content(), ChunkContent::Items(events) => {
367                assert_eq!(events.len(), 2);
368                check_test_event(&events[0], "hello");
369                check_test_event(&events[1], "world");
370            });
371        }
372
373        {
374            let second = chunks.next().unwrap();
375            assert_eq!(second.identifier(), CId::new(1));
376
377            assert_matches!(second.content(), ChunkContent::Gap(gap) => {
378                assert_eq!(gap.prev_token, "parmesan");
379            });
380        }
381
382        {
383            let third = chunks.next().unwrap();
384            assert_eq!(third.identifier(), CId::new(2));
385
386            assert_matches!(third.content(), ChunkContent::Items(events) => {
387                assert_eq!(events.len(), 1);
388                check_test_event(&events[0], "sup");
389            });
390        }
391
392        assert!(chunks.next().is_none());
393    }
394
395    async fn test_rebuild_empty_linked_chunk(&self) {
396        // When I rebuild a linked chunk from an empty store, it's empty.
397        let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
398        assert!(rebuild_linked_chunk(raw_parts).is_none());
399    }
400
401    async fn test_clear_all_rooms_chunks(&self) {
402        let r0 = room_id!("!r0:matrix.org");
403        let r1 = room_id!("!r1:matrix.org");
404
405        // Add updates for the first room.
406        self.handle_linked_chunk_updates(
407            r0,
408            vec![
409                // new chunk
410                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
411                // new items on 0
412                Update::PushItems {
413                    at: Position::new(CId::new(0), 0),
414                    items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
415                },
416            ],
417        )
418        .await
419        .unwrap();
420
421        // Add updates for the second room.
422        self.handle_linked_chunk_updates(
423            r1,
424            vec![
425                // Empty items chunk.
426                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
427                // a gap chunk
428                Update::NewGapChunk {
429                    previous: Some(CId::new(0)),
430                    new: CId::new(1),
431                    next: None,
432                    gap: Gap { prev_token: "bleu d'auvergne".to_owned() },
433                },
434                // another items chunk
435                Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
436                // new items on 0
437                Update::PushItems {
438                    at: Position::new(CId::new(2), 0),
439                    items: vec![make_test_event(r0, "yummy")],
440                },
441            ],
442        )
443        .await
444        .unwrap();
445
446        // Sanity check: both linked chunks can be reloaded.
447        assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some());
448        assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_some());
449
450        // Clear the chunks.
451        self.clear_all_rooms_chunks().await.unwrap();
452
453        // Both rooms now have no linked chunk.
454        assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
455        assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none());
456    }
457
458    async fn test_remove_room(&self) {
459        let r0 = room_id!("!r0:matrix.org");
460        let r1 = room_id!("!r1:matrix.org");
461
462        // Add updates to the first room.
463        self.handle_linked_chunk_updates(
464            r0,
465            vec![
466                // new chunk
467                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
468                // new items on 0
469                Update::PushItems {
470                    at: Position::new(CId::new(0), 0),
471                    items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
472                },
473            ],
474        )
475        .await
476        .unwrap();
477
478        // Add updates to the second room.
479        self.handle_linked_chunk_updates(
480            r1,
481            vec![
482                // new chunk
483                Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
484                // new items on 0
485                Update::PushItems {
486                    at: Position::new(CId::new(0), 0),
487                    items: vec![make_test_event(r0, "yummy")],
488                },
489            ],
490        )
491        .await
492        .unwrap();
493
494        // Try to remove content from r0.
495        self.remove_room(r0).await.unwrap();
496
497        // Check that r0 doesn't have a linked chunk anymore.
498        let r0_linked_chunk = self.reload_linked_chunk(r0).await.unwrap();
499        assert!(r0_linked_chunk.is_empty());
500
501        // Check that r1 is unaffected.
502        let r1_linked_chunk = self.reload_linked_chunk(r1).await.unwrap();
503        assert!(!r1_linked_chunk.is_empty());
504    }
505}
506
507/// Macro building to allow your `EventCacheStore` implementation to run the
508/// entire tests suite locally.
509///
510/// You need to provide a `async fn get_event_cache_store() ->
511/// EventCacheStoreResult<impl EventCacheStore>` providing a fresh event cache
512/// store on the same level you invoke the macro.
513///
514/// ## Usage Example:
515/// ```no_run
516/// # use matrix_sdk_base::event_cache::store::{
517/// #    EventCacheStore,
518/// #    MemoryStore as MyStore,
519/// #    Result as EventCacheStoreResult,
520/// # };
521///
522/// #[cfg(test)]
523/// mod tests {
524///     use super::{EventCacheStore, EventCacheStoreResult, MyStore};
525///
526///     async fn get_event_cache_store(
527///     ) -> EventCacheStoreResult<impl EventCacheStore> {
528///         Ok(MyStore::new())
529///     }
530///
531///     event_cache_store_integration_tests!();
532/// }
533/// ```
534#[allow(unused_macros, unused_extern_crates)]
535#[macro_export]
536macro_rules! event_cache_store_integration_tests {
537    () => {
538        mod event_cache_store_integration_tests {
539            use matrix_sdk_test::async_test;
540            use $crate::event_cache::store::{
541                EventCacheStoreIntegrationTests, IntoEventCacheStore,
542            };
543
544            use super::get_event_cache_store;
545
546            #[async_test]
547            async fn test_media_content() {
548                let event_cache_store =
549                    get_event_cache_store().await.unwrap().into_event_cache_store();
550                event_cache_store.test_media_content().await;
551            }
552
553            #[async_test]
554            async fn test_replace_media_key() {
555                let event_cache_store =
556                    get_event_cache_store().await.unwrap().into_event_cache_store();
557                event_cache_store.test_replace_media_key().await;
558            }
559
560            #[async_test]
561            async fn test_handle_updates_and_rebuild_linked_chunk() {
562                let event_cache_store =
563                    get_event_cache_store().await.unwrap().into_event_cache_store();
564                event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
565            }
566
567            #[async_test]
568            async fn test_rebuild_empty_linked_chunk() {
569                let event_cache_store =
570                    get_event_cache_store().await.unwrap().into_event_cache_store();
571                event_cache_store.test_rebuild_empty_linked_chunk().await;
572            }
573
574            #[async_test]
575            async fn test_clear_all_rooms_chunks() {
576                let event_cache_store =
577                    get_event_cache_store().await.unwrap().into_event_cache_store();
578                event_cache_store.test_clear_all_rooms_chunks().await;
579            }
580
581            #[async_test]
582            async fn test_remove_room() {
583                let event_cache_store =
584                    get_event_cache_store().await.unwrap().into_event_cache_store();
585                event_cache_store.test_remove_room().await;
586            }
587        }
588    };
589}
590
591/// Macro generating tests for the event cache store, related to time (mostly
592/// for the cross-process lock).
593#[allow(unused_macros)]
594#[macro_export]
595macro_rules! event_cache_store_integration_tests_time {
596    () => {
597        #[cfg(not(target_arch = "wasm32"))]
598        mod event_cache_store_integration_tests_time {
599            use std::time::Duration;
600
601            use matrix_sdk_test::async_test;
602            use $crate::event_cache::store::IntoEventCacheStore;
603
604            use super::get_event_cache_store;
605
606            #[async_test]
607            async fn test_lease_locks() {
608                let store = get_event_cache_store().await.unwrap().into_event_cache_store();
609
610                let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
611                assert!(acquired0);
612
613                // Should extend the lease automatically (same holder).
614                let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
615                assert!(acquired2);
616
617                // Should extend the lease automatically (same holder + time is ok).
618                let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
619                assert!(acquired3);
620
621                // Another attempt at taking the lock should fail, because it's taken.
622                let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
623                assert!(!acquired4);
624
625                // Even if we insist.
626                let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
627                assert!(!acquired5);
628
629                // That's a nice test we got here, go take a little nap.
630                tokio::time::sleep(Duration::from_millis(50)).await;
631
632                // Still too early.
633                let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
634                assert!(!acquired55);
635
636                // Ok you can take another nap then.
637                tokio::time::sleep(Duration::from_millis(250)).await;
638
639                // At some point, we do get the lock.
640                let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
641                assert!(acquired6);
642
643                tokio::time::sleep(Duration::from_millis(1)).await;
644
645                // The other gets it almost immediately too.
646                let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
647                assert!(acquired7);
648
649                tokio::time::sleep(Duration::from_millis(1)).await;
650
651                // But when we take a longer lease...
652                let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
653                assert!(acquired8);
654
655                // It blocks the other user.
656                let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
657                assert!(!acquired9);
658
659                // We can hold onto our lease.
660                let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
661                assert!(acquired10);
662            }
663        }
664    };
665}