Skip to content

Commit aa07079

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 d9a1c86 commit aa07079

File tree

1 file changed

+179
-3
lines changed

1 file changed

+179
-3
lines changed

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

Lines changed: 179 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,172 @@ 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
434+
.event(event_id, None)
435+
.await
436+
.expect(&format!("Should receive Bob's event with content '{}'", expected_content));
437+
assert_decrypted_message_eq!(
438+
event,
439+
expected_content,
440+
"The decrypted event should match the message Bob has sent"
441+
);
442+
};
443+
444+
let bob_send_test_event = async |event_content: &str| {
445+
let bob_event_id = bob_room
446+
.send(RoomMessageEventContent::text_plain(event_content))
447+
.into_future()
448+
.instrument(bob_span.clone())
449+
.await
450+
.expect("We should be able to send a message to the room")
451+
.event_id;
452+
453+
alice
454+
.sync_once()
455+
.instrument(alice_span.clone())
456+
.await
457+
.expect("Alice should be able to sync");
458+
459+
assert_event_received(&alice_room, &bob_event_id, event_content).await;
460+
461+
bob_event_id
462+
};
463+
464+
let event_id_1 = bob_send_test_event("Event 1").await;
465+
466+
// 3. Alice changes the history visibility to "joined"
467+
alice_room
468+
.send_state_event(RoomHistoryVisibilityEventContent::new(HistoryVisibility::Joined))
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+
.instrument(alice_span.clone())
479+
.await?;
480+
481+
// 5. Bob sends a second message; the key is "unshareable" because Bob still
482+
// thinks the history viz is "joined".
483+
assert_eq!(bob_room.history_visibility(), Some(HistoryVisibility::Joined));
484+
let event_id_2 = bob_send_test_event("Event 2").await;
485+
486+
// 6. Bob syncs, and sends a third message; the key is now "shareable".
487+
bob.sync_once().instrument(bob_span.clone()).await?;
488+
assert_eq!(bob_room.history_visibility(), Some(HistoryVisibility::Shared));
489+
let event_id_3 = bob_send_test_event("Event 3").await;
490+
491+
// 7. Alice invites Charlie.
492+
alice_room.invite_user_by_id(charlie.user_id().unwrap()).instrument(alice_span.clone()).await?;
493+
494+
// Workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5770: Charlie needs a copy of
495+
// Alice's identity.
496+
charlie
497+
.encryption()
498+
.request_user_identity(alice.user_id().unwrap())
499+
.instrument(charlie_span.clone())
500+
.await?;
501+
502+
// 8. Charlie joins the room
503+
charlie.sync_once().instrument(charlie_span.clone()).await?;
504+
let charlie_room = charlie
505+
.join_room_by_id(alice_room.room_id())
506+
.instrument(charlie_span.clone())
507+
.await
508+
.expect("Charlie should be able to accept the invitation from Alice");
509+
510+
// Events 1 and 3 should be decryptable; 2 should be "history not shared".
511+
assert_event_received(&charlie_room, &event_id_1, "Event 1").await;
512+
assert_event_received(&charlie_room, &event_id_3, "Event 3").await;
513+
let event = charlie_room.event(&event_id_2, None).await.expect("Should receive Bob's event 2");
514+
assert_let!(TimelineEventKind::UnableToDecrypt { utd_info, .. } = event.kind);
515+
assert_eq!(
516+
utd_info.reason,
517+
MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
518+
);
519+
520+
Ok(())
521+
}
522+
523+
async fn create_encryption_enabled_client(username: &str) -> Result<SyncTokenAwareClient> {
524+
let encryption_settings =
525+
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
526+
527+
let client = SyncTokenAwareClient::new(
528+
TestClientBuilder::new(username)
529+
.use_sqlite()
530+
.encryption_settings(encryption_settings)
531+
.enable_share_history_on_invite(true)
532+
.build()
533+
.await?,
534+
);
535+
536+
client.encryption().wait_for_e2ee_initialization_tasks().await;
537+
Ok(client)
538+
}

0 commit comments

Comments
 (0)