diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index c63a0e42968..65dcb153196 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -343,7 +343,7 @@ impl SyncTimelineEvent { /// Get the event id of this `SyncTimelineEvent` if the event has any valid /// id. pub fn event_id(&self) -> Option { - self.kind.raw().get_field::("event_id").ok().flatten() + self.kind.event_id() } /// Returns a reference to the (potentially decrypted) Matrix event inside @@ -529,6 +529,12 @@ impl TimelineEventKind { } } + /// Get the event id of this `TimelineEventKind` if the event has any valid + /// id. + pub fn event_id(&self) -> Option { + self.raw().get_field::("event_id").ok().flatten() + } + /// If the event was a decrypted event that was successfully decrypted, get /// its encryption info. Otherwise, `None`. pub fn encryption_info(&self) -> Option<&EncryptionInfo> { diff --git a/crates/matrix-sdk/src/test_utils.rs b/crates/matrix-sdk/src/test_utils.rs index f4aee4b6e14..0a6996ad756 100644 --- a/crates/matrix-sdk/src/test_utils.rs +++ b/crates/matrix-sdk/src/test_utils.rs @@ -14,6 +14,9 @@ use url::Url; pub mod events; +#[cfg(not(target_arch = "wasm32"))] +pub mod mocks; + use crate::{ config::RequestConfig, matrix_auth::{MatrixSession, MatrixSessionTokens}, diff --git a/crates/matrix-sdk/src/test_utils/mocks.rs b/crates/matrix-sdk/src/test_utils/mocks.rs new file mode 100644 index 00000000000..935ccc8c639 --- /dev/null +++ b/crates/matrix-sdk/src/test_utils/mocks.rs @@ -0,0 +1,471 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Helpers to mock a server and have a client automatically connected to that +//! server, for the purpose of integration tests. + +#![allow(missing_debug_implementations)] + +use std::sync::{Arc, Mutex}; + +use matrix_sdk_base::deserialized_responses::TimelineEvent; +use matrix_sdk_test::{ + test_json, InvitedRoomBuilder, JoinedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder, + SyncResponseBuilder, +}; +use ruma::{OwnedEventId, OwnedRoomId, RoomId}; +use serde_json::json; +use wiremock::{ + matchers::{header, method, path, path_regex}, + Mock, MockBuilder, MockGuard, MockServer, Respond, ResponseTemplate, Times, +}; + +use super::logged_in_client; +use crate::{Client, Room}; + +/// A `wiremock` server along with a client connected to it, with useful methods +/// to help mocking Matrix client-server API endpoints easily. +/// +/// This is a pair of a [`MockServer`] and a [`Client]` (one can retrieve them +/// respectively with [`Self::server()`] and [`Self::client()`]). +/// +/// It implements mock endpoints, limiting the shared code as much as possible, +/// so the mocks are still flexible to use as scoped/unscoped mounts, named, and +/// so on. +/// +/// It works like this: +/// +/// - start by saying which endpoint you'd like to mock, e.g. +/// [`Self::mock_room_send()`]. This returns a specialized `MockSomething` +/// data structure, with its own impl. For this example, it's +/// [`MockRoomSend`]. +/// - configure the response on the endpoint-specific mock data structure. For +/// instance, if you want the sending to result in a transient failure, call +/// [`MockRoomSend::error500`]; if you want it to succeed and return the event +/// `$42`, call [`MockRoomSend::ok`]. It's still possible to call +/// [`MockRoomSend::respond_with()`], as we do with wiremock MockBuilder, for +/// maximum flexibility when the helpers aren't sufficient. +/// - once the endpoint's response is configured, for any mock builder, you get +/// a [`MatrixMock`]; this is a plain [`wiremock::Mock`] with the server +/// curried, so one doesn't have to pass it around when calling +/// [`MatrixMock::mount()`] or [`MatrixMock::mount_as_scoped()`]. As such, it +/// mostly defers its implementations to [`wiremock::Mock`] under the hood. +pub struct MatrixMockServer { + server: MockServer, + client: Client, + + /// Make the sync response builder stateful, to keep in memory the batch + /// token and avoid the client ignoring subsequent responses after the first + /// one. + sync_response_builder: Arc>, +} + +impl MatrixMockServer { + /// Create a new `wiremock` server specialized for Matrix usage. + pub async fn new() -> Self { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri().to_string())).await; + Self { client, server, sync_response_builder: Default::default() } + } + + /// Creates a new [`MatrixMockServer`] when both parts have been already + /// created. + pub fn from_parts(server: MockServer, client: Client) -> Self { + Self { client, server, sync_response_builder: Default::default() } + } + + /// Return the underlying client. + pub fn client(&self) -> Client { + self.client.clone() + } + + /// Return the underlying server. + pub fn server(&self) -> &MockServer { + &self.server + } + + /// Overrides the sync/ endpoint with knowledge that the given + /// invited/joined/knocked/left room exists, runs a sync and returns the + /// given room. + pub async fn sync_room(&self, room_id: &RoomId, room_data: impl Into) -> Room { + let any_room = room_data.into(); + + self.mock_sync() + .ok_and_run(move |builder| match any_room { + AnyRoomBuilder::Invited(invited) => { + builder.add_invited_room(invited); + } + AnyRoomBuilder::Joined(joined) => { + builder.add_joined_room(joined); + } + AnyRoomBuilder::Left(left) => { + builder.add_left_room(left); + } + AnyRoomBuilder::Knocked(knocked) => { + builder.add_knocked_room(knocked); + } + }) + .await; + + self.client.get_room(room_id).expect("look at me, the room is known now") + } + + /// Overrides the sync/ endpoint with knowledge that the given room exists + /// in the joined state, runs a sync and returns the given room. + pub async fn sync_joined_room(&self, room_id: &RoomId) -> Room { + self.sync_room(room_id, JoinedRoomBuilder::new(room_id)).await + } + + /// Verify that the previous mocks expected number of requests match + /// reality, and then cancels all active mocks. + pub async fn verify_and_reset(&self) { + self.server.verify().await; + self.server.reset().await; + } +} + +// Specific mount endpoints. +impl MatrixMockServer { + /// Mocks a sync endpoint. + pub fn mock_sync(&self) -> MockSync<'_> { + let mock = Mock::given(method("GET")) + .and(path("/_matrix/client/r0/sync")) + .and(header("authorization", "Bearer 1234")); + MockSync { + mock, + client: self.client.clone(), + server: &self.server, + sync_response_builder: self.sync_response_builder.clone(), + } + } + + /// Creates a prebuilt mock for sending an event in a room. + /// + /// Note: works with *any* room. + pub fn mock_room_send(&self) -> MockRoomSend<'_> { + let mock = Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")); + MockRoomSend { mock, server: &self.server } + } + + /// Creates a prebuilt mock for asking whether *a* room is encrypted or not. + /// + /// Note: Applies to all rooms. + pub fn mock_room_state_encryption(&self) -> MockEncryptionState<'_> { + let mock = Mock::given(method("GET")) + .and(header("authorization", "Bearer 1234")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")); + MockEncryptionState { mock, server: &self.server } + } + + /// Creates a prebuilt mock for setting the room encryption state. + /// + /// Note: Applies to all rooms. + pub fn mock_set_room_state_encryption(&self) -> MockSetEncryptionState<'_> { + let mock = Mock::given(method("PUT")) + .and(header("authorization", "Bearer 1234")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?")); + MockSetEncryptionState { mock, server: &self.server } + } + + /// Creates a prebuilt mock for the room redact endpoint. + pub fn mock_room_redact(&self) -> MockRoomRedact<'_> { + let mock = Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) + .and(header("authorization", "Bearer 1234")); + MockRoomRedact { mock, server: &self.server } + } + + /// Creates a prebuilt mock for retrieving an event with /room/.../event. + pub fn mock_room_event(&self) -> MockRoomEvent<'_> { + let mock = Mock::given(method("GET")).and(header("authorization", "Bearer 1234")); + MockRoomEvent { mock, server: &self.server, room: None, match_event_id: false } + } +} + +/// Parameter to [`MatrixMockServer::sync_room`]. +pub enum AnyRoomBuilder { + /// A room we've been invited to. + Invited(InvitedRoomBuilder), + /// A room we've joined. + Joined(JoinedRoomBuilder), + /// A room we've left. + Left(LeftRoomBuilder), + /// A room we've knocked to. + Knocked(KnockedRoomBuilder), +} + +impl From for AnyRoomBuilder { + fn from(val: InvitedRoomBuilder) -> AnyRoomBuilder { + AnyRoomBuilder::Invited(val) + } +} + +impl From for AnyRoomBuilder { + fn from(val: JoinedRoomBuilder) -> AnyRoomBuilder { + AnyRoomBuilder::Joined(val) + } +} + +impl From for AnyRoomBuilder { + fn from(val: LeftRoomBuilder) -> AnyRoomBuilder { + AnyRoomBuilder::Left(val) + } +} + +impl From for AnyRoomBuilder { + fn from(val: KnockedRoomBuilder) -> AnyRoomBuilder { + AnyRoomBuilder::Knocked(val) + } +} + +/// A wrapper for a [`Mock`] as well as a [`MockServer`], allowing us to call +/// [`Mock::mount`] or [`Mock::mount_as_scoped`] without having to pass the +/// [`MockServer`] reference (i.e. call `mount()` instead of `mount(&server)`). +pub struct MatrixMock<'a> { + mock: Mock, + server: &'a MockServer, +} + +impl<'a> MatrixMock<'a> { + /// Set an expectation on the number of times this [`MatrixMock`] should + /// match in the current test case. + /// + /// Expectations are verified when the server is shutting down: if + /// the expectation is not satisfied, the [`MatrixMockServer`] will panic + /// and the `error_message` is shown. + /// + /// By default, no expectation is set for [`MatrixMock`]s. + pub fn expect>(self, num_calls: T) -> Self { + Self { mock: self.mock.expect(num_calls), ..self } + } + + /// Assign a name to your mock. + /// + /// The mock name will be used in error messages (e.g. if the mock + /// expectation is not satisfied) and debug logs to help you identify + /// what failed. + pub fn named(self, name: impl Into) -> Self { + Self { mock: self.mock.named(name), ..self } + } + + /// Respond to a response of this endpoint exactly once. + /// + /// After it's been called, subsequent responses will hit the next handler + /// or a 404. + /// + /// Also verifies that it's been called once. + pub fn mock_once(self) -> Self { + Self { mock: self.mock.up_to_n_times(1).expect(1), ..self } + } + + /// Mount a [`MatrixMock`] on the attached server. + /// + /// The [`MatrixMock`] will remain active until the [`MatrixMockServer`] is + /// shut down. If you want to control or limit how long your + /// [`MatrixMock`] stays active, check out [`Self::mount_as_scoped`]. + pub async fn mount(self) { + self.mock.mount(self.server).await; + } + + /// Mount a [`MatrixMock`] as **scoped** on the attached server. + /// + /// When using [`Self::mount`], your [`MatrixMock`]s will be active until + /// the [`MatrixMockServer`] is shut down. + /// + /// When using `mount_as_scoped`, your [`MatrixMock`]s will be active as + /// long as the returned [`MockGuard`] is not dropped. + /// + /// When the returned [`MockGuard`] is dropped, [`MatrixMockServer`] will + /// verify that the expectations set on the scoped [`MatrixMock`] were + /// verified - if not, it will panic. + pub async fn mount_as_scoped(self) -> MockGuard { + self.mock.mount_as_scoped(self.server).await + } +} + +/// A prebuilt mock for sending events to a room. +pub struct MockRoomSend<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockRoomSend<'a> { + /// Returns a send endpoint that emulates success, i.e. the event has been + /// sent with the given event id. + pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { + let returned_event_id = returned_event_id.into(); + MatrixMock { + mock: self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "event_id": returned_event_id + }))), + server: self.server, + } + } + + /// Returns a send endpoint that emulates a transient failure, i.e responds + /// with error 500. + pub fn error500(self) -> MatrixMock<'a> { + MatrixMock { mock: self.mock.respond_with(ResponseTemplate::new(500)), server: self.server } + } + + /// Returns a send endpoint that emulates a permanent failure (event is too + /// large). + pub fn error_too_large(self) -> MatrixMock<'a> { + MatrixMock { + mock: self.mock.respond_with(ResponseTemplate::new(413).set_body_json(json!({ + // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response + "errcode": "M_TOO_LARGE", + }))), + server: self.server, + } + } + + /// Specify how to respond to a query (viz., like + /// [`MockBuilder::respond_with`] does), when other predefined responses + /// aren't sufficient. + pub fn respond_with(self, func: R) -> MatrixMock<'a> { + MatrixMock { mock: self.mock.respond_with(func), server: self.server } + } +} + +/// A prebuilt mock for running sync v2. +pub struct MockSync<'a> { + mock: MockBuilder, + server: &'a MockServer, + sync_response_builder: Arc>, + client: Client, +} + +impl<'a> MockSync<'a> { + /// Temporarily mocks the sync with the given endpoint and runs a client + /// sync with it. + /// + /// After calling this function, the sync endpoint isn't mocked anymore. + pub async fn ok_and_run(self, func: F) { + let json_response = { + let mut builder = self.sync_response_builder.lock().unwrap(); + func(&mut builder); + builder.build_json_sync_response() + }; + + let _scope = self + .mock + .respond_with(ResponseTemplate::new(200).set_body_json(json_response)) + .mount_as_scoped(self.server) + .await; + + let _response = self.client.sync_once(Default::default()).await.unwrap(); + } +} + +/// A prebuilt mock for reading the encryption state of a room. +pub struct MockEncryptionState<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockEncryptionState<'a> { + /// Marks the room as encrypted. + pub fn encrypted(self) -> MatrixMock<'a> { + let mock = self.mock.respond_with( + ResponseTemplate::new(200).set_body_json(&*test_json::sync_events::ENCRYPTION_CONTENT), + ); + MatrixMock { mock, server: self.server } + } + + /// Marks the room as not encrypted. + pub fn plain(self) -> MatrixMock<'a> { + let mock = self + .mock + .respond_with(ResponseTemplate::new(404).set_body_json(&*test_json::NOT_FOUND)); + MatrixMock { mock, server: self.server } + } +} + +/// A prebuilt mock for setting the encryption state of a room. +pub struct MockSetEncryptionState<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockSetEncryptionState<'a> { + /// Returns a mock for a successful setting of the encryption state event. + pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { + let event_id = returned_event_id.into(); + let mock = self.mock.respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id })), + ); + MatrixMock { server: self.server, mock } + } +} + +/// A prebuilt mock for redacting an event in a room. +pub struct MockRoomRedact<'a> { + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockRoomRedact<'a> { + /// Returns a redact endpoint that emulates success, i.e. the redaction + /// event has been sent with the given event id. + pub fn ok(self, returned_event_id: impl Into) -> MatrixMock<'a> { + let event_id = returned_event_id.into(); + let mock = self.mock.respond_with( + ResponseTemplate::new(200).set_body_json(json!({ "event_id": event_id })), + ); + MatrixMock { server: self.server, mock } + } +} + +/// A prebuilt mock for getting a single event in a room. +pub struct MockRoomEvent<'a> { + room: Option, + match_event_id: bool, + server: &'a MockServer, + mock: MockBuilder, +} + +impl<'a> MockRoomEvent<'a> { + /// Limits the scope of this mock to a specific room. + pub fn room(self, room: impl Into) -> Self { + Self { room: Some(room.into()), ..self } + } + + /// Whether the mock checks for the event id from the event. + pub fn match_event_id(self) -> Self { + Self { match_event_id: true, ..self } + } + + /// Returns a redact endpoint that emulates success, i.e. the redaction + /// event has been sent with the given event id. + pub fn ok(self, event: TimelineEvent) -> MatrixMock<'a> { + let event_path = if self.match_event_id { + let event_id = event.kind.event_id().expect("an event id is required"); + event_id.to_string() + } else { + // Event is at the end, so no need to add anything. + "".to_owned() + }; + + let room_path = self.room.map_or_else(|| ".*".to_owned(), |room| room.to_string()); + + let mock = self + .mock + .and(path_regex(format!("^/_matrix/client/r0/rooms/{room_path}/event/{event_path}"))) + .respond_with(ResponseTemplate::new(200).set_body_json(event.into_raw().json())); + MatrixMock { server: self.server, mock } + } +} diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 13cebccde96..10205a5604f 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -1,9 +1,8 @@ // The http mocking library is not supported for wasm32 #![cfg(not(target_arch = "wasm32"))] -use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server, Client, Room}; -use matrix_sdk_test::{test_json, SyncResponseBuilder}; -use ruma::RoomId; +use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server, Client}; +use matrix_sdk_test::test_json; use serde::Serialize; use wiremock::{ matchers::{header, method, path, query_param, query_param_is_missing}, @@ -79,28 +78,3 @@ async fn mock_sync_scoped( .mount_as_scoped(server) .await } - -/// Does a sync for a given room, and returns its `Room` object. -/// -/// Note this sync is token-less. -async fn mock_sync_with_new_room( - func: F, - client: &Client, - server: &MockServer, - room_id: &RoomId, -) -> Room { - let mut sync_response_builder = SyncResponseBuilder::default(); - func(&mut sync_response_builder); - let json_response = sync_response_builder.build_json_sync_response(); - - let _scope = Mock::given(method("GET")) - .and(path("/_matrix/client/r0/sync")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json_response)) - .mount_as_scoped(server) - .await; - - let _response = client.sync_once(Default::default()).await.unwrap(); - - client.get_room(room_id).expect("we should find the room we just sync'd from") -} diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index e8f294bb0d0..70e4d31200d 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -7,7 +7,7 @@ use futures_util::future::join_all; use matrix_sdk::{ config::SyncSettings, room::{edit::EditedContent, Receipts, ReportedContentScore, RoomMemberRole}, - test_utils::events::EventFactory, + test_utils::{events::EventFactory, mocks::MatrixMockServer}, }; use matrix_sdk_base::RoomState; use matrix_sdk_test::{ @@ -33,7 +33,7 @@ use wiremock::{ Mock, ResponseTemplate, }; -use crate::{logged_in_client_with_server, mock_sync, mock_sync_with_new_room, synced_client}; +use crate::{logged_in_client_with_server, mock_sync, synced_client}; #[async_test] async fn test_invite_user_by_id() { let (client, server) = logged_in_client_with_server().await; @@ -720,78 +720,43 @@ async fn test_make_reply_event_doesnt_require_event_cache() { // Even if we don't have enabled the event cache, we'll resort to using the // /event query to get details on an event. - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; + let user_id = mock.client().user_id().unwrap().to_owned(); - let event_id = event_id!("$1"); - let resp_event_id = event_id!("$resp"); let room_id = room_id!("!galette:saucisse.bzh"); + let room = mock.sync_joined_room(room_id).await; + let event_id = event_id!("$1"); let f = EventFactory::new(); - - let raw_original_event = f - .text_msg("hi") - .event_id(event_id) - .sender(client.user_id().unwrap()) - .room(room_id) - .into_raw_timeline(); - - mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(raw_original_event.json())) + mock.mock_room_event() + .ok(f.text_msg("hi").event_id(event_id).sender(&user_id).room(room_id).into_timeline()) .expect(1) .named("/event") - .mount(&server) + .mount() .await; let new_content = RoomMessageEventContentWithoutRelation::text_plain("uh i mean bonjour"); - let room = client.get_room(room_id).unwrap(); - // make_edit_event works, even if the event cache hasn't been enabled. - room.make_edit_event(resp_event_id, EditedContent::RoomMessage(new_content)).await.unwrap(); + room.make_edit_event(event_id, EditedContent::RoomMessage(new_content)).await.unwrap(); } #[async_test] async fn test_enable_encryption_doesnt_stay_unencrypted() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; - mock_encryption_state(&server, false).await; - - Mock::given(method("PUT")) - .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!({ "event_id": "$1"}))) - .mount(&server) - .await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_set_room_state_encryption().ok(event_id!("$1")).mount().await; let room_id = room_id!("!a:b.c"); - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; assert!(!room.is_encrypted().await.unwrap()); room.enable_encryption().await.expect("enabling encryption should work"); - server.reset().await; - mock_encryption_state(&server, true).await; + mock.verify_and_reset().await; + mock.mock_room_state_encryption().encrypted().mount().await; assert!(room.is_encrypted().await.unwrap()); } diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index ecdb57f451f..34e3efa7b6a 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -1,11 +1,4 @@ -use std::{ - ops::Not as _, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Mutex as StdMutex, - }, - time::Duration, -}; +use std::{ops::Not as _, sync::Arc, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use matrix_sdk::{ @@ -17,15 +10,11 @@ use matrix_sdk::{ RoomSendQueueUpdate, }, test_utils::{ - events::EventFactory, logged_in_client, logged_in_client_with_server, set_client_session, + events::EventFactory, logged_in_client, mocks::MatrixMockServer, set_client_session, }, Client, MemoryStore, }; -use matrix_sdk_test::{ - async_test, - mocks::{mock_encryption_state, mock_redaction}, - InvitedRoomBuilder, JoinedRoomBuilder, LeftRoomBuilder, -}; +use matrix_sdk_test::{async_test, InvitedRoomBuilder, KnockedRoomBuilder, LeftRoomBuilder}; use ruma::{ api::MatrixVersion, event_id, @@ -42,7 +31,7 @@ use ruma::{ }, mxc_uri, owned_user_id, room_id, serde::Raw, - uint, EventId, MxcUri, OwnedEventId, TransactionId, + uint, MxcUri, OwnedEventId, TransactionId, }; use serde_json::json; use tokio::{ @@ -50,12 +39,10 @@ use tokio::{ time::{sleep, timeout}, }; use wiremock::{ - matchers::{header, method, path, path_regex}, + matchers::{header, method, path}, Mock, Request, ResponseTemplate, }; -use crate::mock_sync_with_new_room; - // TODO put into the MatrixMockServer fn mock_jpeg_upload(mxc: &MxcUri, lock: Arc>) -> Mock { let mxc = mxc.to_owned(); @@ -79,24 +66,6 @@ fn mock_jpeg_upload(mxc: &MxcUri, lock: Arc>) -> Mock { }) } -fn mock_send_event(returned_event_id: &EventId) -> Mock { - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "event_id": returned_event_id, - }))) -} - -/// Return a mock that will fail all requests to /rooms/ROOM_ID/send with a -/// transient 500 error. -fn mock_send_transient_failure() -> Mock { - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(500)) -} - // A macro to assert on a stream of `RoomSendQueueUpdate`s. macro_rules! assert_update { // Check the next stream event is a local echo for an uploaded media. @@ -238,20 +207,11 @@ macro_rules! assert_update { #[async_test] async fn test_cant_send_invited_room() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // When I'm invited to a room, let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_invited_room(InvitedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_room(room_id, InvitedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -262,20 +222,28 @@ async fn test_cant_send_invited_room() { #[async_test] async fn test_cant_send_left_room() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // When I've left a room, let room_id = room_id!("!a:b.c"); + let room = mock.sync_room(room_id, LeftRoomBuilder::new(room_id)).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_left_room(LeftRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + // I can't send message to it with the send queue. + assert_matches!( + room.send_queue() + .send(RoomMessageEventContent::text_plain("Farewell, World!").into()) + .await, + Err(RoomSendQueueError::RoomNotJoined) + ); +} + +#[async_test] +async fn test_cant_send_knocked_room() { + let mock = MatrixMockServer::new().await; + + // When I've knocked into a room, + let room_id = room_id!("!a:b.c"); + let room = mock.sync_room(room_id, KnockedRoomBuilder::new(room_id)).await; // I can't send message to it with the send queue. assert_matches!( @@ -288,26 +256,17 @@ async fn test_cant_send_left_room() { #[async_test] async fn test_nothing_sent_when_disabled() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; // When I disable the send queue, let event_id = event_id!("$1"); - mock_send_event(event_id).expect(0).mount(&server).await; + mock.mock_room_send().ok(event_id).expect(0).mount().await; - client.send_queue().set_enabled(false).await; + mock.client().send_queue().set_enabled(false).await; // A message is queued, but never sent. room.send_queue() @@ -316,11 +275,10 @@ async fn test_nothing_sent_when_disabled() { .unwrap(); // But I can still send it with room.send(). - server.verify().await; - server.reset().await; + mock.verify_and_reset().await; - mock_encryption_state(&server, false).await; - mock_send_event(event_id).expect(1).mount(&server).await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id).expect(1).mount().await; let response = room.send(RoomMessageEventContent::text_plain("Hello, World!")).await.unwrap(); assert_eq!(response.event_id, event_id); @@ -328,20 +286,11 @@ async fn test_nothing_sent_when_disabled() { #[async_test] async fn test_smoke() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -357,11 +306,9 @@ async fn test_smoke() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -378,7 +325,7 @@ async fn test_smoke() { })) }) .expect(1) - .mount(&server) + .mount() .await; room.send_queue().send(RoomMessageEventContent::text_plain("1").into()).await.unwrap(); @@ -403,20 +350,11 @@ async fn test_smoke() { #[async_test] async fn test_smoke_raw() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -427,8 +365,8 @@ async fn test_smoke_raw() { // When the queue is enabled and I send message in some order, it does send it. let event_id = event_id!("$1"); - mock_encryption_state(&server, false).await; - mock_send_event(event_id!("$1")).mount(&server).await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id).mount().await; let json_content = r#"{"baguette": 42}"#.to_owned(); let event = Raw::from_json_string(json_content.clone()).unwrap(); @@ -456,8 +394,9 @@ async fn test_smoke_raw() { #[async_test] async fn test_error_then_locally_reenabling() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); // Starting with a globally enabled queue. @@ -466,16 +405,7 @@ async fn test_error_then_locally_reenabling() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -488,11 +418,10 @@ async fn test_error_then_locally_reenabling() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + let scoped_send = mock + .mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -507,7 +436,7 @@ async fn test_error_then_locally_reenabling() { ResponseTemplate::new(500) }) .expect(3) - .mount(&server) + .mount_as_scoped() .await; q.send(RoomMessageEventContent::text_plain("1").into()).await.unwrap(); @@ -548,8 +477,8 @@ async fn test_error_then_locally_reenabling() { // But the room send queue is disabled. assert!(!room.send_queue().is_enabled()); - server.reset().await; - mock_send_event(event_id!("$42")).expect(1).mount(&server).await; + drop(scoped_send); + mock.mock_room_send().ok(event_id!("$42")).expect(1).mount().await; // Re-enabling the *room* queue will re-send the same message in that room. room.send_queue().set_enabled(true); @@ -567,8 +496,9 @@ async fn test_error_then_locally_reenabling() { #[async_test] async fn test_error_then_globally_reenabling() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); // Starting with a globally enabled queue. @@ -577,16 +507,7 @@ async fn test_error_then_globally_reenabling() { // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -594,9 +515,9 @@ async fn test_error_then_globally_reenabling() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; - mock_encryption_state(&server, false).await; - mock_send_transient_failure().expect(3).mount(&server).await; + mock.verify_and_reset().await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().error500().expect(3).mount().await; q.send(RoomMessageEventContent::text_plain("1").into()).await.unwrap(); @@ -622,9 +543,9 @@ async fn test_error_then_globally_reenabling() { assert!(watch.is_empty()); - server.reset().await; - mock_encryption_state(&server, false).await; - mock_send_event(event_id!("$42")).expect(1).mount(&server).await; + mock.verify_and_reset().await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$42")).expect(1).mount().await; // Re-enabling the global queue will cause the event to be sent. client.send_queue().set_enabled(true).await; @@ -640,21 +561,13 @@ async fn test_error_then_globally_reenabling() { #[async_test] async fn test_reenabling_queue() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); + let room = mock.sync_joined_room(room_id).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - + let client = mock.client(); let errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -694,25 +607,11 @@ async fn test_reenabling_queue() { assert!(watch.is_empty()); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - let num_request = std::sync::Mutex::new(1); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(move |_req: &Request| { - let mut num_request = num_request.lock().unwrap(); - - let event_id = format!("${}", *num_request); - *num_request += 1; - - ResponseTemplate::new(200).set_body_json(json!({ - "event_id": event_id, - })) - }) - .expect(3) - .mount(&server) - .await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + mock.mock_room_send().ok(event_id!("$2")).mock_once().mount().await; + mock.mock_room_send().ok(event_id!("$3")).mock_once().mount().await; // But when reenabling the queue globally, client.send_queue().set_enabled(true).await; @@ -733,23 +632,16 @@ async fn test_reenabling_queue() { #[async_test] async fn test_disjoint_enabled_status() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id1 = room_id!("!a:b.c"); let room_id2 = room_id!("!b:b.c"); - let room1 = mock_sync_with_new_room( - |builder| { - builder - .add_joined_room(JoinedRoomBuilder::new(room_id1)) - .add_joined_room(JoinedRoomBuilder::new(room_id2)); - }, - &client, - &server, - room_id1, - ) - .await; - let room2 = client.get_room(room_id2).unwrap(); + + let room1 = mock.sync_joined_room(room_id1).await; + let room2 = mock.sync_joined_room(room_id2).await; + + let client = mock.client(); // When I start with a disabled send queue, client.send_queue().set_enabled(false).await; @@ -778,20 +670,11 @@ async fn test_disjoint_enabled_status() { #[async_test] async fn test_cancellation() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -805,12 +688,10 @@ async fn test_cancellation() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; let num_request = std::sync::Mutex::new(1); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -832,11 +713,11 @@ async fn test_cancellation() { })) }) .expect(2) - .mount(&server) + .mount() .await; // The redact of txn1 will happen because we asked for it previously. - mock_redaction(event_id!("$1")).expect(1).mount(&server).await; + mock.mock_room_redact().ok(event_id!("$1")).mount().await; let handle1 = q.send(RoomMessageEventContent::text_plain("msg1").into()).await.unwrap(); let handle2 = q.send(RoomMessageEventContent::text_plain("msg2").into()).await.unwrap(); @@ -905,20 +786,11 @@ async fn test_edit() { // to edit a local echo, since if the cancellation test passes, all ways // would work here too similarly. - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -932,12 +804,10 @@ async fn test_edit() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; let num_request = std::sync::Mutex::new(1); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -959,27 +829,22 @@ async fn test_edit() { })) }) .expect(3) - .mount(&server) + .mount() .await; // The /event endpoint is used to retrieve the original event, during creation // of the edit event. - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/")) - .and(header("authorization", "Bearer 1234")) - .respond_with( - ResponseTemplate::new(200).set_body_json( - EventFactory::new() - .text_msg("msg1") - .sender(client.user_id().unwrap()) - .room(room_id) - .into_raw_timeline() - .json(), - ), - ) + let client = mock.client(); + mock.mock_room_event() + .room(room_id) + .ok(EventFactory::new() + .text_msg("msg1") + .sender(client.user_id().unwrap()) + .room(room_id) + .into_timeline()) .expect(1) - .named("get_event") - .mount(&server) + .named("room_event") + .mount() .await; let handle1 = q.send(RoomMessageEventContent::text_plain("msg1").into()).await.unwrap(); @@ -1025,20 +890,11 @@ async fn test_edit() { #[async_test] async fn test_edit_with_poll_start() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1052,12 +908,10 @@ async fn test_edit_with_poll_start() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; let num_request = std::sync::Mutex::new(1); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -1080,27 +934,21 @@ async fn test_edit_with_poll_start() { }) .named("send_event") .expect(2) - .mount(&server) + .mount() .await; // The /event endpoint is used to retrieve the original event, during creation // of the edit event. - Mock::given(method("GET")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/event/")) - .and(header("authorization", "Bearer 1234")) - .respond_with( - ResponseTemplate::new(200).set_body_json( - EventFactory::new() - .poll_start("poll_start", "question", vec!["Answer A"]) - .sender(client.user_id().unwrap()) - .room(room_id) - .into_raw_timeline() - .json(), - ), - ) + let client = mock.client(); + mock.mock_room_event() + .ok(EventFactory::new() + .poll_start("poll_start", "question", vec!["Answer A"]) + .sender(client.user_id().unwrap()) + .room(room_id) + .into_timeline()) .expect(1) .named("get_event") - .mount(&server) + .mount() .await; let poll_answers: UnstablePollAnswers = @@ -1170,20 +1018,11 @@ async fn test_edit_with_poll_start() { #[async_test] async fn test_edit_while_being_sent_and_fails() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1197,11 +1036,9 @@ async fn test_edit_while_being_sent_and_fails() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -1216,7 +1053,7 @@ async fn test_edit_while_being_sent_and_fails() { ResponseTemplate::new(500) }) .expect(3) // reattempts, because of short_retry() - .mount(&server) + .mount() .await; let handle = q.send(RoomMessageEventContent::text_plain("yo").into()).await.unwrap(); @@ -1261,20 +1098,11 @@ async fn test_edit_while_being_sent_and_fails() { #[async_test] async fn test_edit_wakes_the_sending_task() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1283,18 +1111,9 @@ async fn test_edit_wakes_the_sending_task() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - let send_mock_scope = Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(413).set_body_json(json!({ - // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response - "errcode": "M_TOO_LARGE", - }))) - .expect(1) - .mount_as_scoped(&server) - .await; + let send_mock_scope = mock.mock_room_send().error_too_large().expect(1).mount_as_scoped().await; let handle = q.send(RoomMessageEventContent::text_plain("welcome to my ted talk").into()).await.unwrap(); @@ -1311,7 +1130,7 @@ async fn test_edit_wakes_the_sending_task() { // Now edit the event's content (imagine we make it "shorter"). drop(send_mock_scope); - mock_send_event(event_id!("$1")).mount(&server).await; + mock.mock_room_send().ok(event_id!("$1")).mount().await; let edited = handle .edit(RoomMessageEventContent::text_plain("here's the summary of my ted talk").into()) @@ -1328,21 +1147,13 @@ async fn test_edit_wakes_the_sending_task() { #[async_test] async fn test_abort_after_disable() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); + let room = mock.sync_joined_room(room_id).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1359,12 +1170,12 @@ async fn test_abort_after_disable() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; + mock.verify_and_reset().await; - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; // Respond to /send with a transient 500 error. - mock_send_transient_failure().expect(3).mount(&server).await; + mock.mock_room_send().error500().expect(3).mount().await; // One message is queued. let handle = q.send(RoomMessageEventContent::text_plain("hey there").into()).await.unwrap(); @@ -1394,22 +1205,14 @@ async fn test_abort_after_disable() { #[async_test] async fn test_abort_or_edit_after_send() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; // Start with an enabled sending queue. + let client = mock.client(); client.send_queue().set_enabled(true).await; let q = room.send_queue(); @@ -1418,9 +1221,9 @@ async fn test_abort_or_edit_after_send() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; - mock_encryption_state(&server, false).await; - mock_send_event(event_id!("$1")).mount(&server).await; + mock.verify_and_reset().await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$1")).mount().await; let handle = q.send(RoomMessageEventContent::text_plain("hey there").into()).await.unwrap(); @@ -1445,20 +1248,11 @@ async fn test_abort_or_edit_after_send() { #[async_test] async fn test_abort_while_being_sent_and_fails() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1472,11 +1266,9 @@ async fn test_abort_while_being_sent_and_fails() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -1491,7 +1283,7 @@ async fn test_abort_while_being_sent_and_fails() { ResponseTemplate::new(500) }) .expect(3) // reattempts, because of short_retry() - .mount(&server) + .mount() .await; let handle = q.send(RoomMessageEventContent::text_plain("yo").into()).await.unwrap(); @@ -1524,21 +1316,13 @@ async fn test_abort_while_being_sent_and_fails() { #[async_test] async fn test_unrecoverable_errors() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); + let room = mock.sync_joined_room(room_id).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1555,33 +1339,14 @@ async fn test_unrecoverable_errors() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; + mock.verify_and_reset().await; - mock_encryption_state(&server, false).await; - - let respond_with_unrecoverable = AtomicBool::new(true); + mock.mock_room_state_encryption().plain().mount().await; // Respond to the first /send with an unrecoverable error. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(move |_req: &Request| { - // The first message gets M_TOO_LARGE, subsequent messages will encounter a - // great success. - if respond_with_unrecoverable.swap(false, Ordering::SeqCst) { - ResponseTemplate::new(413).set_body_json(json!({ - // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response - "errcode": "M_TOO_LARGE", - })) - } else { - ResponseTemplate::new(200).set_body_json(json!({ - "event_id": "$42", - })) - } - }) - .expect(2) - .mount(&server) - .await; + mock.mock_room_send().error_too_large().mock_once().mount().await; + // Respond to the second /send with an OK response. + mock.mock_room_send().ok(event_id!("$42")).mock_once().mount().await; // Queue two messages. q.send(RoomMessageEventContent::text_plain("i'm too big for ya").into()).await.unwrap(); @@ -1613,21 +1378,13 @@ async fn test_unrecoverable_errors() { #[async_test] async fn test_unwedge_unrecoverable_errors() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); + let room = mock.sync_joined_room(room_id).await; - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; - + let client = mock.client(); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1644,33 +1401,14 @@ async fn test_unwedge_unrecoverable_errors() { assert!(local_echoes.is_empty()); assert!(watch.is_empty()); - server.reset().await; + mock.verify_and_reset().await; - mock_encryption_state(&server, false).await; - - let respond_with_unrecoverable = AtomicBool::new(true); + mock.mock_room_state_encryption().plain().mount().await; // Respond to the first /send with an unrecoverable error. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(move |_req: &Request| { - // The first message gets M_TOO_LARGE, subsequent messages will encounter a - // great success. - if respond_with_unrecoverable.swap(false, Ordering::SeqCst) { - ResponseTemplate::new(413).set_body_json(json!({ - // From https://spec.matrix.org/v1.10/client-server-api/#standard-error-response - "errcode": "M_TOO_LARGE", - })) - } else { - ResponseTemplate::new(200).set_body_json(json!({ - "event_id": "$42", - })) - } - }) - .expect(2) - .mount(&server) - .await; + mock.mock_room_send().error_too_large().mock_once().mount().await; + // Respond to the second /send with an OK response. + mock.mock_room_send().ok(event_id!("$42")).mock_once().mount().await; // Queue the unrecoverable message. q.send(RoomMessageEventContent::text_plain("i'm too big for ya").into()).await.unwrap(); @@ -1709,25 +1447,16 @@ async fn test_no_network_access_error_is_recoverable() { // server in a static. Using the line below will create a "bare" server, // which is effectively dropped upon `drop()`. let server = wiremock::MockServer::builder().start().await; - let client = logged_in_client(Some(server.uri().to_string())).await; + let mock = MatrixMockServer::from_parts(server, client.clone()); // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; // Dropping the server: any subsequent attempt to connect mimics an unreachable // server, which might be caused by missing network. - drop(server); + drop(mock); let mut errors = client.send_queue().subscribe_errors(); assert!(errors.is_empty()); @@ -1783,20 +1512,11 @@ async fn test_reloading_rooms_with_unsent_events() { .unwrap(); set_client_session(&client).await; - // Mark two rooms as joined. - let room = mock_sync_with_new_room( - |builder| { - builder - .add_joined_room(JoinedRoomBuilder::new(room_id)) - .add_joined_room(JoinedRoomBuilder::new(room_id2)); - }, - &client, - &server, - room_id, - ) - .await; + let mock = MatrixMockServer::from_parts(server, client.clone()); - let room2 = client.get_room(room_id2).unwrap(); + // Mark two rooms as joined. + let room = mock.sync_joined_room(room_id).await; + let room2 = mock.sync_joined_room(room_id2).await; // Globally disable the send queue. let q = client.send_queue(); @@ -1819,7 +1539,7 @@ async fn test_reloading_rooms_with_unsent_events() { sleep(Duration::from_millis(300)).await; assert!(watch.is_empty()); - server.reset().await; + mock.verify_and_reset().await; { // Kill the client, let it close background tasks. @@ -1832,26 +1552,13 @@ async fn test_reloading_rooms_with_unsent_events() { // Create a new client with the same memory backend. As the send queues are // enabled by default, it will respawn tasks for sending events to those two // rooms in the background. - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - let event_id = StdMutex::new(0); - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) - .respond_with(move |_req: &Request| { - let mut event_id_guard = event_id.lock().unwrap(); - let event_id = *event_id_guard; - *event_id_guard += 1; - ResponseTemplate::new(200).set_body_json(json!({ - "event_id": event_id - })) - }) - .expect(2) - .mount(&server) - .await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; + mock.mock_room_send().ok(event_id!("$2")).mock_once().mount().await; let client = Client::builder() - .homeserver_url(server.uri()) + .homeserver_url(mock.server().uri()) .server_versions([MatrixVersion::V1_0]) .store_config(StoreConfig::new().state_store(store)) .request_config(RequestConfig::new().disable_retry()) @@ -1866,25 +1573,16 @@ async fn test_reloading_rooms_with_unsent_events() { sleep(Duration::from_secs(1)).await; // The real assertion is on the expect(2) on the above Mock. - server.verify().await; + mock.verify_and_reset().await; } #[async_test] async fn test_reactions() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -1897,11 +1595,9 @@ async fn test_reactions() { let mock_lock = lock.clone(); - mock_encryption_state(&server, false).await; + mock.mock_room_state_encryption().plain().mount().await; - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) - .and(header("authorization", "Bearer 1234")) + mock.mock_room_send() .respond_with(move |_req: &Request| { // Wait for the signal from the main thread that we can process this query. let mock_lock = mock_lock.clone(); @@ -1921,18 +1617,12 @@ async fn test_reactions() { })) }) .expect(3) - .mount(&server) + .mount() .await; // Sending of the second emoji has started; abort it, it will result in a redact // request. - Mock::given(method("PUT")) - .and(path_regex(r"^/_matrix/client/r0/rooms/.*/redact/.*?/.*?")) - .and(header("authorization", "Bearer 1234")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({"event_id": "$3"}))) - .expect(1) - .mount(&server) - .await; + mock.mock_room_redact().ok(event_id!("$3")).expect(1).mount().await; // Send a message. let msg_handle = @@ -2010,20 +1700,12 @@ async fn test_reactions() { #[async_test] async fn test_media_uploads() { - let (client, server) = logged_in_client_with_server().await; + let mock = MatrixMockServer::new().await; // Mark the room as joined. let room_id = room_id!("!a:b.c"); - let room = mock_sync_with_new_room( - |builder| { - builder.add_joined_room(JoinedRoomBuilder::new(room_id)); - }, - &client, - &server, - room_id, - ) - .await; + let room = mock.sync_joined_room(room_id).await; let q = room.send_queue(); @@ -2063,12 +1745,13 @@ async fn test_media_uploads() { // ---------------------- // Prepare endpoints. - mock_encryption_state(&server, false).await; - mock_send_event(event_id!("$1")).expect(1).mount(&server).await; + mock.mock_room_state_encryption().plain().mount().await; + mock.mock_room_send().ok(event_id!("$1")).mock_once().mount().await; let allow_upload_lock = Arc::new(Mutex::new(())); let block_upload = allow_upload_lock.lock().await; + let server = mock.server(); mock_jpeg_upload(mxc_uri!("mxc://sdk.rs/thumbnail"), allow_upload_lock.clone()) .up_to_n_times(1) .expect(1) @@ -2119,6 +1802,7 @@ async fn test_media_uploads() { assert!(mxc.to_string().starts_with("mxc://send-queue.localhost/"), "{mxc}"); // The media is immediately available from the cache. + let client = mock.client(); let file_media = client .media() .get_media_content(