diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index f524ad953..5b62c9c66 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -153,7 +153,7 @@ impl LatestEventValue { } } -pub fn find_and_map( +fn find_and_map( event: &Event, power_levels: &Option<(&UserId, RoomPowerLevels)>, ) -> Option { @@ -239,3 +239,366 @@ pub fn find_and_map( } } } + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use matrix_sdk_test::event_factory::EventFactory; + use ruma::{event_id, user_id}; + + use super::{find_and_map, LatestEventValue}; + + macro_rules! assert_latest_event_value { + ( with | $event_factory:ident | $event_builder:block + it produces $match:pat ) => { + let user_id = user_id!("@mnt_io:matrix.org"); + let event_factory = EventFactory::new().sender(user_id); + let event = { + let $event_factory = event_factory; + $event_builder + }; + + assert_matches!(find_and_map(&event, &None), $match); + }; + } + + #[test] + fn test_latest_event_value_room_message() { + assert_latest_event_value!( + with |event_factory| { + event_factory.text_msg("hello").into_event() + } + it produces Some(LatestEventValue::RoomMessage(_)) + ); + } + + #[test] + fn test_latest_event_value_room_message_redacted() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .redacted( + user_id!("@mnt_io:matrix.org"), + ruma::events::room::message::RedactedRoomMessageEventContent::new() + ) + .into_event() + } + it produces Some(LatestEventValue::RoomMessage(_)) + ); + } + + #[test] + fn test_latest_event_value_room_message_replacement() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .text_msg("bonjour") + .edit( + event_id!("$ev0"), + ruma::events::room::message::RoomMessageEventContent::text_plain("hello").into() + ) + .into_event() + } + it produces None + ); + } + + #[test] + fn test_latest_event_value_poll() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .poll_start( + "the people need to know", + "comté > gruyère", + vec!["yes", "oui"] + ) + .into_event() + } + it produces Some(LatestEventValue::Poll(_)) + ); + } + + #[test] + fn test_latest_event_value_call_invite() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .call_invite( + ruma::OwnedVoipId::from("vvooiipp".to_owned()), + ruma::UInt::from(1234u32), + ruma::events::call::SessionDescription::new("type".to_owned(), "sdp".to_owned()), + ruma::VoipVersionId::V1, + ) + .into_event() + } + it produces Some(LatestEventValue::CallInvite(_)) + ); + } + + #[test] + fn test_latest_event_value_call_notify() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .call_notify( + "call_id".to_owned(), + ruma::events::call::notify::ApplicationType::Call, + ruma::events::call::notify::NotifyType::Ring, + ruma::events::Mentions::new(), + ) + .into_event() + } + it produces Some(LatestEventValue::CallNotify(_)) + ); + } + + #[test] + fn test_latest_event_value_sticker() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .sticker( + "wink wink", + ruma::events::room::ImageInfo::new(), + ruma::OwnedMxcUri::from("mxc://foo/bar") + ) + .into_event() + } + it produces Some(LatestEventValue::Sticker(_)) + ); + } + + #[test] + fn test_latest_event_value_encrypted_room_message() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .event(ruma::events::room::encrypted::RoomEncryptedEventContent::new( + ruma::events::room::encrypted::EncryptedEventScheme::MegolmV1AesSha2( + ruma::events::room::encrypted::MegolmV1AesSha2ContentInit { + ciphertext: "cipher".to_owned(), + sender_key: "sender_key".to_owned(), + device_id: "device_id".into(), + session_id: "session_id".to_owned(), + } + .into(), + ), + None, + )) + .into_event() + } + it produces None + ); + } + + #[test] + fn test_latest_event_value_reaction() { + // Take a random message-like event. + assert_latest_event_value!( + with |event_factory| { + event_factory + .reaction(event_id!("$ev0"), "+1") + .into_event() + } + it produces None + ); + } + + #[test] + fn test_latest_event_state_event() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .room_topic("new room topic") + .into_event() + } + it produces None + ); + } + + #[test] + fn test_latest_event_knocked_state_event_without_power_levels() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .member(user_id!("@other_mnt_io:server.name")) + .membership(ruma::events::room::member::MembershipState::Knock) + .into_event() + } + it produces None + ); + } + + #[test] + fn test_latest_event_knocked_state_event_with_power_levels() { + use ruma::events::room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}; + + let user_id = user_id!("@mnt_io:matrix.org"); + let other_user_id = user_id!("@other_mnt_io:server.name"); + let event_factory = EventFactory::new().sender(user_id); + let event = event_factory + .member(other_user_id) + .membership(ruma::events::room::member::MembershipState::Knock) + .into_event(); + + let room_power_levels_event = RoomPowerLevelsEventContent::new(); + let mut room_power_levels = RoomPowerLevels::from(room_power_levels_event); + room_power_levels.users_default = 5.into(); + + // Cannot accept. Cannot decline. + { + let mut room_power_levels = room_power_levels.clone(); + room_power_levels.invite = 10.into(); + room_power_levels.kick = 10.into(); + assert_matches!( + find_and_map(&event, &Some((user_id, room_power_levels))), + None, + "cannot accept, cannot decline", + ); + } + + // Can accept. Cannot decline. + { + let mut room_power_levels = room_power_levels.clone(); + room_power_levels.invite = 0.into(); + room_power_levels.kick = 10.into(); + assert_matches!( + find_and_map(&event, &Some((user_id, room_power_levels))), + Some(LatestEventValue::KnockedStateEvent(_)), + "can accept, cannot decline", + ); + } + + // Cannot accept. Can decline. + { + let mut room_power_levels = room_power_levels.clone(); + room_power_levels.invite = 10.into(); + room_power_levels.kick = 0.into(); + assert_matches!( + find_and_map(&event, &Some((user_id, room_power_levels))), + Some(LatestEventValue::KnockedStateEvent(_)), + "cannot accept, can decline", + ); + } + + // Can accept. Can decline. + { + room_power_levels.invite = 0.into(); + room_power_levels.kick = 0.into(); + assert_matches!( + find_and_map(&event, &Some((user_id, room_power_levels))), + Some(LatestEventValue::KnockedStateEvent(_)), + "can accept, can decline", + ); + } + } + + #[test] + fn test_latest_event_value_room_message_verification_request() { + assert_latest_event_value!( + with |event_factory| { + event_factory + .event( + ruma::events::room::message::RoomMessageEventContent::new( + ruma::events::room::message::MessageType::VerificationRequest( + ruma::events::room::message::KeyVerificationRequestEventContent::new( + "body".to_owned(), + vec![], + ruma::OwnedDeviceId::from("device_id"), + user_id!("@user:server.name").to_owned(), + ) + ) + ) + ) + .into_event() + } + it produces None + ); + } +} + +#[cfg(all(not(target_family = "wasm"), test))] +mod tests_non_wasm { + use assert_matches::assert_matches; + use matrix_sdk_test::{async_test, event_factory::EventFactory}; + use ruma::{event_id, room_id, user_id}; + + use super::LatestEventValue; + use crate::test_utils::mocks::MatrixMockServer; + + #[async_test] + async fn test_latest_event_value_is_scanning_event_backwards_from_event_cache() { + use matrix_sdk_base::{ + linked_chunk::{ChunkIdentifier, Position, Update}, + RoomState, + }; + + use crate::{client::WeakClient, room::WeakRoom}; + + let room_id = room_id!("!r0"); + let user_id = user_id!("@mnt_io:matrix.org"); + let event_factory = EventFactory::new().sender(user_id).room(room_id); + let event_id_0 = event_id!("$ev0"); + let event_id_1 = event_id!("$ev1"); + let event_id_2 = event_id!("$ev2"); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Prelude. + { + // Create the room. + client.base_client().get_or_create_room(room_id, RoomState::Joined); + + // Initialise the event cache store. + client + .event_cache_store() + .lock() + .await + .unwrap() + .handle_linked_chunk_updates( + matrix_sdk_base::linked_chunk::LinkedChunkId::Room(room_id), + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(0), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(0), 0), + items: vec![ + // a latest event candidate + event_factory.text_msg("hello").event_id(event_id_0).into(), + // a latest event candidate + event_factory.text_msg("world").event_id(event_id_1).into(), + // not a latest event candidate + event_factory + .room_topic("new room topic") + .event_id(event_id_2) + .into(), + ], + }, + ], + ) + .await + .unwrap(); + } + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap(); + let weak_room = WeakRoom::new(WeakClient::from_client(&client), room_id.to_owned()); + + assert_matches!( + LatestEventValue::new(room_id, None, &room_event_cache, &weak_room).await, + LatestEventValue::RoomMessage(given_event) => { + // We get `event_id_1` because `event_id_2` isn't a candidate, + // and `event_id_0` hasn't been read yet (because events are + // read backwards). + assert_eq!(given_event.event_id(), event_id_1); + } + ); + } +} diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index 0aecd53d9..0fb4a53ec 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -647,15 +647,20 @@ async fn compute_latest_events(registered_rooms: &RegisteredRooms, for_rooms: &[ mod tests { use std::ops::Not; - use matrix_sdk_base::RoomState; - use matrix_sdk_test::async_test; - use ruma::{event_id, owned_room_id, room_id}; + use assert_matches::assert_matches; + use matrix_sdk_base::{ + linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update}, + RoomState, + }; + use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder}; + use ruma::{event_id, owned_room_id, room_id, user_id}; + use stream_assert::assert_pending; use super::{ - broadcast, compute_latest_events, listen_to_event_cache_and_send_queue_updates, mpsc, - HashSet, RegisteredRooms, RoomEventCacheGenericUpdate, RoomRegistration, WeakClient, + broadcast, listen_to_event_cache_and_send_queue_updates, mpsc, HashSet, LatestEventValue, + RoomEventCacheGenericUpdate, RoomRegistration, }; - use crate::test_utils::logged_in_client_with_server; + use crate::test_utils::mocks::MatrixMockServer; #[async_test] async fn test_latest_events_are_lazy() { @@ -665,7 +670,8 @@ mod tests { let thread_id_1_0 = event_id!("$ev1.0"); let thread_id_2_0 = event_id!("$ev2.0"); - let (client, _server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; client.base_client().get_or_create_room(room_id_0, RoomState::Joined); client.base_client().get_or_create_room(room_id_1, RoomState::Joined); @@ -727,7 +733,8 @@ mod tests { let room_id_0 = room_id!("!r0"); let room_id_1 = room_id!("!r1"); - let (client, _server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; client.base_client().get_or_create_room(room_id_0, RoomState::Joined); client.base_client().get_or_create_room(room_id_1, RoomState::Joined); @@ -766,7 +773,8 @@ mod tests { let room_id_1 = room_id!("!r1"); let thread_id_0_0 = event_id!("$ev0.0"); - let (client, _server) = logged_in_client_with_server().await; + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; client.base_client().get_or_create_room(room_id_0, RoomState::Joined); client.base_client().get_or_create_room(room_id_1, RoomState::Joined); @@ -995,19 +1003,91 @@ mod tests { } #[async_test] - async fn test_compute_latest_events() { + async fn test_latest_event_value_is_updated_via_event_cache() { let room_id = owned_room_id!("!r0"); + let user_id = user_id!("@mnt_io:matrix.org"); + let event_factory = EventFactory::new().sender(user_id).room(&room_id); + let event_id_0 = event_id!("$ev0"); + let event_id_1 = event_id!("$ev1"); + let event_id_2 = event_id!("$ev2"); - let (client, _server) = logged_in_client_with_server().await; - let weak_client = WeakClient::from_client(&client); + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Prelude. + { + // Create the room. + client.base_client().get_or_create_room(&room_id, RoomState::Joined); + + // Initialise the event cache store. + client + .event_cache_store() + .lock() + .await + .unwrap() + .handle_linked_chunk_updates( + LinkedChunkId::Room(&room_id), + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(0), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(0), 0), + items: vec![ + event_factory.text_msg("hello").event_id(event_id_0).into(), + event_factory.text_msg("world").event_id(event_id_1).into(), + ], + }, + ], + ) + .await + .unwrap(); + } let event_cache = client.event_cache(); event_cache.subscribe().unwrap(); - let (room_registration_sender, _room_registration_receiver) = mpsc::channel(1); - let registered_rooms = - RegisteredRooms::new(room_registration_sender, weak_client, event_cache); + let latest_events = client.latest_events().await; - compute_latest_events(®istered_rooms, &[room_id]).await; + // Subscribe to the latest event values for this room. + let mut latest_event_stream = + latest_events.listen_and_subscribe_to_room(&room_id).await.unwrap().unwrap(); + + // The initial latest event value is set to `event_id_1` because it's… the… + // latest event! + assert_matches!( + latest_event_stream.get().await, + LatestEventValue::RoomMessage(event) => { + assert_eq!(event.event_id(), event_id_1); + } + ); + + // The stream is pending: no new latest event for the moment. + assert_pending!(latest_event_stream); + + // Update the event cache with a sync. + server + .sync_room( + &client, + JoinedRoomBuilder::new(&room_id).add_timeline_event( + event_factory + .text_msg("venez découvrir cette nouvelle raclette !") + .event_id(event_id_2) + .into_raw(), + ), + ) + .await; + + // The event cache has received its update from the sync. It has emitted a + // generic update, which has been received by `LatestEvents` tasks, up to the + // `compute_latest_events` which has updated the latest event value. + assert_matches!( + latest_event_stream.next().await, + Some(LatestEventValue::RoomMessage(event)) => { + assert_eq!(event.event_id(), event_id_2); + } + ); } } diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index 268fe4165..64afcbc34 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -26,6 +26,11 @@ use matrix_sdk_common::deserialized_responses::{ use ruma::{ events::{ beacon::BeaconEventContent, + call::{ + invite::CallInviteEventContent, + notify::{ApplicationType, CallNotifyEventContent, NotifyType}, + SessionDescription, + }, member_hints::MemberHintsEventContent, poll::{ unstable_end::UnstablePollEndEventContent, @@ -60,13 +65,13 @@ use ruma::{ sticker::StickerEventContent, typing::TypingEventContent, AnyMessageLikeEvent, AnyStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, - AnyTimelineEvent, BundledMessageLikeRelations, RedactedMessageLikeEventContent, + AnyTimelineEvent, BundledMessageLikeRelations, Mentions, RedactedMessageLikeEventContent, RedactedStateEventContent, StateEventContent, StaticEventContent, }, serde::Raw, server_name, EventId, Int, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, - OwnedRoomAliasId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, - TransactionId, UInt, UserId, + OwnedRoomAliasId, OwnedRoomId, OwnedTransactionId, OwnedUserId, OwnedVoipId, RoomId, + RoomVersionId, TransactionId, UInt, UserId, VoipVersionId, }; use serde::Serialize; use serde_json::json; @@ -928,6 +933,28 @@ impl EventFactory { self.event(StickerEventContent::new(body.into(), info, url)) } + /// Create a new `m.call.invite` event. + pub fn call_invite( + &self, + call_id: OwnedVoipId, + lifetime: UInt, + offer: SessionDescription, + version: VoipVersionId, + ) -> EventBuilder { + self.event(CallInviteEventContent::new(call_id, lifetime, offer, version)) + } + + /// Create a new `m.call.notify` event. + pub fn call_notify( + &self, + call_id: String, + application: ApplicationType, + notify_type: NotifyType, + mentions: Mentions, + ) -> EventBuilder { + self.event(CallNotifyEventContent::new(call_id, application, notify_type, mentions)) + } + /// Set the next server timestamp. /// /// Timestamps will continue to increase by 1 (millisecond) from that value.