mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-08-04 03:52:19 -04:00
This patch extracts fields from `RoomEventCacheState` and move them into `RoomEventCacheStateLock`. This lock provides 2 methods: `read` and `write`, respectively to acquire a read-only lock, and a write-only lock, represented by the `RoomEventCacheStateLockReadGuard` and the `RoomEventCacheStateLockWriteGuard` types. All “public” methods on `RoomEventCacheState` now are facade to the read and write guards. This refactoring makes the code to compile with the last change in `EventCacheStore::lock`, which now returns a `EventCacheStoreLockState`. The next step is to re-load `RoomEventCacheStateLock` when the lock is dirty! But before doing that, we need this new mechanism to centralise the management of the store lock.
358 lines
16 KiB
Rust
358 lines
16 KiB
Rust
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use std::io::Write;
|
|
|
|
use assert_matches::assert_matches;
|
|
use eyeball_im::VectorDiff;
|
|
use matrix_sdk::{
|
|
assert_next_matches_with_timeout,
|
|
linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update},
|
|
test_utils::mocks::MatrixMockServer,
|
|
};
|
|
use matrix_sdk_test::{BOB, async_test, event_factory::EventFactory};
|
|
use matrix_sdk_ui::timeline::RoomExt;
|
|
use ruma::{
|
|
event_id,
|
|
events::room::encrypted::{
|
|
EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent,
|
|
},
|
|
room_id,
|
|
};
|
|
use stream_assert::assert_pending;
|
|
use tempfile::NamedTempFile;
|
|
|
|
/// The event cache can store Unable To Decrypt event
|
|
/// ([`TimelineEventKind::UnableToDecrypt`]). If such event is part of the
|
|
/// initial items of the `Timeline`, they are automatically decrypted if
|
|
/// possible.
|
|
///
|
|
/// [`TimelineEventKind::UnableToDecrypt`]: matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt
|
|
#[async_test]
|
|
async fn test_an_utd_from_the_event_cache_as_an_initial_item_is_decrypted() {
|
|
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
|
|
const SESSION_KEY: &[u8] = b"\
|
|
-----BEGIN MEGOLM SESSION DATA-----\n\
|
|
ASKcWoiAVUM97482UAi83Avce62hSLce7i5JhsqoF6xeAAAACqt2Cg3nyJPRWTTMXxXH7TXnkfdlmBXbQtq5\
|
|
bpHo3LRijcq2Gc6TXilESCmJN14pIsfKRJrWjZ0squ/XsoTFytuVLWwkNaW3QF6obeg2IoVtJXLMPdw3b2vO\
|
|
vgwGY3OMP0XafH13j1vcb6YLzvgLkZQLnYvd47hv3yK/9GmKS9tokuaQ7dCVYckYcIOS09EDTs70YdxUd5WG\
|
|
rQynATCLFP1p/NAGv70r9MK7Cy/mNpjD0r4qC7UEDIoi1kOWzHgnLo19wtvwsb8Fg8ATxcs3Wmtj8hIUYpDx\
|
|
ia4sM10zbytUuaPUAfCDf42IyxdmOnGe1CueXhgI71y+RW0s0argNqUt7jB70JT0o9CyX6UBGRaqLk2MPY9T\
|
|
hUu5J8X3UgIa6rcbWigzohzWm9rdbEHFrSWqjpfQYMaAKQQgETrjSy4XTrp2RhC2oNqG/hylI4ab+F4X6fpH\
|
|
DYP1NqNMP5g36xNu7LhDnrUB5qsPjYOmWORxGLfudpF3oLYCSlr3DgHqEIB6HjQblLZ3KQuPBse3zxyROTnS\
|
|
AhdPH4a/z1wioFtKNVph3hecsiKEdqnz4Y2coSIdhz58mJ9JWNQoFAENE5CSsoEZAGvafYZVpW4C75YY2zq1\
|
|
wIeiFi1dT43/jLAUGkslsi1VvnyfUu8qO404RxYO3XHoGLMFoFLOO+lZ+VGci2Vz10AhxJhEBHxRKxw4k2uB\
|
|
HztoSJUr/2Y\n\
|
|
-----END MEGOLM SESSION DATA-----";
|
|
|
|
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
|
|
let event_factory = EventFactory::new().room(room_id).sender(&BOB);
|
|
|
|
let mock_server = MatrixMockServer::new().await;
|
|
let client = mock_server.client_builder().build().await;
|
|
|
|
// Set up the event cache store.
|
|
{
|
|
let event_cache_store = client.event_cache_store().lock().await.unwrap();
|
|
|
|
// The event cache contains one chunk as such:
|
|
//
|
|
// 1. a chunk of 1 item
|
|
//
|
|
// The item is an encrypted event! It has been stored before having a chance to
|
|
// be decrypted. Damn. We want to see if decryption will trigger automatically.
|
|
event_cache_store
|
|
.as_clean()
|
|
.unwrap()
|
|
.handle_linked_chunk_updates(
|
|
LinkedChunkId::Room(room_id),
|
|
vec![
|
|
// chunk #1
|
|
Update::NewItemsChunk {
|
|
previous: None,
|
|
new: ChunkIdentifier::new(0),
|
|
next: None,
|
|
},
|
|
// … and its item
|
|
Update::PushItems {
|
|
at: Position::new(ChunkIdentifier::new(0), 0),
|
|
items: vec![event_factory
|
|
.event(RoomEncryptedEventContent::new(
|
|
EncryptedEventScheme::MegolmV1AesSha2(
|
|
MegolmV1AesSha2ContentInit {
|
|
ciphertext: "\
|
|
AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\
|
|
cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\
|
|
YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\
|
|
CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\
|
|
hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\
|
|
QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ"
|
|
.to_owned(),
|
|
sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA"
|
|
.to_owned(),
|
|
device_id: "NLAZCWIOCO".into(),
|
|
session_id: SESSION_ID.into(),
|
|
}
|
|
.into(),
|
|
),
|
|
None,
|
|
))
|
|
.event_id(event_id!("$ev0"))
|
|
.into_utd_sync_timeline_event(),
|
|
],
|
|
},
|
|
],
|
|
)
|
|
.await
|
|
.expect("Failed to setup the event cache");
|
|
}
|
|
|
|
// Import the key to decrypt the cached event.
|
|
{
|
|
let mut tempfile =
|
|
NamedTempFile::new().expect("Failed to create a temporary file for the keys");
|
|
tempfile.write_all(SESSION_KEY).expect("Failed to write the keys in the temporary file");
|
|
let tempfile_path = tempfile.into_temp_path();
|
|
|
|
let room_key_import_result = client
|
|
.encryption()
|
|
.import_room_keys(tempfile_path.to_path_buf(), "1234")
|
|
.await
|
|
.expect("Failed to import the keys");
|
|
assert_eq!(room_key_import_result.imported_count, 1);
|
|
}
|
|
|
|
// Set up the event cache.
|
|
let event_cache = client.event_cache();
|
|
event_cache.subscribe().unwrap();
|
|
|
|
let room = mock_server.sync_joined_room(&client, room_id).await;
|
|
let timeline = room.timeline().await.unwrap();
|
|
let (initial_updates, mut updates_stream) = timeline.subscribe().await;
|
|
|
|
assert_eq!(initial_updates.len(), 2);
|
|
|
|
// First item is the date divider.
|
|
assert!(&initial_updates[0].is_date_divider());
|
|
|
|
// Second item is `$ev0` but as an UTD!
|
|
assert_matches!(&initial_updates[1].as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
|
|
assert!(event.content().is_unable_to_decrypt());
|
|
});
|
|
|
|
// But wait… there is more!
|
|
assert_next_matches_with_timeout!(updates_stream, 250, updates => {
|
|
// Ho ho, one update?
|
|
assert_eq!(updates.len(), 1, "Expecting 1 update from the `Timeline`");
|
|
|
|
// Yes, the event is decrypted!
|
|
assert_matches!(&updates[0], VectorDiff::Set { index: 1, value: event } => {
|
|
assert_matches!(event.as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
|
|
assert_matches!(event.content().as_message(), Some(message) => {
|
|
assert_eq!(message.body(), "It's a secret to everybody");
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// That's all folks!
|
|
assert_pending!(updates_stream);
|
|
}
|
|
|
|
/// The event cache can store Unable To Decrypt event
|
|
/// ([`TimelineEventKind::UnableToDecrypt`]). If such event is part of the
|
|
/// paginated items of the `Timeline`, they are automatically decrypted if
|
|
/// possible.
|
|
///
|
|
/// [`TimelineEventKind::UnableToDecrypt`]: matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt
|
|
#[async_test]
|
|
async fn test_an_utd_from_the_event_cache_as_a_paginated_item_is_decrypted() {
|
|
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
|
|
const SESSION_KEY: &[u8] = b"\
|
|
-----BEGIN MEGOLM SESSION DATA-----\n\
|
|
ASKcWoiAVUM97482UAi83Avce62hSLce7i5JhsqoF6xeAAAACqt2Cg3nyJPRWTTMXxXH7TXnkfdlmBXbQtq5\
|
|
bpHo3LRijcq2Gc6TXilESCmJN14pIsfKRJrWjZ0squ/XsoTFytuVLWwkNaW3QF6obeg2IoVtJXLMPdw3b2vO\
|
|
vgwGY3OMP0XafH13j1vcb6YLzvgLkZQLnYvd47hv3yK/9GmKS9tokuaQ7dCVYckYcIOS09EDTs70YdxUd5WG\
|
|
rQynATCLFP1p/NAGv70r9MK7Cy/mNpjD0r4qC7UEDIoi1kOWzHgnLo19wtvwsb8Fg8ATxcs3Wmtj8hIUYpDx\
|
|
ia4sM10zbytUuaPUAfCDf42IyxdmOnGe1CueXhgI71y+RW0s0argNqUt7jB70JT0o9CyX6UBGRaqLk2MPY9T\
|
|
hUu5J8X3UgIa6rcbWigzohzWm9rdbEHFrSWqjpfQYMaAKQQgETrjSy4XTrp2RhC2oNqG/hylI4ab+F4X6fpH\
|
|
DYP1NqNMP5g36xNu7LhDnrUB5qsPjYOmWORxGLfudpF3oLYCSlr3DgHqEIB6HjQblLZ3KQuPBse3zxyROTnS\
|
|
AhdPH4a/z1wioFtKNVph3hecsiKEdqnz4Y2coSIdhz58mJ9JWNQoFAENE5CSsoEZAGvafYZVpW4C75YY2zq1\
|
|
wIeiFi1dT43/jLAUGkslsi1VvnyfUu8qO404RxYO3XHoGLMFoFLOO+lZ+VGci2Vz10AhxJhEBHxRKxw4k2uB\
|
|
HztoSJUr/2Y\n\
|
|
-----END MEGOLM SESSION DATA-----";
|
|
|
|
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
|
|
let event_factory = EventFactory::new().room(room_id).sender(&BOB);
|
|
|
|
let mock_server = MatrixMockServer::new().await;
|
|
let client = mock_server.client_builder().build().await;
|
|
|
|
// Set up the event cache store.
|
|
{
|
|
let event_cache_store = client.event_cache_store().lock().await.unwrap();
|
|
|
|
// The event cache contains one chunk as such:
|
|
//
|
|
// 1. a chunk of 1 item
|
|
// 2. a chunk of 1 item
|
|
//
|
|
// The older item is an encrypted event! It has been stored before having a
|
|
// chance to be decrypted. Damn. We want to see if decryption will trigger
|
|
// automatically.
|
|
event_cache_store
|
|
.as_clean()
|
|
.unwrap()
|
|
.handle_linked_chunk_updates(
|
|
LinkedChunkId::Room(room_id),
|
|
vec![
|
|
// chunk #1
|
|
Update::NewItemsChunk {
|
|
previous: None,
|
|
new: ChunkIdentifier::new(0),
|
|
next: None,
|
|
},
|
|
// … and its item
|
|
Update::PushItems {
|
|
at: Position::new(ChunkIdentifier::new(0), 0),
|
|
items: vec![event_factory
|
|
.event(RoomEncryptedEventContent::new(
|
|
EncryptedEventScheme::MegolmV1AesSha2(
|
|
MegolmV1AesSha2ContentInit {
|
|
ciphertext: "\
|
|
AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\
|
|
cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\
|
|
YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\
|
|
CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\
|
|
hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\
|
|
QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ"
|
|
.to_owned(),
|
|
sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA"
|
|
.to_owned(),
|
|
device_id: "NLAZCWIOCO".into(),
|
|
session_id: SESSION_ID.into(),
|
|
}
|
|
.into(),
|
|
),
|
|
None,
|
|
))
|
|
.event_id(event_id!("$ev0"))
|
|
.into_utd_sync_timeline_event(),
|
|
],
|
|
},
|
|
// chunk #2
|
|
Update::NewItemsChunk {
|
|
previous: Some(ChunkIdentifier::new(0)),
|
|
new: ChunkIdentifier::new(1),
|
|
next: None,
|
|
},
|
|
// … and its item
|
|
Update::PushItems {
|
|
at: Position::new(ChunkIdentifier::new(1), 0),
|
|
items: vec![event_factory
|
|
.text_msg("hello")
|
|
.event_id(event_id!("$ev1"))
|
|
.into_event()
|
|
]
|
|
}
|
|
],
|
|
)
|
|
.await
|
|
.expect("Failed to setup the event cache");
|
|
}
|
|
|
|
// Import the key to decrypt the cached event.
|
|
{
|
|
let mut tempfile =
|
|
NamedTempFile::new().expect("Failed to create a temporary file for the keys");
|
|
tempfile.write_all(SESSION_KEY).expect("Failed to write the keys in the temporary file");
|
|
let tempfile_path = tempfile.into_temp_path();
|
|
|
|
let room_key_import_result = client
|
|
.encryption()
|
|
.import_room_keys(tempfile_path.to_path_buf(), "1234")
|
|
.await
|
|
.expect("Failed to import the keys");
|
|
assert_eq!(room_key_import_result.imported_count, 1);
|
|
}
|
|
|
|
// Set up the event cache.
|
|
let event_cache = client.event_cache();
|
|
event_cache.subscribe().unwrap();
|
|
|
|
let room = mock_server.sync_joined_room(&client, room_id).await;
|
|
let timeline = room.timeline().await.unwrap();
|
|
let (initial_updates, mut updates_stream) = timeline.subscribe().await;
|
|
|
|
assert_eq!(initial_updates.len(), 2);
|
|
|
|
// First item is the date divider.
|
|
assert!(&initial_updates[0].is_date_divider());
|
|
|
|
// Second item is `$ev1`.
|
|
assert_matches!(&initial_updates[1].as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev1");
|
|
assert_matches!(event.content().as_message(), Some(message) => {
|
|
assert_eq!(message.body(), "hello");
|
|
});
|
|
});
|
|
|
|
// The stream is pending, because, well, everything is alright so far!
|
|
assert_pending!(updates_stream);
|
|
|
|
// Now we can paginate to load the UTD!
|
|
let reached_start = timeline.paginate_backwards(1).await.unwrap();
|
|
|
|
// We have reached the start of the timeline. Not really part of this test, but
|
|
// let's test everything :-).
|
|
assert!(reached_start);
|
|
|
|
assert_next_matches_with_timeout!(updates_stream, 250, updates => {
|
|
assert_eq!(updates.len(), 1, "We get the start of timeline item");
|
|
|
|
assert_matches!(&updates[0], VectorDiff::PushFront { value } => {
|
|
assert!(value.is_timeline_start());
|
|
});
|
|
});
|
|
|
|
// Now, let's look at the updates. We must observe an update reflecting the UTD
|
|
// has entered the `Timeline`.
|
|
assert_next_matches_with_timeout!(updates_stream, 250, updates => {
|
|
assert_eq!(updates.len(), 2, "Expecting 2 updates from the `Timeline`");
|
|
|
|
// UTD! UTD!
|
|
assert_matches!(&updates[0], VectorDiff::Insert { index: 2, value: event } => {
|
|
assert_matches!(event.as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
|
|
assert!(event.content().is_unable_to_decrypt());
|
|
});
|
|
});
|
|
|
|
// UTD is decrypted now!
|
|
assert_matches!(&updates[1], VectorDiff::Set { index: 2, value: event } => {
|
|
assert_matches!(event.as_event(), Some(event) => {
|
|
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
|
|
assert_matches!(event.content().as_message(), Some(message) => {
|
|
assert_eq!(message.body(), "It's a secret to everybody");
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// That's all folks!
|
|
assert_pending!(updates_stream);
|
|
}
|