Skip to content

Commit c008db8

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 a0f07b1 commit c008db8

File tree

1 file changed

+178
-3
lines changed

1 file changed

+178
-3
lines changed

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

Lines changed: 178 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,171 @@ 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+
.instrument(alice_span.clone())
469+
.await?;
470+
bob.sync_once().instrument(bob_span.clone()).await?;
471+
assert_eq!(bob_room.history_visibility(), Some(HistoryVisibility::Joined));
472+
473+
// 4. Alice changes the history visibility back to "shared", but Bob doesn't
474+
// know about it.
475+
alice_room
476+
.send_state_event(RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared))
477+
.instrument(alice_span.clone())
478+
.await?;
479+
480+
// 5. Bob sends a second message; the key is "unshareable" because Bob still
481+
// thinks the history viz is "joined".
482+
assert_eq!(bob_room.history_visibility(), Some(HistoryVisibility::Joined));
483+
let event_id_2 = bob_send_test_event("Event 2").await;
484+
485+
// 6. Bob syncs, and sends a third message; the key is now "shareable".
486+
bob.sync_once().instrument(bob_span.clone()).await?;
487+
assert_eq!(bob_room.history_visibility(), Some(HistoryVisibility::Shared));
488+
let event_id_3 = bob_send_test_event("Event 3").await;
489+
490+
// 7. Alice invites Charlie.
491+
alice_room.invite_user_by_id(charlie.user_id().unwrap()).instrument(alice_span.clone()).await?;
492+
493+
// Workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5770: Charlie needs a copy of
494+
// Alice's identity.
495+
charlie
496+
.encryption()
497+
.request_user_identity(alice.user_id().unwrap())
498+
.instrument(charlie_span.clone())
499+
.await?;
500+
501+
// 8. Charlie joins the room
502+
charlie.sync_once().instrument(charlie_span.clone()).await?;
503+
let charlie_room = charlie
504+
.join_room_by_id(alice_room.room_id())
505+
.instrument(charlie_span.clone())
506+
.await
507+
.expect("Charlie should be able to accept the invitation from Alice");
508+
509+
// Events 1 and 3 should be decryptable; 2 should be "history not shared".
510+
assert_event_received(&charlie_room, &event_id_1, "Event 1").await;
511+
assert_event_received(&charlie_room, &event_id_3, "Event 3").await;
512+
let event = charlie_room.event(&event_id_2, None).await.expect("Should receive Bob's event 2");
513+
assert_let!(TimelineEventKind::UnableToDecrypt { utd_info, .. } = event.kind);
514+
assert_eq!(
515+
utd_info.reason,
516+
MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
517+
);
518+
519+
Ok(())
520+
}
521+
522+
async fn create_encryption_enabled_client(username: &str) -> Result<SyncTokenAwareClient> {
523+
let encryption_settings =
524+
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
525+
526+
let client = SyncTokenAwareClient::new(
527+
TestClientBuilder::new(username)
528+
.use_sqlite()
529+
.encryption_settings(encryption_settings)
530+
.enable_share_history_on_invite(true)
531+
.build()
532+
.await?,
533+
);
534+
535+
client.encryption().wait_for_e2ee_initialization_tasks().await;
536+
Ok(client)
537+
}

0 commit comments

Comments
 (0)