Skip to content

Commit 3d0e189

Browse files
committed
test: integ test for withhelds in history sharing
Add an integration test that ensures that the correct withheld code is sent when history is marked as "not shareable"
1 parent 70f6680 commit 3d0e189

File tree

1 file changed

+180
-3
lines changed

1 file changed

+180
-3
lines changed

testing/matrix-sdk-integration-testing/src/tests/e2ee/shared_history.rs

Lines changed: 180 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@ use assert_matches2::{assert_let, assert_matches};
55
use assign::assign;
66
use futures::{FutureExt, StreamExt, future, pin_mut};
77
use matrix_sdk::{
8-
assert_decrypted_message_eq, assert_next_with_timeout,
8+
Room, assert_decrypted_message_eq, assert_next_with_timeout,
99
deserialized_responses::TimelineEventKind,
1010
encryption::EncryptionSettings,
11+
room::power_levels::RoomPowerLevelChanges,
1112
ruma::{
13+
EventId,
1214
api::client::{
1315
room::create_room::v3::{Request as CreateRoomRequest, RoomPreset},
1416
uiaa::Password,
1517
},
16-
events::room::message::RoomMessageEventContent,
18+
events::room::{
19+
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
20+
message::RoomMessageEventContent,
21+
},
1722
},
1823
timeout::timeout,
1924
};
20-
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
25+
use matrix_sdk_common::deserialized_responses::{
26+
ProcessedToDeviceEvent, UnableToDecryptReason::MissingMegolmSession, WithheldCode,
27+
};
2128
use matrix_sdk_ui::sync_service::SyncService;
2229
use similar_asserts::assert_eq;
2330
use tracing::{Instrument, info};
@@ -360,3 +367,173 @@ async fn test_history_share_on_invite_pin_violation() -> Result<()> {
360367

361368
Ok(())
362369
}
370+
371+
/// Test history sharing where some sessions are withheld.
372+
///
373+
/// In this scenario we have three separate users:
374+
///
375+
/// 1. Alice and Bob share a room, where the history visibility is set to
376+
/// "shared".
377+
/// 2. Bob sends a message. This will be "shareable".
378+
/// 3. Alice changes the history viz to "joined".
379+
/// 4. Alice changes the history viz back to "shared", but Bob doesn't (yet)
380+
/// receive the memo.
381+
/// 5. Bob sends a second message; the key is "unshareable" because Bob still
382+
/// thinks the history viz is "joined".
383+
/// 6. Bob syncs, and sends a third message; the key is now "shareable".
384+
/// 7. Alice invites Charlie.
385+
/// 8. Charlie joins the room. He should see Bob's first message; the second
386+
/// should have an appropriate withheld code from Alice; the third should be
387+
/// decryptable.
388+
///
389+
/// This tests correct "withheld" code handling.
390+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
391+
async fn test_transitive_history_share_with_withhelds() -> Result<()> {
392+
let alice_span = tracing::info_span!("alice");
393+
let bob_span = tracing::info_span!("bob");
394+
let charlie_span = tracing::info_span!("charlie");
395+
396+
let alice = create_encryption_enabled_client("alice").instrument(alice_span.clone()).await?;
397+
let bob = create_encryption_enabled_client("bob").instrument(bob_span.clone()).await?;
398+
let charlie =
399+
create_encryption_enabled_client("charlie").instrument(charlie_span.clone()).await?;
400+
401+
// 1. Alice creates a room, and enables encryption
402+
let alice_room = alice
403+
.create_room(assign!(CreateRoomRequest::new(), {
404+
preset: Some(RoomPreset::PublicChat),
405+
}))
406+
.instrument(alice_span.clone())
407+
.await?;
408+
alice_room.enable_encryption().instrument(alice_span.clone()).await?;
409+
// Allow regular users to send invites
410+
alice.sync_once().instrument(alice_span.clone()).await?;
411+
alice_room
412+
.apply_power_level_changes(RoomPowerLevelChanges { invite: Some(0), ..Default::default() })
413+
.instrument(alice_span.clone())
414+
.await
415+
.expect("Should be able to set power levels");
416+
417+
info!(room_id = ?alice_room.room_id(), "Alice has created and enabled encryption in the room");
418+
419+
// ... and invites Bob to the room
420+
alice_room.invite_user_by_id(bob.user_id().unwrap()).instrument(alice_span.clone()).await?;
421+
422+
// Bob joins
423+
bob.sync_once().instrument(bob_span.clone()).await?;
424+
425+
let bob_room = bob
426+
.join_room_by_id(alice_room.room_id())
427+
.instrument(bob_span.clone())
428+
.await
429+
.expect("Bob should be able to accept the invitation from Alice");
430+
431+
// 2. Bob sends a message, which Alice should receive
432+
let assert_event_received = async |room: &Room, event_id: &EventId, expected_content: &str| {
433+
let event = room.event(event_id, None).await.unwrap_or_else(|err| {
434+
panic!("Should receive Bob's event with content '{expected_content}': {err:?}")
435+
});
436+
assert_decrypted_message_eq!(
437+
event,
438+
expected_content,
439+
"The decrypted event should match the message Bob has sent"
440+
);
441+
};
442+
443+
let bob_send_test_event = async |event_content: &str| {
444+
let bob_event_id = bob_room
445+
.send(RoomMessageEventContent::text_plain(event_content))
446+
.into_future()
447+
.instrument(bob_span.clone())
448+
.await
449+
.expect("We should be able to send a message to the room")
450+
.event_id;
451+
452+
alice
453+
.sync_once()
454+
.instrument(alice_span.clone())
455+
.await
456+
.expect("Alice should be able to sync");
457+
458+
assert_event_received(&alice_room, &bob_event_id, event_content).await;
459+
460+
bob_event_id
461+
};
462+
463+
let event_id_1 = bob_send_test_event("Event 1").await;
464+
465+
// 3. Alice changes the history visibility to "joined"
466+
alice_room
467+
.send_state_event(RoomHistoryVisibilityEventContent::new(HistoryVisibility::Joined))
468+
.into_future()
469+
.instrument(alice_span.clone())
470+
.await?;
471+
bob.sync_once().instrument(bob_span.clone()).await?;
472+
assert_eq!(bob_room.history_visibility(), Some(HistoryVisibility::Joined));
473+
474+
// 4. Alice changes the history visibility back to "shared", but Bob doesn't
475+
// know about it.
476+
alice_room
477+
.send_state_event(RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared))
478+
.into_future()
479+
.instrument(alice_span.clone())
480+
.await?;
481+
482+
// 5. Bob sends a second message; the key is "unshareable" because Bob still
483+
// thinks the history viz is "joined".
484+
assert_eq!(bob_room.history_visibility(), Some(HistoryVisibility::Joined));
485+
let event_id_2 = bob_send_test_event("Event 2").await;
486+
487+
// 6. Bob syncs, and sends a third message; the key is now "shareable".
488+
bob.sync_once().instrument(bob_span.clone()).await?;
489+
assert_eq!(bob_room.history_visibility(), Some(HistoryVisibility::Shared));
490+
let event_id_3 = bob_send_test_event("Event 3").await;
491+
492+
// 7. Alice invites Charlie.
493+
alice_room.invite_user_by_id(charlie.user_id().unwrap()).instrument(alice_span.clone()).await?;
494+
495+
// Workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5770: Charlie needs a copy of
496+
// Alice's identity.
497+
charlie
498+
.encryption()
499+
.request_user_identity(alice.user_id().unwrap())
500+
.instrument(charlie_span.clone())
501+
.await?;
502+
503+
// 8. Charlie joins the room
504+
charlie.sync_once().instrument(charlie_span.clone()).await?;
505+
let charlie_room = charlie
506+
.join_room_by_id(alice_room.room_id())
507+
.instrument(charlie_span.clone())
508+
.await
509+
.expect("Charlie should be able to accept the invitation from Alice");
510+
511+
// Events 1 and 3 should be decryptable; 2 should be "history not shared".
512+
assert_event_received(&charlie_room, &event_id_1, "Event 1").await;
513+
assert_event_received(&charlie_room, &event_id_3, "Event 3").await;
514+
let event = charlie_room.event(&event_id_2, None).await.expect("Should receive Bob's event 2");
515+
assert_let!(TimelineEventKind::UnableToDecrypt { utd_info, .. } = event.kind);
516+
assert_eq!(
517+
utd_info.reason,
518+
MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
519+
);
520+
521+
Ok(())
522+
}
523+
524+
async fn create_encryption_enabled_client(username: &str) -> Result<SyncTokenAwareClient> {
525+
let encryption_settings =
526+
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
527+
528+
let client = SyncTokenAwareClient::new(
529+
TestClientBuilder::new(username)
530+
.use_sqlite()
531+
.encryption_settings(encryption_settings)
532+
.enable_share_history_on_invite(true)
533+
.build()
534+
.await?,
535+
);
536+
537+
client.encryption().wait_for_e2ee_initialization_tasks().await;
538+
Ok(client)
539+
}

0 commit comments

Comments
 (0)