diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs index 121d57b9956..6774102f227 100644 --- a/crates/matrix-sdk/src/test_utils/mocks.rs +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -27,11 +27,16 @@ use matrix_sdk_test::{ test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, }; -use ruma::{directory::PublicRoomsChunk, MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName}; +use ruma::{ + directory::PublicRoomsChunk, + events::{AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType}, + serde::Raw, + MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName, +}; use serde::Deserialize; -use serde_json::json; +use serde_json::{json, Value}; use wiremock::{ - matchers::{body_partial_json, header, method, path, path_regex}, + matchers::{body_partial_json, header, method, path, path_regex, query_param}, Mock, MockBuilder, MockGuard, MockServer, Request, Respond, ResponseTemplate, Times, }; @@ -312,11 +317,61 @@ impl MatrixMockServer { /// ``` pub fn mock_room_send(&self) -> MockEndpoint<'_, RoomSendEndpoint> { let mock = Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")); + .and(header("authorization", "Bearer 1234")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/.*".to_owned())); MockEndpoint { mock, server: &self.server, endpoint: RoomSendEndpoint } } + /// Creates a prebuilt mock for sending a state event in a room. + /// + /// Similar to: [`MatrixMockServer::mock_room_send`] + /// + /// Note: works with *any* room. + /// Note: works with *any* event type. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// mock_server + /// .mock_room_send_state() + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response_not_mocked = room.send_raw("m.room.create", json!({ "body": "Hello world" })).await; + /// // The `/send` endpoint should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// + /// let response = room.send_state_event_raw("m.room.message", "my_key", json!({ "body": "Hello world" })).await?; + /// // The `/state` endpoint should be mocked by the server. + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn mock_room_send_state(&self) -> MockEndpoint<'_, RoomSendStateEndpoint> { + let mock = Mock::given(method("PUT")) + .and(header("authorization", "Bearer 1234")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/.*/.*")); + MockEndpoint { mock, server: &self.server, endpoint: RoomSendStateEndpoint::default() } + } + /// Creates a prebuilt mock for asking whether *a* room is encrypted or not. /// /// Note: Applies to all rooms. @@ -432,6 +487,15 @@ impl MatrixMockServer { } } + /// Create a prebuild mock for paginating room message with the `/messages` + /// endpoint. + pub fn mock_room_messages(&self) -> MockEndpoint<'_, RoomMessagesEndpoint> { + let mock = Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")); + MockEndpoint { mock, server: &self.server, endpoint: RoomMessagesEndpoint } + } + /// Create a prebuilt mock for uploading media. pub fn mock_upload(&self) -> MockEndpoint<'_, UploadEndpoint> { let mock = Mock::given(method("POST")) @@ -755,7 +819,7 @@ impl<'a, T> MockEndpoint<'a, T> { } } -/// A prebuilt mock for sending an event in a room. +/// A prebuilt mock for sending a message like event in a room. pub struct RoomSendEndpoint; impl<'a> MockEndpoint<'a, RoomSendEndpoint> { @@ -781,14 +845,14 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { /// .await; /// /// let event_id = event_id!("$some_id"); - /// let send_guard = mock_server + /// mock_server /// .mock_room_send() /// .body_matches_partial_json(json!({ /// "body": "Hello world", /// })) /// .ok(event_id) /// .expect(1) - /// .mount_as_scoped() + /// .mount() /// .await; /// /// let content = RoomMessageEventContent::text_plain("Hello world"); @@ -801,10 +865,63 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { /// ); /// # anyhow::Ok(()) }); /// ``` - pub fn body_matches_partial_json(self, body: serde_json::Value) -> Self { + pub fn body_matches_partial_json(self, body: Value) -> Self { Self { mock: self.mock.and(body_partial_json(body)), ..self } } + /// Ensures that the send endpoint request uses a specific event type. + /// + /// # Examples + /// + /// see also [`MatrixMockServer::mock_room_send`] for more context. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// mock_server + /// .mock_room_send() + /// .for_type("m.room.message".into()) + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response_not_mocked = room.send_raw("m.room.reaction", json!({ "body": "Hello world" })).await; + /// // The `m.room.reaction` event type should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// let response = room.send_raw("m.room.message", json!({ "body": "Hello world" })).await?; + /// // The `m.room.message` event type should be mocked by the server. + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn for_type(self, event_type: MessageLikeEventType) -> Self { + Self { + // Note: we already defined a path when constructing the mock builder, but this one + // ought to be more specialized. + mock: self + .mock + .and(path_regex(format!(r"^/_matrix/client/v3/rooms/.*/send/{event_type}",))), + ..self + } + } + /// Returns a send endpoint that emulates success, i.e. the event has been /// sent with the given event id. /// @@ -845,6 +962,231 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> { } } +/// A prebuilt mock for sending a state event in a room. +#[derive(Default)] +pub struct RoomSendStateEndpoint { + state_key: Option, + event_type: Option, +} + +impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> { + fn generate_path_regexp(endpoint: &RoomSendStateEndpoint) -> String { + format!( + r"^/_matrix/client/v3/rooms/.*/state/{}/{}", + endpoint.event_type.as_ref().map_or_else(|| ".*".to_owned(), |t| t.to_string()), + endpoint.state_key.as_ref().map_or_else(|| ".*".to_owned(), |k| k.to_string()) + ) + } + + /// Ensures that the body of the request is a superset of the provided + /// `body` parameter. + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{room_id, event_id, events::room::power_levels::RoomPowerLevelsEventContent}, + /// test_utils::mocks::MatrixMockServer + /// }; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// mock_server + /// .mock_room_send_state() + /// .body_matches_partial_json(json!({ + /// "redact": 51, + /// })) + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let mut content = RoomPowerLevelsEventContent::new(); + /// // Update the power level to a non default value. + /// // Otherwise it will be skipped from serialization. + /// content.redact = 51.into(); + /// + /// let response = room.send_state_event(content).await?; + /// + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn body_matches_partial_json(self, body: Value) -> Self { + Self { mock: self.mock.and(body_partial_json(body)), ..self } + } + + /// Ensures that the send endpoint request uses a specific event type. + /// + /// Note: works with *any* room. + /// + /// # Examples + /// + /// see also [`MatrixMockServer::mock_room_send`] for more context. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{ + /// event_id, + /// events::room::{ + /// create::RoomCreateEventContent, power_levels::RoomPowerLevelsEventContent, + /// }, + /// events::StateEventType, + /// room_id, + /// }, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await; + /// + /// let event_id = event_id!("$some_id"); + /// + /// mock_server + /// .mock_room_send_state() + /// .for_type(StateEventType::RoomPowerLevels) + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response_not_mocked = room.send_state_event(RoomCreateEventContent::new_v11()).await; + /// // The `m.room.reaction` event type should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// let response = room.send_state_event(RoomPowerLevelsEventContent::new()).await?; + /// // The `m.room.message` event type should be mocked by the server. + /// assert_eq!( + /// event_id, response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// + /// # anyhow::Ok(()) }); + /// ``` + pub fn for_type(mut self, event_type: StateEventType) -> Self { + self.endpoint.event_type = Some(event_type); + // Note: we may have already defined a path, but this one ought to be more + // specialized (unless for_key/for_type were called multiple times). + Self { mock: self.mock.and(path_regex(Self::generate_path_regexp(&self.endpoint))), ..self } + } + + /// + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ + /// ruma::{ + /// event_id, + /// events::{call::member::CallMemberEventContent, AnyStateEventContent}, + /// room_id, + /// }, + /// test_utils::mocks::MatrixMockServer, + /// }; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await; + /// + /// let event_id = event_id!("$some_id"); + /// + /// mock_server + /// .mock_room_send_state() + /// .for_key("my_key".to_owned()) + /// .ok(event_id) + /// .expect(1) + /// .mount() + /// .await; + /// + /// let response_not_mocked = room + /// .send_state_event_for_key( + /// "", + /// AnyStateEventContent::CallMember(CallMemberEventContent::new_empty(None)), + /// ) + /// .await; + /// // The `m.room.reaction` event type should not be mocked by the server. + /// assert!(response_not_mocked.is_err()); + /// + /// let response = room + /// .send_state_event_for_key( + /// "my_key", + /// AnyStateEventContent::CallMember(CallMemberEventContent::new_empty(None)), + /// ) + /// .await + /// .unwrap(); + /// + /// // The `m.room.message` event type should be mocked by the server. + /// assert_eq!( + /// event_id, response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn for_key(mut self, state_key: String) -> Self { + self.endpoint.state_key = Some(state_key); + // Note: we may have already defined a path, but this one ought to be more + // specialized (unless for_key/for_type were called multiple times). + Self { mock: self.mock.and(path_regex(Self::generate_path_regexp(&self.endpoint))), ..self } + } + + /// Returns a send endpoint that emulates success, i.e. the event has been + /// sent with the given event id. + /// + /// # Examples + /// ``` + /// # tokio_test::block_on(async { + /// use matrix_sdk::{ruma::{room_id, event_id}, test_utils::mocks::MatrixMockServer}; + /// use serde_json::json; + /// + /// let mock_server = MatrixMockServer::new().await; + /// let client = mock_server.client_builder().build().await; + /// + /// mock_server.mock_room_state_encryption().plain().mount().await; + /// + /// let room = mock_server + /// .sync_joined_room(&client, room_id!("!room_id:localhost")) + /// .await; + /// + /// let event_id = event_id!("$some_id"); + /// let send_guard = mock_server + /// .mock_room_send_state() + /// .ok(event_id) + /// .expect(1) + /// .mount_as_scoped() + /// .await; + /// + /// let response = room.send_state_event_raw("m.room.message", "my_key", json!({ "body": "Hello world" })).await?; + /// + /// assert_eq!( + /// event_id, + /// response.event_id, + /// "The event ID we mocked should match the one we received when we sent the event" + /// ); + /// # anyhow::Ok(()) }); + /// ``` + pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { + self.ok_with_event_id(returned_event_id.into()) + } +} + /// A prebuilt mock for running sync v2. pub struct SyncEndpoint { sync_response_builder: Arc>, @@ -1024,6 +1366,38 @@ impl<'a> MockEndpoint<'a, RoomEventEndpoint> { } } +/// A prebuilt mock for the `/messages` endpoint. +pub struct RoomMessagesEndpoint; + +/// A prebuilt mock for getting a room messages in a room. +impl<'a> MockEndpoint<'a, RoomMessagesEndpoint> { + /// Expects an optional limit to be set on the request. + pub fn limit(self, limit: u32) -> Self { + Self { mock: self.mock.and(query_param("limit", limit.to_string())), ..self } + } + + /// Returns a messages endpoint that emulates success, i.e. the messages + /// provided as `response` could be retrieved. + /// + /// Note: pass `chunk` in the correct order: topological for forward + /// pagination, reverse topological for backwards pagination. + pub fn ok( + self, + start: String, + end: Option, + chunk: Vec>>, + state: Vec>>, + ) -> MatrixMock<'a> { + let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "start": start, + "end": end, + "chunk": chunk.into_iter().map(|ev| ev.into()).collect::>(), + "state": state.into_iter().map(|ev| ev.into()).collect::>(), + }))); + MatrixMock { server: self.server, mock } + } +} + /// A prebuilt mock for uploading media. pub struct UploadEndpoint; diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index f6fc42dd2ad..17e50d04f37 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -257,6 +257,7 @@ async fn test_ignored_unignored() { /// Puts a mounting point for /messages for a pagination request, matching /// against a precise `from` token given as `expected_from`, and returning the /// chunk of events and the next token as `end` (if available). +// TODO: replace this with the `mock_room_messages` from mocks.rs async fn mock_messages( server: &MockServer, expected_from: &str, diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index ecd00353937..d5ed2702d3f 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -25,18 +25,23 @@ use matrix_sdk::{ Client, }; use matrix_sdk_common::{executor::spawn, timeout::timeout}; -use matrix_sdk_test::{async_test, EventBuilder, JoinedRoomBuilder, ALICE, BOB}; +use matrix_sdk_test::{ + async_test, event_factory::EventFactory, EventBuilder, JoinedRoomBuilder, ALICE, BOB, +}; use once_cell::sync::Lazy; use ruma::{ event_id, - events::room::{ - member::{MembershipState, RoomMemberEventContent}, - message::RoomMessageEventContent, - name::RoomNameEventContent, - topic::RoomTopicEventContent, + events::{ + room::{ + member::{MembershipState, RoomMemberEventContent}, + message::RoomMessageEventContent, + name::RoomNameEventContent, + topic::RoomTopicEventContent, + }, + AnyStateEvent, StateEventType, }, owned_room_id, - serde::JsonObject, + serde::{JsonObject, Raw}, user_id, OwnedRoomId, }; use serde::Serialize; @@ -297,51 +302,22 @@ async fn test_read_messages_with_msgtype_capabilities() { // No messages from the driver assert_matches!(recv_message(&driver_handle).now_or_never(), None); + let f = EventFactory::new().room(&ROOM_ID).sender(user_id!("@example:localhost")); + { - let response_json = json!({ - "chunk": [ - { - "content": { - "body": "custom content", - "msgtype": "m.custom.element", - }, - "event_id": "$msda7m0df9E9op3", - "origin_server_ts": 152037220, - "sender": "@example:localhost", - "type": "m.room.message", - "room_id": &*ROOM_ID, - }, - { - "content": { - "body": "hello", - "msgtype": "m.text", - }, - "event_id": "$msda7m0df9E9op5", - "origin_server_ts": 152037280, - "sender": "@example:localhost", - "type": "m.room.message", - "room_id": &*ROOM_ID, - }, - { - "content": { - }, - "event_id": "$msda7m0df9E9op7", - "origin_server_ts": 152037290, - "sender": "@example:localhost", - "type": "m.reaction", - "room_id": &*ROOM_ID, - }, - ], - "end": "t47409-4357353_219380_26003_2269", - "start": "t392-516_47314_0_7_1_1_1_11444_1" - }); - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/messages$")) - .and(header("authorization", "Bearer 1234")) - .and(query_param("limit", "3")) - .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) - .expect(1) - .mount(mock_server.server()) + let start = "t392-516_47314_0_7_1_1_1_11444_1".to_owned(); + let end = Some("t47409-4357353_219380_26003_2269".to_owned()); + let chun2 = vec![ + f.notice("custom content").event_id(event_id!("$msda7m0df9E9op3")).into_raw_timeline(), + f.text_msg("hello").event_id(event_id!("$msda7m0df9E9op5")).into_raw_timeline(), + f.reaction(event_id!("$event_id"), "annotation".to_owned()).into_raw_timeline(), + ]; + mock_server + .mock_room_messages() + .limit(3) + .ok(start, end, chun2, Vec::>::new()) + .mock_once() + .mount() .await; // Ask the driver to read messages @@ -508,11 +484,12 @@ async fn test_send_room_message() { negotiate_capabilities(&driver_handle, json!(["org.matrix.msc2762.send.event:m.room.message"])) .await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/m.room.message/.*$")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) - .expect(1) - .mount(mock_server.server()) + mock_server + .mock_room_send() + .for_type("m.room.message".into()) + .ok(event_id!("$foobar")) + .mock_once() + .mount() .await; send_request( @@ -549,11 +526,12 @@ async fn test_send_room_name() { ) .await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/?$")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) - .expect(1) - .mount(mock_server.server()) + mock_server + .mock_room_send_state() + .for_type(StateEventType::RoomName) + .ok(event_id!("$foobar")) + .mock_once() + .mount() .await; send_request( @@ -594,7 +572,8 @@ async fn test_send_delayed_message_event() { .await; Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/m.room.message/.*$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/send/m.room.message/.*")) + .and(query_param("org.matrix.msc4140.delay", "1000")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "delay_id": "1234", }))) @@ -641,7 +620,8 @@ async fn test_send_delayed_state_event() { .await; Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/?$")) + .and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.room.name/.*")) + .and(query_param("org.matrix.msc4140.delay", "1000")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "delay_id": "1234", })))