tests: Assert room key rotated on leave event after being offline

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
Skye Elliot
2026-03-17 14:44:14 +00:00
parent 7a26db66b5
commit fd806a9c11
@@ -37,6 +37,7 @@ use matrix_sdk_ui::{
},
};
use similar_asserts::assert_eq;
use tempfile::tempdir;
use tracing::{Instrument, info};
use crate::{
@@ -1236,6 +1237,146 @@ async fn test_history_share_on_invite_room_key_rotation() -> Result<()> {
Ok(())
}
/// A variant of the above test that verifies the room key is rotated on member
/// leave even when the client shuts down for a short while, potentially
/// resulting in state deltas appearing in the `/sync` response, as opposed to
/// `timeline`.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_history_share_on_invite_room_key_rotation_with_shutdown() -> Result<()> {
let alice_span = tracing::info_span!("alice");
let bob_span = tracing::info_span!("bob");
let charlie_span = tracing::info_span!("charlie");
let alice =
create_encryption_enabled_client("alice", false).instrument(alice_span.clone()).await?;
let bob_sqlite_dir = tempdir()?;
let bob = SyncTokenAwareClient::new(
TestClientBuilder::new("bob")
.use_sqlite_dir(bob_sqlite_dir.path())
.encryption_settings(EncryptionSettings {
auto_enable_cross_signing: true,
..Default::default()
})
.enable_share_history_on_invite(true)
.build()
.await?,
);
bob.encryption().wait_for_e2ee_initialization_tasks().await;
let charlie = create_encryption_enabled_client("charlie", false)
.await
.expect("Failed to create Charlie's client");
// 1. Alice creates a room with `shared` history visibility and invites Bob.
let alice_room = alice
.create_room(assign!(CreateRoomRequest::new(), {
preset: Some(RoomPreset::PublicChat),
}))
.instrument(alice_span.clone())
.await?;
alice_room.enable_encryption().instrument(alice_span.clone()).await?;
alice_room.invite_user_by_id(bob.user_id().unwrap()).instrument(alice_span.clone()).await?;
bob.sync_once().instrument(bob_span.clone()).await?;
let bob_room = bob.join_room_by_id(alice_room.room_id()).instrument(bob_span.clone()).await?;
alice.sync_once().instrument(alice_span.clone()).await?;
// 2. Bob sends M1, which Charlie should be able to read later as Alice will
// send them a key bundle.
let event_id_a = bob_room
.send(RoomMessageEventContent::text_plain("Charlie is cool!"))
.into_future()
.instrument(bob_span.clone())
.await?
.response
.event_id;
// Store the session ID for later comparison.
let event_m1 = bob_room.event(&event_id_a, None).instrument(bob_span.clone()).await?;
let event_m1_session_id = event_m1
.encryption_info()
.and_then(|info| info.session_id())
.expect("Bob should be able to check the session ID of event M1");
// 4. Alice invites Charlie; Charlie joins and receives the keys for M1.
alice.sync_once().instrument(alice_span.clone()).await?;
alice_room.invite_user_by_id(charlie.user_id().unwrap()).instrument(alice_span.clone()).await?;
let sync_response = charlie.sync_once().instrument(charlie_span.clone()).await?;
assert_received_room_key_bundle(sync_response);
let charlie_room =
charlie.join_room_by_id(alice_room.room_id()).instrument(charlie_span.clone()).await?;
charlie.sync_once().instrument(charlie_span.clone()).await?;
// Sanity check: Charlie can decrypt message M1 via the bundle.
let event_a = charlie_room.event(&event_id_a, None).instrument(charlie_span.clone()).await?;
assert!(
event_a.encryption_info().is_some(),
"Charlie should be able to decrypt message M1 via the key bundle"
);
// 5. Charlie leaves the room.
charlie_room.leave().instrument(charlie_span.clone()).await?;
// 6. Bob starts back up, and syncs to learn about Charlie's departure, which
// should trigger key rotation.
let bob = SyncTokenAwareClient::new(
TestClientBuilder::new("bob")
.use_sqlite_dir(bob_sqlite_dir.path())
.encryption_settings(EncryptionSettings {
auto_enable_cross_signing: true,
..Default::default()
})
.enable_share_history_on_invite(true)
.duplicate(&bob)
.instrument(bob_span.clone())
.await?,
);
bob.encryption().wait_for_e2ee_initialization_tasks().await;
bob.sync_once().instrument(bob_span.clone()).await?;
let bob_room = bob.join_room_by_id(alice_room.room_id()).instrument(bob_span.clone()).await?;
// 7. Bob sends M2. Because key rotation should have been performed, this should
// be using a fresh session that hasn't been shared with Charlie.
let event_id_b = bob_room
.send(RoomMessageEventContent::text_plain("Charlie is mean!"))
.into_future()
.instrument(bob_span.clone())
.await?
.response
.event_id;
// Ensure the two session IDs of M1 and M2 are different
let event_b = bob_room.event(&event_id_b, None).instrument(bob_span.clone()).await?;
let event_m2_session_id = event_b
.encryption_info()
.and_then(|info| info.session_id())
.expect("Bob should be able to check the session ID of event M2");
assert_ne!(event_m1_session_id, event_m2_session_id, "Session was not rotated");
// 8. Charlie rejoins the room via ID.
charlie.sync_once().instrument(charlie_span.clone()).await?;
let charlie_room =
charlie.join_room_by_id(alice_room.room_id()).instrument(charlie_span.clone()).await?;
// 9. Charlie attempts to decrypt M2. He should not be able to, because the
// session was rotated after he left the room.
let event_b = charlie_room.event(&event_id_b, None).instrument(charlie_span.clone()).await?;
assert!(
event_b.encryption_info().is_none(),
"Charlie should not be able to decrypt message M2 after rejoining"
);
Ok(())
}
/// Creates a new encryption-enabled client with the given username and
/// settings.
///