use std::{collections::BTreeMap, ops::Not as _, time::Duration};
use assert_matches2::{assert_let, assert_matches};
use eyeball_im::VectorDiff;
use futures_util::FutureExt;
use matrix_sdk::{
Client, Error, MemoryStore, SlidingSyncList, StateChanges, StateStore, ThreadingSupport,
authentication::oauth::{OAuthError, error::OAuthTokenRevocationError},
config::{RequestConfig, StoreConfig, SyncSettings, SyncToken},
sleep::sleep,
store::{RoomLoadSettings, ThreadSubscriptionStatus},
sync::{RoomUpdate, State},
test_utils::{
client::mock_matrix_session, mocks::MatrixMockServer, no_retry_test_client_with_server,
},
};
use matrix_sdk_base::{RoomState, sync::RoomUpdates};
use matrix_sdk_common::executor::spawn;
use matrix_sdk_test::{
DEFAULT_TEST_ROOM_ID, InvitedRoomBuilder, JoinedRoomBuilder, StateTestEvent,
StrippedStateTestEvent, SyncResponseBuilder, async_test,
event_factory::EventFactory,
sync_state_event,
test_json::{
self, TAG,
sync::{
MIXED_INVITED_ROOM_ID, MIXED_JOINED_ROOM_ID, MIXED_KNOCKED_ROOM_ID, MIXED_LEFT_ROOM_ID,
MIXED_SYNC,
},
sync_events::PINNED_EVENTS,
},
};
use ruma::{
EventId, OwnedUserId, RoomId,
api::client::{
directory::{
get_public_rooms,
get_public_rooms_filtered::{self, v3::Request as PublicRoomsFilterRequest},
},
sync::sync_events::v5,
threads::get_thread_subscriptions_changes::unstable::{
ThreadSubscription, ThreadUnsubscription,
},
uiaa,
},
assign, device_id,
directory::Filter,
event_id,
events::{
AnyInitialStateEvent,
direct::{DirectEventContent, OwnedDirectUserIdentifier},
room::{
encrypted::OriginalSyncRoomEncryptedEvent, history_visibility::HistoryVisibility,
member::MembershipState,
},
},
owned_event_id, owned_room_id,
room::JoinRule,
room_id,
serde::Raw,
uint, user_id,
};
use serde_json::{Value as JsonValue, json};
use stream_assert::{assert_next_matches, assert_pending};
use tempfile::tempdir;
use tokio_stream::wrappers::BroadcastStream;
use wiremock::{
Mock, Request, ResponseTemplate,
matchers::{header, method, path, path_regex},
};
use crate::{logged_in_client_with_server, mock_sync};
#[async_test]
async fn test_sync() {
let (client, server) = logged_in_client_with_server().await;
mock_sync(&server, &*test_json::SYNC, None).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
let response = client.sync_once(sync_settings).await.unwrap();
assert_ne!(response.next_batch, "");
}
#[async_test]
async fn test_devices() {
let (client, server) = logged_in_client_with_server().await;
Mock::given(method("GET"))
.and(path("/_matrix/client/r0/devices"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::DEVICES))
.mount(&server)
.await;
client.devices().await.unwrap();
}
#[async_test]
async fn test_delete_devices() {
let (client, server) = no_retry_test_client_with_server().await;
Mock::given(method("POST"))
.and(path("/_matrix/client/r0/delete_devices"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"flows": [
{
"stages": [
"m.login.password"
]
}
],
"params": {},
"session": "vBslorikviAjxzYBASOBGfPp"
})))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/_matrix/client/r0/delete_devices"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"flows": [
{
"stages": [
"m.login.password"
]
}
],
"params": {},
"session": "vBslorikviAjxzYBASOBGfPp"
})))
.mount(&server)
.await;
let devices = &[device_id!("DEVICEID").to_owned()];
if let Err(e) = client.delete_devices(devices, None).await
&& let Some(info) = e.as_uiaa_response()
{
let mut auth_parameters = BTreeMap::new();
let identifier = json!({
"type": "m.id.user",
"user": "example",
});
auth_parameters.insert("identifier".to_owned(), identifier);
auth_parameters.insert("password".to_owned(), "wordpass".into());
let auth_data = uiaa::AuthData::Password(assign!(
uiaa::Password::new(
uiaa::UserIdentifier::UserIdOrLocalpart("example".to_owned()),
"wordpass".to_owned(),
), {
session: info.session.clone(),
}
));
client.delete_devices(devices, Some(auth_data)).await.unwrap();
}
}
#[async_test]
async fn test_resolve_room_alias() {
let (client, server) = no_retry_test_client_with_server().await;
Mock::given(method("GET"))
.and(path("/_matrix/client/r0/directory/room/%23alias:example.org"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::GET_ALIAS))
.mount(&server)
.await;
let alias = ruma::room_alias_id!("#alias:example.org");
client.resolve_room_alias(alias).await.unwrap();
}
#[async_test]
async fn test_join_leave_room() {
let (client, server) = logged_in_client_with_server().await;
mock_sync(&server, &*test_json::SYNC, None).await;
let room = client.get_room(&DEFAULT_TEST_ROOM_ID);
assert!(room.is_none());
let sync_token = client.sync_once(SyncSettings::default()).await.unwrap().next_batch;
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
assert_eq!(room.state(), RoomState::Joined);
mock_sync(&server, &*test_json::LEAVE_SYNC_EVENT, Some(sync_token.clone())).await;
client.sync_once(SyncSettings::default().token(sync_token)).await.unwrap();
assert_eq!(room.state(), RoomState::Left);
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
assert_eq!(room.state(), RoomState::Left);
}
#[async_test]
async fn test_join_room_by_id() {
let (client, server) = logged_in_client_with_server().await;
Mock::given(method("POST"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/join"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_ID))
.mount(&server)
.await;
assert_eq!(
client.join_room_by_id(&DEFAULT_TEST_ROOM_ID).await.unwrap().room_id(),
*DEFAULT_TEST_ROOM_ID
);
}
#[async_test]
async fn test_join_room_by_id_or_alias() {
let (client, server) = logged_in_client_with_server().await;
Mock::given(method("POST"))
.and(path_regex(r"^/_matrix/client/r0/join/"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_ID))
.mount(&server)
.await;
assert_eq!(
client
.join_room_by_id_or_alias(
(&**DEFAULT_TEST_ROOM_ID).into(),
&["server.com".try_into().unwrap()]
)
.await
.unwrap()
.room_id(),
*DEFAULT_TEST_ROOM_ID
);
}
#[async_test]
async fn test_room_search_all() {
let (client, server) = no_retry_test_client_with_server().await;
Mock::given(method("GET"))
.and(path("/_matrix/client/r0/publicRooms"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS))
.mount(&server)
.await;
let get_public_rooms::v3::Response { chunk, .. } =
client.public_rooms(Some(10), None, None).await.unwrap();
assert_eq!(chunk.len(), 1);
}
#[async_test]
async fn test_room_search_filtered() {
let (client, server) = logged_in_client_with_server().await;
Mock::given(method("POST"))
.and(path("/_matrix/client/r0/publicRooms"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::PUBLIC_ROOMS))
.mount(&server)
.await;
let generic_search_term = Some("cheese".to_owned());
let filter = assign!(Filter::new(), { generic_search_term });
let request = assign!(PublicRoomsFilterRequest::new(), { filter });
let get_public_rooms_filtered::v3::Response { chunk, .. } =
client.public_rooms_filtered(request).await.unwrap();
assert_eq!(chunk.len(), 1);
}
#[async_test]
async fn test_invited_rooms() {
let (client, server) = logged_in_client_with_server().await;
mock_sync(&server, &*test_json::INVITE_SYNC, None).await;
let _response = client.sync_once(SyncSettings::default()).await.unwrap();
assert!(client.joined_rooms().is_empty());
assert!(client.left_rooms().is_empty());
assert!(!client.invited_rooms().is_empty());
assert!(client.joined_space_rooms().is_empty());
let room = client.get_room(room_id!("!696r7674:example.com")).unwrap();
assert_eq!(room.state(), RoomState::Invited);
}
#[async_test]
async fn test_left_rooms() {
let (client, server) = logged_in_client_with_server().await;
mock_sync(&server, &*test_json::LEAVE_SYNC, None).await;
let _response = client.sync_once(SyncSettings::default()).await.unwrap();
assert!(client.joined_rooms().is_empty());
assert!(!client.left_rooms().is_empty());
assert!(client.invited_rooms().is_empty());
assert!(client.joined_space_rooms().is_empty());
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
assert_eq!(room.state(), RoomState::Left);
}
#[async_test]
async fn test_joined_space_rooms() {
let (client, server) = logged_in_client_with_server().await;
mock_sync(&server, &*test_json::JOIN_SPACE_SYNC, None).await;
let _response = client.sync_once(SyncSettings::default()).await.unwrap();
assert!(!client.joined_rooms().is_empty());
assert!(!client.joined_space_rooms().is_empty());
assert!(client.left_rooms().is_empty());
assert!(client.invited_rooms().is_empty());
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
assert_eq!(room.state(), RoomState::Joined);
assert!(room.is_space());
}
#[async_test]
async fn test_whoami() {
let (client, server) = logged_in_client_with_server().await;
Mock::given(method("GET"))
.and(path("/_matrix/client/r0/account/whoami"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::WHOAMI))
.mount(&server)
.await;
let user_id = user_id!("@joe:example.org");
assert_eq!(client.whoami().await.unwrap().user_id, user_id);
}
#[async_test]
async fn test_room_update_channel() {
let (client, server) = logged_in_client_with_server().await;
let mut rx = client.subscribe_to_room_updates(&DEFAULT_TEST_ROOM_ID);
mock_sync(&server, &*test_json::SYNC, None).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
client.sync_once(sync_settings).await.unwrap();
let update = rx.recv().now_or_never().unwrap().unwrap();
assert_let!(RoomUpdate::Joined { updates, .. } = update);
assert_eq!(updates.account_data.len(), 1);
assert_eq!(updates.ephemeral.len(), 1);
assert_matches!(updates.state, State::Before(state_events));
assert_eq!(state_events.len(), 9);
assert!(updates.timeline.limited);
assert_eq!(updates.timeline.events.len(), 1);
assert_eq!(updates.timeline.prev_batch, Some("t392-516_47314_0_7_1_1_1_11444_1".to_owned()));
assert_eq!(updates.unread_notifications.highlight_count, 0);
assert_eq!(updates.unread_notifications.notification_count, 11);
}
#[async_test]
async fn test_subscribe_all_room_updates() {
let (client, server) = logged_in_client_with_server().await;
let mut rx = client.subscribe_to_all_room_updates();
mock_sync(&server, &*MIXED_SYNC, None).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
client.sync_once(sync_settings).await.unwrap();
let room_updates = rx.recv().now_or_never().unwrap().unwrap();
assert_let!(RoomUpdates { left, joined, invited, knocked } = room_updates);
{
assert_eq!(left.len(), 1);
let (room_id, update) = left.iter().next().unwrap();
assert_eq!(room_id, *MIXED_LEFT_ROOM_ID);
assert_matches!(&update.state, State::Before(state_events));
assert!(state_events.is_empty());
assert_eq!(update.timeline.events.len(), 1);
assert!(update.account_data.is_empty());
}
{
assert_eq!(joined.len(), 1);
let (room_id, update) = joined.iter().next().unwrap();
assert_eq!(room_id, *MIXED_JOINED_ROOM_ID);
assert_eq!(update.account_data.len(), 1);
assert_eq!(update.ephemeral.len(), 1);
assert_matches!(&update.state, State::Before(state_events));
assert_eq!(state_events.len(), 1);
assert!(update.timeline.limited);
assert_eq!(update.timeline.events.len(), 1);
assert_eq!(update.timeline.prev_batch, Some("t392-516_47314_0_7_1_1_1_11444_1".to_owned()));
assert_eq!(update.unread_notifications.highlight_count, 0);
assert_eq!(update.unread_notifications.notification_count, 11);
}
{
assert_eq!(invited.len(), 1);
let (room_id, update) = invited.iter().next().unwrap();
assert_eq!(room_id, *MIXED_INVITED_ROOM_ID);
assert_eq!(update.invite_state.events.len(), 2);
}
{
assert_eq!(knocked.len(), 1);
let (room_id, update) = knocked.iter().next().unwrap();
assert_eq!(room_id, *MIXED_KNOCKED_ROOM_ID);
assert_eq!(update.knock_state.events.len(), 2);
}
}
#[cfg(all(feature = "e2e-encryption", not(target_family = "wasm")))]
#[async_test]
async fn test_request_encryption_event_before_sending() {
let (client, server) = logged_in_client_with_server().await;
mock_sync(&server, &*test_json::SYNC, None).await;
client
.sync_once(SyncSettings::default())
.await
.expect("We should be able to performs an initial sync");
let room =
client.get_room(&DEFAULT_TEST_ROOM_ID).expect("We should know about our default room");
Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.encryption/"))
.and(header("authorization", "Bearer 1234"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({
"algorithm": "m.megolm.v1.aes-sha2",
"rotation_period_ms": 604800000,
"rotation_period_msgs": 100
}))
.set_delay(Duration::from_millis(50)),
)
.mount(&server)
.await;
let first_handle = spawn({
let room = room.to_owned();
async move { room.to_owned().latest_encryption_state().await.map(|state| state.is_encrypted()) }
});
let second_handle =
spawn(
async move { room.latest_encryption_state().await.map(|state| state.is_encrypted()) },
);
let first_encrypted =
first_handle.await.unwrap().expect("We should be able to test if the room is encrypted.");
let second_encrypted =
second_handle.await.unwrap().expect("We should be able to test if the room is encrypted.");
assert!(first_encrypted, "We should have found out that the room is encrypted.");
assert_eq!(
first_encrypted, second_encrypted,
"Both attempts to find out if the room is encrypted should return the same result."
);
}
#[async_test]
async fn test_marking_room_as_dm() {
let (client, server) = logged_in_client_with_server().await;
mock_sync(&server, &*test_json::SYNC, None).await;
client
.sync_once(SyncSettings::default())
.await
.expect("We should be able to perform an initial sync");
let account_data = client
.account()
.account_data::<DirectEventContent>()
.await
.expect("We should be able to fetch the account data event from the store");
assert!(account_data.is_none(), "We should not have any account data initially");
let bob = user_id!("@bob:example.com");
let users = vec![bob.to_owned()];
Mock::given(method("GET"))
.and(path("_matrix/client/r0/user/@example:localhost/account_data/m.direct"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"@bob:example.com": [
"!abcdefgh:example.com",
"!hgfedcba:example.com"
],
"@alice:example.com": [
"!abcdefgh:example.com",
]
})))
.expect(1..)
.named("m.direct account data GET")
.mount(&server)
.await;
let put_direct_content_matcher = |request: &Request| {
let content: DirectEventContent = request.body_json().expect(
"The body of the PUT /account_data request should be a valid DirectEventContent",
);
let bob_entry = content
.get(&OwnedDirectUserIdentifier::from(bob.to_owned()))
.expect("We should have bob in the direct event content");
assert_eq!(content.len(), 2, "We should have entries for bob and foo");
assert_eq!(bob_entry.len(), 3, "Bob should have 3 direct rooms");
content.len() == 2 && bob_entry.len() == 3
};
Mock::given(method("PUT"))
.and(path("_matrix/client/r0/user/@example:localhost/account_data/m.direct"))
.and(header("authorization", "Bearer 1234"))
.and(put_direct_content_matcher)
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1..)
.named("m.direct account data PUT")
.mount(&server)
.await;
client
.account()
.mark_as_dm(&DEFAULT_TEST_ROOM_ID, &users)
.await
.expect("We should be able to mark the room as a DM");
server.verify().await;
}
#[async_test]
async fn test_marking_room_as_dm_fails_if_undeserializable() {
let (client, server) = logged_in_client_with_server().await;
mock_sync(&server, &*test_json::SYNC, None).await;
client
.sync_once(SyncSettings::default())
.await
.expect("We should be able to perform an initial sync");
let account_data = client
.account()
.account_data::<DirectEventContent>()
.await
.expect("We should be able to fetch the account data event from the store");
assert!(account_data.is_none(), "We should not have any account data initially");
let bob = user_id!("@bob:example.com");
let users = vec![bob.to_owned()];
Mock::given(method("GET"))
.and(path("_matrix/client/r0/user/@example:localhost/account_data/m.direct"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!(["hey", null, true, 42])))
.expect(1)
.named("m.direct account data GET")
.mount(&server)
.await;
let result = client.account().mark_as_dm(&DEFAULT_TEST_ROOM_ID, &users).await;
assert_matches!(result, Err(Error::SerdeJson(_)));
server.verify().await;
}
#[cfg(feature = "e2e-encryption")]
#[async_test]
async fn test_get_own_device() {
let (client, _) = logged_in_client_with_server().await;
let device = client
.encryption()
.get_own_device()
.await
.unwrap()
.expect("We should always have access to our own device, even before any sync");
assert!(
device.user_id() == client.user_id().expect("The client should know about its user ID"),
"The user ID of the client and our own device should match"
);
assert!(
device.device_id()
== client.device_id().expect("The client should know about its device ID"),
"The device ID of the client and our own device should match"
);
}
#[cfg(feature = "e2e-encryption")]
#[async_test]
async fn test_cross_signing_status() {
let (client, server) = logged_in_client_with_server().await;
Mock::given(method("POST"))
.and(path("/_matrix/client/unstable/keys/device_signing/upload"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/_matrix/client/unstable/keys/signatures/upload"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"failures": {}
})))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/_matrix/client/r0/keys/upload"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"one_time_key_counts": {}
})))
.mount(&server)
.await;
let status = client
.encryption()
.cross_signing_status()
.await
.expect("We should be able to fetch our cross-signing status");
assert!(
!status.has_master && !status.has_self_signing && !status.has_user_signing,
"Initially we shouldn't have any cross-signing keys"
);
client.encryption().bootstrap_cross_signing(None).await.unwrap();
client
.encryption()
.cross_signing_status()
.await
.expect("We should be able to fetch our cross-signing status");
let status = client
.encryption()
.cross_signing_status()
.await
.expect("We should have the private cross-signing keys after the bootstrap process");
assert!(status.is_complete(), "We should have all the private cross-signing keys locally");
server.verify().await;
}
#[cfg(feature = "e2e-encryption")]
#[async_test]
async fn test_encrypt_room_event() {
use std::sync::Arc;
use ruma::events::room::encrypted::RoomEncryptedEventContent;
let (client, server) = logged_in_client_with_server().await;
let user_id = client.user_id().unwrap();
Mock::given(method("POST"))
.and(path("/_matrix/client/r0/keys/query"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"device_keys": {
user_id: {}
}
})))
.mount(&server)
.await;
mock_sync(&server, &*test_json::SYNC, None).await;
client
.sync_once(SyncSettings::default())
.await
.expect("We should be able to performs an initial sync");
let room =
client.get_room(&DEFAULT_TEST_ROOM_ID).expect("We should know about our default room");
Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.encryption/"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"algorithm": "m.megolm.v1.aes-sha2",
"rotation_period_ms": 604800000,
"rotation_period_msgs": 100
})))
.mount(&server)
.await;
assert!(
room.latest_encryption_state()
.await
.expect("We should be able to check if the room is encrypted")
.is_encrypted(),
"The room should be encrypted"
);
Mock::given(method("GET"))
.and(path_regex("/_matrix/client/r0/rooms/!SVkFJHzfwvuaIEawgC:localhost/members"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "chunk": []})))
.mount(&server)
.await;
let event_content = Arc::new(std::sync::Mutex::new(None));
let event_content_matcher = {
let event_content = event_content.to_owned();
move |request: &Request| {
let mut path_segments =
request.url.path_segments().expect("The URL should be able to be a base");
let event_type = path_segments
.nth_back(1)
.expect("The path should have a event type as the last segment")
.to_owned();
assert_eq!(
event_type, "m.room.encrypted",
"The event type should be the `m.room.encrypted` event type"
);
let content: RoomEncryptedEventContent = request
.body_json()
.expect("The uploaded content should be a valid `m.room.encrypted` event content");
*event_content.lock().unwrap() = Some(content);
true
}
};
Mock::given(method("PUT"))
.and(path(
"/_matrix/client/r0/rooms/!SVkFJHzfwvuaIEawgC:localhost/send/m.room.encrypted/foobar",
))
.and(event_content_matcher)
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"event_id": "$foobar"
})))
.mount(&server)
.await;
room.send_raw("m.room.message", json!({"body": "Hello", "msgtype": "m.text"}))
.with_transaction_id("foobar".into())
.await
.expect("We should be able to send a message to the encrypted room");
let content = event_content
.lock()
.unwrap()
.take()
.expect("We should have intercepted an `m.room.encrypted` event content");
let event: Raw<OriginalSyncRoomEncryptedEvent> = Raw::new(&json!({
"room_id": room.room_id(),
"event_id": "$foobar",
"origin_server_ts": 1600000u64,
"sender": user_id,
"content": content,
}))
.expect("We should be able to construct a full event from the encrypted event content")
.cast_unchecked();
let push_ctx =
room.push_context().await.expect("We should be able to get the push action context");
let timeline_event = room
.decrypt_event(&event, push_ctx.as_ref())
.await
.expect("We should be able to decrypt an event that we ourselves have encrypted");
let event = timeline_event
.raw()
.deserialize()
.expect("We should be able to deserialize the decrypted event");
assert_let!(
ruma::events::AnySyncTimelineEvent::MessageLike(
ruma::events::AnySyncMessageLikeEvent::RoomMessage(message_event)
) = event
);
let message_event =
message_event.as_original().expect("The decrypted event should not be a redacted event");
assert_eq!(
message_event.content.body(),
"Hello",
"The now decrypted message should match to our plaintext payload"
);
}
#[cfg(not(feature = "e2e-encryption"))]
#[async_test]
async fn test_create_dm_non_encrypted() {
let (client, server) = logged_in_client_with_server().await;
let user_id = user_id!("@invitee:localhost");
Mock::given(method("POST"))
.and(path("/_matrix/client/r0/createRoom"))
.and(|request: &Request| {
let Ok(body) = request.body_json::<Raw<JsonValue>>() else {
return false;
};
if !body.get_field::<bool>("is_direct").is_ok_and(|b| b == Some(true)) {
return false;
}
if !body
.get_field::<String>("preset")
.is_ok_and(|s| s.as_deref() == Some("trusted_private_chat"))
{
return false;
}
if !body
.get_field::<Vec<OwnedUserId>>("invite")
.is_ok_and(|v| v.as_deref() == Some(&[user_id.to_owned()]))
{
return false;
}
body.get_field::<Vec<Raw<AnyInitialStateEvent>>>("initial_state")
.is_ok_and(|v| v.is_none())
})
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"room_id": "!sefiuhWgwghwWgh:example.com"
})))
.expect(1)
.mount(&server)
.await;
client.create_dm(user_id).await.unwrap();
}
#[cfg(feature = "e2e-encryption")]
#[async_test]
async fn test_create_dm_encrypted() {
let (client, server) = logged_in_client_with_server().await;
let user_id = user_id!("@invitee:localhost");
Mock::given(method("POST"))
.and(path("/_matrix/client/r0/createRoom"))
.and(|request: &Request| {
let Ok(body) = request.body_json::<Raw<JsonValue>>() else {
return false;
};
if !body.get_field::<bool>("is_direct").is_ok_and(|b| b == Some(true)) {
return false;
}
if !body
.get_field::<String>("preset")
.is_ok_and(|s| s.as_deref() == Some("trusted_private_chat"))
{
return false;
}
if !body
.get_field::<Vec<OwnedUserId>>("invite")
.is_ok_and(|v| v.as_deref() == Some(&[user_id.to_owned()]))
{
return false;
}
body.get_field::<Vec<Raw<AnyInitialStateEvent>>>("initial_state").is_ok_and(|v| {
let Some(v) = v else {
return false;
};
if v.len() != 1 {
return false;
}
let initial_event = &v[0];
initial_event
.get_field::<String>("type")
.is_ok_and(|s| s.as_deref() == Some("m.room.encryption"))
})
})
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"room_id": "!sefiuhWgwghwWgh:example.com"
})))
.expect(1)
.mount(&server)
.await;
client.create_dm(user_id).await.unwrap();
}
#[async_test]
async fn test_create_dm_error() {
let (client, _server) = logged_in_client_with_server().await;
let user_id = user_id!("@invitee:localhost");
let error = client.create_dm(user_id).await.unwrap_err();
let client_api_error = error.as_client_api_error().unwrap();
assert_eq!(client_api_error.status_code, 404);
}
#[async_test]
async fn test_test_ambiguity_changes() {
let (client, server) = logged_in_client_with_server().await;
let example_id = user_id!("@example:localhost");
let example_2_id = user_id!("@example2:localhost");
let example_3_id = user_id!("@example3:localhost");
let mut updates = BroadcastStream::new(client.subscribe_to_room_updates(&DEFAULT_TEST_ROOM_ID));
assert_pending!(updates);
mock_sync(&server, &*test_json::SYNC, None).await;
let response =
client.sync_once(SyncSettings::default().token(SyncToken::NoToken)).await.unwrap();
server.reset().await;
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
let changes = &response.rooms.joined.get(*DEFAULT_TEST_ROOM_ID).unwrap().ambiguity_changes;
let example_change = changes.get(event_id!("$151800140517rfvjc:localhost")).unwrap();
assert_eq!(example_change.member_id, example_id);
assert!(!example_change.member_ambiguous);
assert_eq!(example_change.ambiguated_member, None);
assert_eq!(example_change.disambiguated_member, None);
let example_2_change = changes.get(event_id!("$152034824468gOeNB:localhost")).unwrap();
assert_eq!(example_2_change.member_id, example_2_id);
assert!(!example_2_change.member_ambiguous);
assert_eq!(example_2_change.ambiguated_member, None);
assert_eq!(example_2_change.disambiguated_member, None);
let example = room.get_member_no_sync(example_id).await.unwrap().unwrap();
assert!(!example.name_ambiguous());
let example_2 = room.get_member_no_sync(example_2_id).await.unwrap().unwrap();
assert!(!example_2.name_ambiguous());
let changes = assert_next_matches!(updates, Ok(RoomUpdate::Joined { updates, .. }) => updates.ambiguity_changes);
let example_change = changes.get(event_id!("$151800140517rfvjc:localhost")).unwrap();
assert_eq!(example_change.member_id, example_id);
assert!(!example_change.member_ambiguous);
assert_eq!(example_change.ambiguated_member, None);
assert_eq!(example_change.disambiguated_member, None);
let example_2_change = changes.get(event_id!("$152034824468gOeNB:localhost")).unwrap();
assert_eq!(example_2_change.member_id, example_2_id);
assert!(!example_2_change.member_ambiguous);
assert_eq!(example_2_change.ambiguated_member, None);
assert_eq!(example_2_change.disambiguated_member, None);
let example_2_rename_1_event_id = event_id!("$example_2_rename_1");
let example_3_join_event_id = event_id!("$example_3_join");
let mut sync_builder = SyncResponseBuilder::new();
let joined_room = JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID).add_state_bulk([
sync_state_event!({
"content": {
"avatar_url": null,
"displayname": "example",
"membership": "join"
},
"event_id": example_2_rename_1_event_id,
"origin_server_ts": 151800140,
"sender": example_2_id,
"state_key": example_2_id,
"type": "m.room.member",
}),
sync_state_event!({
"content": {
"avatar_url": null,
"displayname": "example",
"membership": "join"
},
"event_id": example_3_join_event_id,
"origin_server_ts": 151800140,
"sender": example_3_id,
"state_key": example_3_id,
"type": "m.room.member",
}),
]);
sync_builder.add_joined_room(joined_room);
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
let response =
client.sync_once(SyncSettings::default().token(SyncToken::NoToken)).await.unwrap();
server.reset().await;
let changes = &response.rooms.joined.get(*DEFAULT_TEST_ROOM_ID).unwrap().ambiguity_changes;
let example_2_change = changes.get(example_2_rename_1_event_id).unwrap();
assert_eq!(example_2_change.member_id, example_2_id);
assert!(example_2_change.member_ambiguous);
assert_eq!(example_2_change.ambiguated_member.as_deref(), Some(example_id));
assert_eq!(example_2_change.disambiguated_member, None);
let example_3_change = changes.get(example_3_join_event_id).unwrap();
assert_eq!(example_3_change.member_id, example_3_id);
assert!(example_3_change.member_ambiguous);
assert_eq!(example_3_change.ambiguated_member, None);
assert_eq!(example_3_change.disambiguated_member, None);
let example = room.get_member_no_sync(example_id).await.unwrap().unwrap();
assert!(example.name_ambiguous());
let example_2 = room.get_member_no_sync(example_2_id).await.unwrap().unwrap();
assert!(example_2.name_ambiguous());
let example_3 = room.get_member_no_sync(example_3_id).await.unwrap().unwrap();
assert!(example_3.name_ambiguous());
let changes = assert_next_matches!(updates, Ok(RoomUpdate::Joined { updates, .. }) => updates.ambiguity_changes);
let example_2_change = changes.get(example_2_rename_1_event_id).unwrap();
assert_eq!(example_2_change.member_id, example_2_id);
assert!(example_2_change.member_ambiguous);
assert_eq!(example_2_change.ambiguated_member.as_deref(), Some(example_id));
assert_eq!(example_2_change.disambiguated_member, None);
let example_3_change = changes.get(example_3_join_event_id).unwrap();
assert_eq!(example_3_change.member_id, example_3_id);
assert!(example_3_change.member_ambiguous);
assert_eq!(example_3_change.ambiguated_member, None);
assert_eq!(example_3_change.disambiguated_member, None);
let example_2_rename_2_event_id = event_id!("$example_2_rename_2");
let joined_room =
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID).add_state_bulk([sync_state_event!({
"content": {
"avatar_url": null,
"displayname": "another example",
"membership": "join"
},
"event_id": example_2_rename_2_event_id,
"origin_server_ts": 151800140,
"sender": example_2_id,
"state_key": example_2_id,
"type": "m.room.member",
})]);
sync_builder.add_joined_room(joined_room);
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
let response =
client.sync_once(SyncSettings::default().token(SyncToken::NoToken)).await.unwrap();
server.reset().await;
let changes = &response.rooms.joined.get(*DEFAULT_TEST_ROOM_ID).unwrap().ambiguity_changes;
let example_2_change = changes.get(example_2_rename_2_event_id).unwrap();
assert_eq!(example_2_change.member_id, example_2_id);
assert!(!example_2_change.member_ambiguous);
assert_eq!(example_2_change.ambiguated_member, None);
assert_eq!(example_2_change.disambiguated_member, None);
let example = room.get_member_no_sync(example_id).await.unwrap().unwrap();
assert!(example.name_ambiguous());
let example_2 = room.get_member_no_sync(example_2_id).await.unwrap().unwrap();
assert!(!example_2.name_ambiguous());
let example_3 = room.get_member_no_sync(example_3_id).await.unwrap().unwrap();
assert!(example_3.name_ambiguous());
let changes = assert_next_matches!(updates, Ok(RoomUpdate::Joined { updates, .. }) => updates.ambiguity_changes);
let example_2_change = changes.get(example_2_rename_2_event_id).unwrap();
assert_eq!(example_2_change.member_id, example_2_id);
assert!(!example_2_change.member_ambiguous);
assert_eq!(example_2_change.ambiguated_member, None);
assert_eq!(example_2_change.disambiguated_member, None);
let example_3_rename_event_id = event_id!("$example_3_rename");
let joined_room =
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID).add_state_bulk([sync_state_event!({
"content": {
"avatar_url": null,
"displayname": "another example",
"membership": "join"
},
"event_id": example_3_rename_event_id,
"origin_server_ts": 151800140,
"sender": example_3_id,
"state_key": example_3_id,
"type": "m.room.member",
})]);
sync_builder.add_joined_room(joined_room);
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
let response =
client.sync_once(SyncSettings::default().token(SyncToken::NoToken)).await.unwrap();
server.reset().await;
let changes = &response.rooms.joined.get(*DEFAULT_TEST_ROOM_ID).unwrap().ambiguity_changes;
let example_3_change = changes.get(example_3_rename_event_id).unwrap();
assert_eq!(example_3_change.member_id, example_3_id);
assert!(example_3_change.member_ambiguous);
assert_eq!(example_3_change.ambiguated_member.as_deref(), Some(example_2_id));
assert_eq!(example_3_change.disambiguated_member.as_deref(), Some(example_id));
let example = room.get_member_no_sync(example_id).await.unwrap().unwrap();
assert!(!example.name_ambiguous());
let example_2 = room.get_member_no_sync(example_2_id).await.unwrap().unwrap();
assert!(example_2.name_ambiguous());
let example_3 = room.get_member_no_sync(example_3_id).await.unwrap().unwrap();
assert!(example_3.name_ambiguous());
let changes = assert_next_matches!(updates, Ok(RoomUpdate::Joined { updates, .. }) => updates.ambiguity_changes);
let example_3_change = changes.get(example_3_rename_event_id).unwrap();
assert_eq!(example_3_change.member_id, example_3_id);
assert!(example_3_change.member_ambiguous);
assert_eq!(example_3_change.ambiguated_member.as_deref(), Some(example_2_id));
assert_eq!(example_3_change.disambiguated_member.as_deref(), Some(example_id));
let example_rename_event_id = event_id!("$example_rename");
let joined_room =
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID).add_state_bulk([sync_state_event!({
"content": {
"avatar_url": null,
"displayname": "the first example",
"membership": "join"
},
"event_id": example_rename_event_id,
"origin_server_ts": 151800140,
"sender": example_id,
"state_key": example_id,
"type": "m.room.member",
})]);
sync_builder.add_joined_room(joined_room);
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
let response =
client.sync_once(SyncSettings::default().token(SyncToken::NoToken)).await.unwrap();
server.reset().await;
let changes = &response.rooms.joined.get(*DEFAULT_TEST_ROOM_ID).unwrap().ambiguity_changes;
let example_change = changes.get(example_rename_event_id).unwrap();
assert_eq!(example_change.member_id, example_id);
assert!(!example_change.member_ambiguous);
assert_eq!(example_change.ambiguated_member, None);
assert_eq!(example_change.disambiguated_member, None);
let example = room.get_member_no_sync(example_id).await.unwrap().unwrap();
assert!(!example.name_ambiguous());
let example_2 = room.get_member_no_sync(example_2_id).await.unwrap().unwrap();
assert!(example_2.name_ambiguous());
let example_3 = room.get_member_no_sync(example_3_id).await.unwrap().unwrap();
assert!(example_3.name_ambiguous());
let changes = assert_next_matches!(updates, Ok(RoomUpdate::Joined { updates, .. }) => updates.ambiguity_changes);
let example_change = changes.get(example_rename_event_id).unwrap();
assert_eq!(example_change.member_id, example_id);
assert!(!example_change.member_ambiguous);
assert_eq!(example_change.ambiguated_member, None);
assert_eq!(example_change.disambiguated_member, None);
let example_avatar_event_id = event_id!("$example_avatar");
let joined_room =
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID).add_state_bulk([sync_state_event!({
"content": {
"avatar_url": "mxc://localhost/avatar",
"displayname": "the first example",
"membership": "join"
},
"event_id": example_avatar_event_id,
"origin_server_ts": 151800140,
"sender": example_id,
"state_key": example_id,
"type": "m.room.member",
})]);
sync_builder.add_joined_room(joined_room);
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
let response =
client.sync_once(SyncSettings::default().token(SyncToken::NoToken)).await.unwrap();
server.reset().await;
assert!(response.rooms.joined.get(*DEFAULT_TEST_ROOM_ID).unwrap().ambiguity_changes.is_empty());
let changes = assert_next_matches!(updates, Ok(RoomUpdate::Joined { updates, .. }) => updates.ambiguity_changes);
assert!(changes.is_empty());
assert_pending!(updates);
}
#[cfg(not(target_family = "wasm"))]
#[async_test]
async fn test_rooms_stream() {
use futures_util::StreamExt as _;
let (client, server) = logged_in_client_with_server().await;
let (rooms, mut rooms_stream) = client.rooms_stream();
assert!(rooms.is_empty());
assert_pending!(rooms_stream);
let room_id_1 = room_id!("!room0:matrix.org");
let room_id_2 = room_id!("!room1:matrix.org");
let room_id_3 = room_id!("!room2:matrix.org");
let payload = json!({
"next_batch": "foo",
"rooms": {
"invite": {},
"join": {
room_id_1: {},
room_id_2: {},
room_id_3: {},
},
"leave": {}
},
});
mock_sync(&server, &payload, None).await;
assert!(client.get_room(room_id_1).is_none());
assert!(client.get_room(room_id_2).is_none());
assert!(client.get_room(room_id_3).is_none());
client.sync_once(SyncSettings::default()).await.unwrap();
assert!(client.get_room(room_id_1).is_some());
assert!(client.get_room(room_id_2).is_some());
assert!(client.get_room(room_id_3).is_some());
assert_let!(Some(diffs) = rooms_stream.next().await);
assert_eq!(diffs.len(), 3);
assert_let!(VectorDiff::PushBack { value: room_1 } = &diffs[0]);
assert_eq!(room_1.room_id(), room_id_1);
assert_let!(VectorDiff::PushBack { value: room_2 } = &diffs[1]);
assert_eq!(room_2.room_id(), room_id_2);
assert_let!(VectorDiff::PushBack { value: room_3 } = &diffs[2]);
assert_eq!(room_3.room_id(), room_id_3);
assert_pending!(rooms_stream);
}
#[async_test]
async fn test_dms_are_processed_in_any_sync_response() {
let (client, server) = logged_in_client_with_server().await;
let user_a_id = user_id!("@a:e.uk");
let user_b_id = user_id!("@b:e.uk");
let room_id_1 = room_id!("!r:e.uk");
let room_id_2 = room_id!("!s:e.uk");
let joined_room_builder = JoinedRoomBuilder::new(room_id_1);
let f = EventFactory::new();
let mut sync_response_builder = SyncResponseBuilder::new();
sync_response_builder.add_global_account_data(
f.direct()
.add_user(user_a_id.to_owned().into(), room_id_1)
.add_user(user_b_id.to_owned().into(), room_id_2),
);
sync_response_builder.add_joined_room(joined_room_builder);
let json_response = sync_response_builder.build_json_sync_response();
mock_sync(&server, json_response, None).await;
client.sync_once(SyncSettings::default().token(SyncToken::NoToken)).await.unwrap();
server.reset().await;
let room_1 = client.get_room(room_id_1).unwrap();
assert!(room_1.is_direct().await.unwrap());
let joined_room_builder = JoinedRoomBuilder::new(room_id_2);
sync_response_builder.add_joined_room(joined_room_builder);
let json_response = sync_response_builder.build_json_sync_response();
mock_sync(&server, json_response, None).await;
client.sync_once(SyncSettings::default().token(SyncToken::NoToken)).await.unwrap();
server.reset().await;
let room_2 = client.get_room(room_id_2).unwrap();
assert!(room_2.is_direct().await.unwrap());
}
#[async_test]
async fn test_restore_room() {
let room_id = room_id!("!stored_room:localhost");
let store = MemoryStore::new();
let mut changes = StateChanges::default();
let raw_tag_event = Raw::new(&*TAG).unwrap().cast_unchecked();
let tag_event = raw_tag_event.deserialize().unwrap();
changes.add_room_account_data(room_id, tag_event, raw_tag_event);
let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast_unchecked();
let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
changes.add_state_event(room_id, pinned_events_event, raw_pinned_events_event);
let room_info = serde_json::from_value(json!({
"room_id": room_id,
"room_state": "Joined",
"notification_counts": {
"highlight_count": 0,
"notification_count": 0,
},
"summary": {
"room_heroes": [],
"joined_member_count": 1,
"invited_member_count": 0,
},
"members_synced": true,
"last_prev_batch": "pb",
"sync_info": "FullySynced",
"encryption_state_synced": true,
"base_info": {
"avatar": null,
"canonical_alias": null,
"create": null,
"dm_targets": [],
"encryption": null,
"guest_access": null,
"history_visibility": null,
"join_rules": null,
"max_power_level": 100,
"name": null,
"tombstone": null,
"topic": null,
},
}))
.unwrap();
changes.add_room(room_info);
store.save_changes(&changes).await.unwrap();
let store_config =
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()).state_store(store);
let client = Client::builder()
.homeserver_url("http://localhost:1234")
.request_config(RequestConfig::new().disable_retry())
.store_config(store_config)
.build()
.await
.unwrap();
client
.matrix_auth()
.restore_session(mock_matrix_session(), RoomLoadSettings::default())
.await
.unwrap();
let room = client.get_room(room_id).unwrap();
assert!(room.is_favourite());
assert!(!room.pinned_event_ids().unwrap().is_empty());
}
#[async_test]
async fn test_logout() {
let server = MatrixMockServer::new().await;
let unlogged_client = server.client_builder().unlogged().build().await;
let res = unlogged_client.logout().await;
assert_matches!(res, Err(Error::AuthenticationRequired));
server.mock_logout().ok().mock_once().named("matrix_logout").mount().await;
let matrix_auth_client = server.client_builder().build().await;
matrix_auth_client.logout().await.unwrap();
server
.oauth()
.mock_server_metadata()
.ok()
.mock_once()
.named("oauth_server_metadata")
.mount()
.await;
let oauth_client = server.client_builder().logged_in_with_oauth().build().await;
let res = oauth_client.logout().await;
assert_matches!(res, Err(Error::OAuth(oauth_error)));
assert_matches!(*oauth_error, OAuthError::Logout(OAuthTokenRevocationError::Url(_)));
}
#[async_test]
async fn test_room_sync_state_after() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let mut rx = client.subscribe_to_room_updates(&DEFAULT_TEST_ROOM_ID);
server
.sync_room(
&client,
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID)
.use_state_after()
.add_state_bulk([
Raw::new(&*test_json::sync_events::CREATE).unwrap().cast_unchecked(),
Raw::new(&*test_json::sync_events::POWER_LEVELS).unwrap().cast_unchecked(),
Raw::new(&*test_json::sync_events::HISTORY_VISIBILITY)
.unwrap()
.cast_unchecked(),
Raw::new(&*test_json::sync_events::JOIN_RULES).unwrap().cast_unchecked(),
Raw::new(&*test_json::sync_events::MEMBER_LEAVE).unwrap().cast_unchecked(),
])
.add_timeline_bulk([
Raw::new(&*test_json::sync_events::MEMBER_ADDITIONAL).unwrap().cast_unchecked(),
Raw::new(&*test_json::sync_events::NAME).unwrap().cast_unchecked(),
]),
)
.await;
let update = rx.recv().now_or_never().unwrap().unwrap();
assert_let!(RoomUpdate::Joined { updates, .. } = update);
assert_matches!(updates.state, State::After(state_events));
assert_eq!(state_events.len(), 5);
assert_eq!(updates.timeline.events.len(), 2);
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
assert!(room.create_content().is_some());
assert_eq!(room.history_visibility().unwrap(), HistoryVisibility::WorldReadable);
assert_eq!(room.join_rule().unwrap(), JoinRule::Public);
assert!(room.name().is_none());
let member = room.get_member_no_sync(user_id!("@invited:localhost")).await.unwrap().unwrap();
assert_eq!(*member.membership(), MembershipState::Leave);
}
#[cfg(feature = "federation-api")]
#[async_test]
async fn test_server_vendor_info() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server.mock_federation_version().ok("Synapse", "1.70.0").mount().await;
let server_info = client.server_vendor_info(None).await.unwrap();
assert_eq!(server_info.server_name, "Synapse");
assert_eq!(server_info.version, "1.70.0");
}
#[async_test]
async fn test_server_version_without_auth() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server.mock_versions().expect_default_access_token().error_unknown_token(true).mount().await;
server.mock_versions().expect_missing_access_token().ok_with_unstable_features().mount().await;
let request_config = RequestConfig::new().disable_retry();
client
.fetch_server_versions(Some(request_config))
.await
.expect_err("We should fail here since we provided an auth token.");
let request_config = RequestConfig::new().disable_retry().skip_auth();
client
.fetch_server_versions(Some(request_config))
.await
.expect("We should not fail here since we did not provide an auth token.");
}
#[cfg(feature = "federation-api")]
#[async_test]
async fn test_server_vendor_info_with_missing_fields() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server.mock_federation_version().ok_empty().mount().await;
let server_info = client.server_vendor_info(None).await.unwrap();
assert_eq!(server_info.server_name, "unknown");
assert_eq!(server_info.version, "unknown");
}
#[async_test]
async fn test_fetch_thread_subscriptions() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let room1 = owned_room_id!("!room1:example.com");
let room2 = owned_room_id!("!room2:example.com");
let room3 = owned_room_id!("!room3:example.com");
let thread1 = owned_event_id!("$thread1:example.com");
let thread2 = owned_event_id!("$thread2:example.com");
let thread3 = owned_event_id!("$thread3:example.com");
server
.mock_get_thread_subscriptions()
.match_from("from")
.match_to("to")
.add_subscription(room1.clone(), thread1.clone(), ThreadSubscription::new(true, uint!(42)))
.add_subscription(room2.clone(), thread2.clone(), ThreadSubscription::new(false, uint!(7)))
.add_unsubscription(room3.clone(), thread3.clone(), ThreadUnsubscription::new(uint!(13)))
.ok(Some("next_batch_token".to_owned()))
.mount()
.await;
let response = client
.fetch_thread_subscriptions(Some("from".to_owned()), Some("to".to_owned()), None)
.await
.unwrap();
assert_eq!(response.end.as_deref(), Some("next_batch_token"));
assert_eq!(response.subscribed.len(), 2);
let s1 = &response.subscribed[&room1][&thread1];
assert!(s1.automatic);
assert_eq!(s1.bump_stamp, uint!(42));
let s2 = &response.subscribed[&room2][&thread2];
assert!(s2.automatic.not());
assert_eq!(s2.bump_stamp, uint!(7));
assert_eq!(response.unsubscribed.len(), 1);
let u = &response.unsubscribed[&room3][&thread3];
assert_eq!(u.bump_stamp, uint!(13));
}
fn thread_subscription_response(
room1: &RoomId,
thread1: &EventId,
room2: &RoomId,
thread2: &EventId,
) -> v5::response::ThreadSubscriptions {
assign!(v5::response::ThreadSubscriptions::default(), {
subscribed: {
let mut map = BTreeMap::new();
map.insert(room1.to_owned(), {
let mut threads = BTreeMap::new();
threads.insert(thread1.to_owned(), ThreadSubscription::new(true, uint!(42)));
threads
});
map
},
unsubscribed: {
let mut map = BTreeMap::new();
map.insert(room2.to_owned(), {
let mut threads = BTreeMap::new();
threads.insert(thread2.to_owned(), ThreadUnsubscription::new(uint!(7)));
threads
});
map
},
prev_batch: None,
})
}
#[async_test]
async fn test_sync_thread_subscriptions() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let room1 = owned_room_id!("!room1:example.com");
let room2 = owned_room_id!("!room2:example.com");
let thread1 = owned_event_id!("$thread1:example.com");
let thread2 = owned_event_id!("$thread2:example.com");
let stored1 = client
.state_store()
.load_thread_subscription(&room1, &thread1)
.await
.expect("loading room1/thread1 works fine");
assert_matches!(stored1, None);
let stored2 = client
.state_store()
.load_thread_subscription(&room2, &thread2)
.await
.expect("loading room2/thread2 works fine");
assert_matches!(stored2, None);
server
.mock_sliding_sync()
.ok_and_run(
&client,
|config_builder| {
config_builder.with_thread_subscriptions_extension(
assign!(v5::request::ThreadSubscriptions::default(), {
enabled: Some(true),
limit: Some(uint!(10)),
}),
)
},
assign!(v5::Response::new("pos".to_owned()), {
extensions: assign!(v5::response::Extensions::default(), {
thread_subscriptions: thread_subscription_response(
&room1, &thread1, &room2, &thread2,
),
}),
}),
)
.await;
let stored1 = client
.state_store()
.load_thread_subscription(&room1, &thread1)
.await
.expect("loading room1/thread1 works fine")
.expect("found room1/thread1 subscription");
assert_eq!(stored1.status, ThreadSubscriptionStatus::Subscribed { automatic: true });
assert_eq!(stored1.bump_stamp, Some(42));
let stored2 = client
.state_store()
.load_thread_subscription(&room2, &thread2)
.await
.expect("loading room2/thread2 works fine")
.expect("found room2/thread2 unsubscription");
assert_eq!(stored2.status, ThreadSubscriptionStatus::Unsubscribed);
assert_eq!(stored2.bump_stamp, Some(7));
}
#[async_test]
async fn test_sync_thread_subscriptions_with_catchup() {
let server = MatrixMockServer::new().await;
let client = server
.client_builder()
.on_builder(|builder| {
builder.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;
let room_id1 = owned_room_id!("!room1:example.com");
let room_id2 = owned_room_id!("!room2:example.com");
let thread1 = owned_event_id!("$thread1:example.com");
let thread2 = owned_event_id!("$thread2:example.com");
let thread3 = owned_event_id!("$thread3:example.com");
server
.mock_get_thread_subscriptions()
.match_from("catchup_token")
.add_subscription(
room_id1.clone(),
thread3.clone(),
ThreadSubscription::new(false, uint!(1337)),
)
.with_delay(Duration::from_millis(300)) .ok(None)
.mock_once()
.mount()
.await;
let mut thread_subscriptions =
thread_subscription_response(&room_id1, &thread1, &room_id2, &thread2);
thread_subscriptions.prev_batch = Some("catchup_token".to_owned());
server
.mock_sliding_sync()
.ok_and_run(
&client,
|config_builder| {
config_builder
.with_thread_subscriptions_extension(
assign!(v5::request::ThreadSubscriptions::default(), {
enabled: Some(true),
limit: Some(uint!(10)),
}),
)
.add_list(SlidingSyncList::builder("rooms"))
},
assign!(v5::Response::new("pos".to_owned()), {
rooms: {
let mut rooms = BTreeMap::new();
rooms.insert(room_id1.clone(), v5::response::Room::default());
rooms.insert(room_id2.clone(), v5::response::Room::default());
rooms
},
extensions: assign!(v5::response::Extensions::default(), {
thread_subscriptions,
}),
}),
)
.await;
server
.mock_room_get_thread_subscription()
.match_room_id(room_id1.clone())
.match_thread_id(thread1.clone())
.ok(true)
.mock_once()
.mount()
.await;
let room1 = client.get_room(&room_id1).unwrap();
let sub1 = room1.load_or_fetch_thread_subscription(&thread1).await.unwrap();
assert_eq!(sub1, Some(matrix_sdk::room::ThreadSubscription { automatic: true }));
sleep(Duration::from_millis(400)).await;
let stored3 = client
.state_store()
.load_thread_subscription(&room_id1, &thread3)
.await
.expect("loading room1/thread3 works fine")
.expect("found room1/thread3 subscription");
assert_eq!(stored3.status, ThreadSubscriptionStatus::Subscribed { automatic: false });
assert_eq!(stored3.bump_stamp, Some(1337));
let sub3 = room1.load_or_fetch_thread_subscription(&thread3).await.unwrap();
assert_eq!(sub3, Some(matrix_sdk::room::ThreadSubscription { automatic: false }));
}
#[async_test]
#[cfg(feature = "sqlite")]
async fn test_sync_processing_of_custom_join_rule() {
let tempdir = tempdir().unwrap();
let room_id = room_id!("!room0:matrix.org");
let server = MatrixMockServer::new().await;
let client = server
.client_builder()
.on_builder(|builder| builder.sqlite_store(tempdir.path(), None))
.build()
.await;
server
.mock_sync()
.ok(|builder| {
builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(
StateTestEvent::Custom(json!({
"content": {
"join_rule": "my_custom_rule"
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.join_rules",
})),
));
})
.mock_once()
.mount()
.await;
client
.sync_once(Default::default())
.await
.expect("We should be able to process the sync despite there being a custom join rule");
}
#[async_test]
#[cfg(feature = "sqlite")]
async fn test_sync_processing_of_custom_stripped_join_rule() {
let tempdir = tempdir().unwrap();
let room_id = room_id!("!room0:matrix.org");
let server = MatrixMockServer::new().await;
let client = server
.client_builder()
.on_builder(|builder| builder.sqlite_store(tempdir.path(), None))
.build()
.await;
server
.mock_sync()
.ok(|builder| {
builder.add_invited_room(InvitedRoomBuilder::new(room_id).add_state_event(
StrippedStateTestEvent::Custom(json!({
"content": {
"join_rule": "my_custom_rule"
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.join_rules",
})),
));
})
.mock_once()
.mount()
.await;
client
.sync_once(Default::default())
.await
.expect("We should be able to process the sync despite there being a custom join rule");
}