test: use a Timeline for shared_history integ tests

I want to be able to test that the correct `UtdCause` is presented for withheld
historical messages. That means we need to use `/sync` rather than `/event` to
obtain the message (since the MSC4115 `membership` field is missing on `/event`
(https://github.com/element-hq/synapse/issues/17486)). So then the most
realistic way to get hold of the actual UtdCause is to use a Timeline.

Of course, the thing I actually want to test doesn't actually work correctly,
so it's left as a FIXME in this commit.
This commit is contained in:
Richard van der Hoff
2025-11-05 13:26:15 +00:00
parent b4ebc8bc25
commit 8f726e4fb9
@@ -1,11 +1,12 @@
use std::{ops::Deref, time::Duration};
use std::{ops::Deref, sync::Arc, time::Duration};
use anyhow::Result;
use assert_matches2::{assert_let, assert_matches};
use assign::assign;
use eyeball_im::VectorDiff;
use futures::{FutureExt, StreamExt, future, pin_mut};
use matrix_sdk::{
Room, assert_decrypted_message_eq, assert_next_with_timeout,
assert_decrypted_message_eq, assert_next_with_timeout,
deserialized_responses::TimelineEventKind,
encryption::EncryptionSettings,
room::power_levels::RoomPowerLevelChanges,
@@ -25,7 +26,13 @@ use matrix_sdk::{
use matrix_sdk_common::deserialized_responses::{
ProcessedToDeviceEvent, UnableToDecryptReason::MissingMegolmSession, WithheldCode,
};
use matrix_sdk_ui::sync_service::SyncService;
use matrix_sdk_ui::{
Timeline,
sync_service::SyncService,
timeline::{
EncryptedMessage, MsgLikeContent, MsgLikeKind, RoomExt, TimelineItem, TimelineItemContent,
},
};
use similar_asserts::assert_eq;
use tracing::{Instrument, info};
@@ -409,6 +416,8 @@ async fn test_transitive_history_share_with_withhelds() -> Result<()> {
}))
.instrument(alice_span.clone())
.await?;
let alice_timeline = alice_room.timeline().await?;
alice_room.enable_encryption().instrument(alice_span.clone()).await?;
// Allow regular users to send invites
alice.sync_once().instrument(alice_span.clone()).await?;
@@ -433,17 +442,6 @@ async fn test_transitive_history_share_with_withhelds() -> Result<()> {
.expect("Bob should be able to accept the invitation from Alice");
// 2. Bob sends a message, which Alice should receive
let assert_event_received = async |room: &Room, event_id: &EventId, expected_content: &str| {
let event = room.event(event_id, None).await.unwrap_or_else(|err| {
panic!("Should receive Bob's event with content '{expected_content}': {err:?}")
});
assert_decrypted_message_eq!(
event,
expected_content,
"The decrypted event should match the message Bob has sent"
);
};
let bob_send_test_event = async |event_content: &str| {
let bob_event_id = bob_room
.send(RoomMessageEventContent::text_plain(event_content))
@@ -459,7 +457,7 @@ async fn test_transitive_history_share_with_withhelds() -> Result<()> {
.await
.expect("Alice should be able to sync");
assert_event_received(&alice_room, &bob_event_id, event_content).await;
assert_event_received(&alice_timeline, &bob_event_id, event_content).await;
bob_event_id
};
@@ -512,15 +510,13 @@ async fn test_transitive_history_share_with_withhelds() -> Result<()> {
.await
.expect("Charlie should be able to accept the invitation from Alice");
let charlie_timeline = charlie_room.timeline().await?;
// Events 1 and 3 should be decryptable; 2 should be "history not shared".
assert_event_received(&charlie_room, &event_id_1, "Event 1").await;
assert_event_received(&charlie_room, &event_id_3, "Event 3").await;
let event = charlie_room.event(&event_id_2, None).await.expect("Should receive Bob's event 2");
assert_let!(TimelineEventKind::UnableToDecrypt { utd_info, .. } = event.kind);
assert_eq!(
utd_info.reason,
MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
);
charlie.sync_once().instrument(charlie_span.clone()).await?;
assert_event_received(&charlie_timeline, &event_id_1, "Event 1").await;
assert_event_received(&charlie_timeline, &event_id_3, "Event 3").await;
assert_utd_history_not_shared(&charlie_timeline, &event_id_2).await;
// 9. Charlie invites Derek.
charlie_room
@@ -544,16 +540,15 @@ async fn test_transitive_history_share_with_withhelds() -> Result<()> {
.await
.expect("Derek should be able to accept the invitation from Charlie");
let derek_timeline = derek_room.timeline().await?;
// As for Charlie: events 1 and 3 should be decryptable; 2 should be "history
// not shared".
assert_event_received(&derek_room, &event_id_1, "Event 1").await;
assert_event_received(&derek_room, &event_id_3, "Event 3").await;
let event = derek_room.event(&event_id_2, None).await.expect("Should receive Bob's event 2");
assert_let!(TimelineEventKind::UnableToDecrypt { utd_info, .. } = event.kind);
assert_eq!(
utd_info.reason,
MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
);
derek.sync_once().instrument(derek_span.clone()).await?;
assert_event_received(&derek_timeline, &event_id_1, "Event 1").await;
assert_event_received(&derek_timeline, &event_id_3, "Event 3").await;
assert_utd_history_not_shared(&derek_timeline, &event_id_2).await;
Ok(())
}
@@ -573,3 +568,107 @@ async fn create_encryption_enabled_client(username: &str) -> Result<SyncTokenAwa
client.encryption().wait_for_e2ee_initialization_tasks().await;
Ok(client)
}
/**
* Wait for an event with the given ID to appear in the timeline.
*
* This function will wait for an event to appear in the timeline, and then
* return it. If the event doesn't appear within the given timeout, it will
* return `None`.
*/
async fn wait_for_timeline_event(
timeline: &Timeline,
event_id: &EventId,
) -> Option<Arc<TimelineItem>> {
let predicate =
|item: &Arc<TimelineItem>| item.as_event().and_then(|e| e.event_id()) == Some(event_id);
let (items, stream) = timeline.subscribe().await;
// If a matching event is already in the timeline, return it.
if let Some(event) = items.into_iter().find(|item| predicate(item)) {
return Some(event);
}
// Otherwise, wait for it to arrive.
pin_mut!(stream);
loop {
let diffs = match tokio::time::timeout(Duration::from_millis(500), stream.next()).await {
Err(_) => return None, // We timed out while waiting for an event to arrive.
Ok(None) => panic!("Stream ended unexpectedly"),
Ok(Some(diffs)) => diffs,
};
for diff in diffs {
let matched_event = match diff {
VectorDiff::Append { values } | VectorDiff::Reset { values } => {
values.into_iter().find(predicate)
}
VectorDiff::PushBack { value }
| VectorDiff::PushFront { value }
| VectorDiff::Insert { value, .. }
| VectorDiff::Set { value, .. } => predicate(&value).then_some(value),
_ => None,
};
if let Some(event) = matched_event {
return Some(event);
}
}
}
}
/**
* Wait for the given event to arrive in the timeline, and assert that its
* content matches that given.
*/
async fn assert_event_received(timeline: &Timeline, event_id: &EventId, expected_content: &str) {
let timeline_item = wait_for_timeline_event(timeline, event_id).await.unwrap_or_else(|| {
panic!("Timeout waiting for event {event_id} with content {expected_content} to arrive")
});
assert_let!(
TimelineItemContent::MsgLike(msg_like_content) =
timeline_item.as_event().unwrap().content()
);
assert_let!(MsgLikeContent { kind: MsgLikeKind::Message(message), .. } = msg_like_content);
assert_eq!(
message.body(),
expected_content,
"The decrypted event should match the message Bob has sent"
);
}
/**
* Assert that the given event is a UTD, with a withheld code of
* "history_not_shared", and an appropriate UtdCause.
*/
async fn assert_utd_history_not_shared(timeline: &Timeline, event_id: &EventId) {
let timeline_item = wait_for_timeline_event(timeline, event_id)
.await
.unwrap_or_else(|| panic!("Timeout waiting for Bob's withheld event {event_id} to arrive"));
assert_let!(
TimelineItemContent::MsgLike(msg_like_content) =
timeline_item.as_event().unwrap().content()
);
assert_let!(
MsgLikeContent { kind: MsgLikeKind::UnableToDecrypt(encrypted), .. } = msg_like_content
);
assert_let!(EncryptedMessage::MegolmV1AesSha2 { cause, .. } = encrypted);
// It should be reported in the UI as a regular "You don't have access to this
// event".
// FIXME: this doesn't work yet.
// assert_eq!(*cause, UtdCause::SentBeforeWeJoined);
// The timeline interface doesn't expose the raw withheld code, so call
// `Room::event` to find it.
let event =
timeline.room().event(event_id, None).await.expect("Should receive Bob's withheld event");
assert_let!(TimelineEventKind::UnableToDecrypt { utd_info, .. } = event.kind);
assert_eq!(
utd_info.reason,
MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
);
}