matrix_sdk/test_utils/
mod.rs

1//! Testing utilities - DO NOT USE IN PRODUCTION.
2
3#![allow(dead_code)]
4
5use assert_matches2::assert_let;
6use matrix_sdk_base::{deserialized_responses::TimelineEvent, store::RoomLoadSettings};
7use ruma::{
8    api::MatrixVersion,
9    events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, room::message::MessageType},
10};
11use url::Url;
12
13pub mod client;
14#[cfg(not(target_family = "wasm"))]
15pub mod mocks;
16
17use self::client::mock_matrix_session;
18use crate::{Client, ClientBuilder, config::RequestConfig};
19
20/// Checks that an event is a message-like text event with the given text.
21#[track_caller]
22pub fn assert_event_matches_msg<E: Clone + Into<TimelineEvent>>(event: &E, expected: &str) {
23    let event: TimelineEvent = event.clone().into();
24    let event = event.raw().deserialize().unwrap();
25    assert_let!(
26        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) = event
27    );
28    let message = message.as_original().unwrap();
29    assert_let!(MessageType::Text(text) = &message.content.msgtype);
30    assert_eq!(text.body, expected);
31}
32
33/// A [`ClientBuilder`] fit for testing, using the given `homeserver_url` (or
34/// localhost:1234).
35pub fn test_client_builder(homeserver_url: Option<String>) -> ClientBuilder {
36    let homeserver = homeserver_url
37        .map(|url| Url::try_from(url.as_str()).unwrap())
38        .unwrap_or_else(|| Url::try_from("http://localhost:1234").unwrap());
39    Client::builder().homeserver_url(homeserver).server_versions([MatrixVersion::V1_0])
40}
41
42/// A [`Client`] using the given `homeserver_url` (or localhost:1234), that will
43/// never retry any failed requests.
44pub async fn no_retry_test_client(homeserver_url: Option<String>) -> Client {
45    test_client_builder(homeserver_url)
46        .request_config(RequestConfig::new().disable_retry())
47        .build()
48        .await
49        .unwrap()
50}
51
52/// Restore the common (Matrix-auth) user session for a client.
53pub async fn set_client_session(client: &Client) {
54    client
55        .matrix_auth()
56        .restore_session(mock_matrix_session(), RoomLoadSettings::default())
57        .await
58        .unwrap();
59}
60
61/// A [`Client`] using the given `homeserver_url` (or localhost:1234), that will
62/// never retry any failed requests, and already logged in with an hardcoded
63/// Matrix authentication session (the user id and device id are hardcoded too).
64pub async fn logged_in_client(homeserver_url: Option<String>) -> Client {
65    let client = no_retry_test_client(homeserver_url).await;
66    set_client_session(&client).await;
67    client
68}
69
70/// Like [`test_client_builder`], but with a mocked server too.
71#[cfg(not(target_family = "wasm"))]
72pub async fn test_client_builder_with_server() -> (ClientBuilder, wiremock::MockServer) {
73    let server = wiremock::MockServer::start().await;
74    let builder = test_client_builder(Some(server.uri()));
75    (builder, server)
76}
77
78/// Like [`no_retry_test_client`], but with a mocked server too.
79#[cfg(not(target_family = "wasm"))]
80pub async fn no_retry_test_client_with_server() -> (Client, wiremock::MockServer) {
81    let server = wiremock::MockServer::start().await;
82    let client = no_retry_test_client(Some(server.uri().to_string())).await;
83    (client, server)
84}
85
86/// Like [`logged_in_client`], but with a mocked server too.
87#[cfg(not(target_family = "wasm"))]
88pub async fn logged_in_client_with_server() -> (Client, wiremock::MockServer) {
89    let server = wiremock::MockServer::start().await;
90    let client = logged_in_client(Some(server.uri().to_string())).await;
91    (client, server)
92}
93
94/// Asserts that the next item in a `Stream` is received within a given timeout.
95///
96/// This macro waits for the next item from an asynchronous `Stream` or, if no
97/// item is received within the specified timeout, the macro panics.
98///
99/// # Parameters
100///
101/// - `$stream`: The `Stream` or `Subscriber` to poll for the next item.
102/// - `$timeout_ms` (optional): The timeout in milliseconds to wait for the next
103///   item. Defaults to 500ms if not provided.
104///
105/// # Example
106///
107/// ```rust
108/// use futures_util::{stream, StreamExt};
109/// use matrix_sdk::assert_next_with_timeout;
110///
111/// # async {
112/// let mut stream = stream::iter(vec![1, 2, 3]);
113/// let next_item = assert_next_with_timeout!(stream, 1000); // Waits up to 1000ms
114/// assert_eq!(next_item, 1);
115///
116/// // The timeout can be omitted, in which case it defaults to 500 ms.
117/// let next_item = assert_next_with_timeout!(stream); // Waits up to 500ms
118/// assert_eq!(next_item, 2);
119/// # };
120/// ```
121#[macro_export]
122macro_rules! assert_next_with_timeout {
123    ($stream:expr) => {
124        $crate::assert_next_with_timeout!($stream, 500)
125    };
126    ($stream:expr, $timeout_ms:expr) => {{
127        // Needed for subscribers, as they won't use the StreamExt features
128        #[allow(unused_imports)]
129        use futures_util::StreamExt as _;
130        tokio::time::timeout(std::time::Duration::from_millis($timeout_ms), $stream.next())
131            .await
132            .expect("Next event timed out")
133            .expect("No next event received")
134    }};
135}
136
137/// Asserts the next item in a `Receiver` can be loaded in the given timeout in
138/// milliseconds.
139#[macro_export]
140macro_rules! assert_recv_with_timeout {
141    ($receiver:expr, $timeout_ms:expr) => {{
142        tokio::time::timeout(std::time::Duration::from_millis($timeout_ms), $receiver.recv())
143            .await
144            .expect("Next event timed out")
145            .expect("No next event received")
146    }};
147}
148
149/// Assert the next item in a `Stream` or `Subscriber` matches the provided
150/// pattern in the given timeout in milliseconds.
151///
152/// If no timeout is provided, a default `100ms` value will be used.
153#[macro_export]
154macro_rules! assert_next_matches_with_timeout {
155    ($stream:expr, $pat:pat) => {
156        $crate::assert_next_matches_with_timeout!($stream, $pat => {})
157    };
158    ($stream:expr, $pat:pat => $arm:expr) => {
159        $crate::assert_next_matches_with_timeout!($stream, 100, $pat => $arm)
160    };
161    ($stream:expr, $timeout_ms:expr, $pat:pat) => {
162        $crate::assert_next_matches_with_timeout!($stream, $timeout_ms, $pat => {})
163    };
164    ($stream:expr, $timeout_ms:expr, $pat:pat => $arm:expr) => {
165        match $crate::assert_next_with_timeout!(&mut $stream, $timeout_ms) {
166            $pat => $arm,
167            val => {
168                ::core::panic!(
169                    "assertion failed: `{:?}` does not match `{}`",
170                    val, ::core::stringify!($pat)
171                );
172            }
173        }
174    };
175}
176
177/// Asserts that the next item from an asynchronous stream is equal to the
178/// expected value, with an optional timeout and custom error message.
179///
180/// # Arguments
181///
182/// * `$stream` - The asynchronous stream to retrieve the next item from.
183/// * `$expected` - The expected value to assert against.
184/// * `$timeout ms` (optional) - A timeout in milliseconds (e.g., `200ms`).
185///   Defaults to `100ms`.
186/// * `$msg` (optional) - A formatted message string for assertion failure.
187///
188/// # Examples
189///
190/// ```
191/// # async {
192/// # use matrix_sdk::assert_next_eq_with_timeout;
193/// # use tokio_stream::StreamExt;
194///
195/// let mut stream = tokio_stream::iter(vec![42]);
196/// assert_next_eq_with_timeout!(stream, 42);
197///
198/// let mut stream = tokio_stream::iter(vec![42]);
199/// assert_next_eq_with_timeout!(stream, 42, 200 ms);
200///
201/// let mut stream = tokio_stream::iter(vec![42]);
202/// assert_next_eq_with_timeout!(
203///     stream,
204///     42,
205///     "Expected 42 but got something else"
206/// );
207///
208/// let mut stream = tokio_stream::iter(vec![42]);
209/// assert_next_eq_with_timeout!(stream, 42, 200 ms, "Expected 42 within 200ms");
210/// # };
211/// ```
212#[macro_export]
213macro_rules! assert_next_eq_with_timeout {
214    ($stream:expr, $expected:expr) => {
215        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis(100));
216    };
217    ($stream:expr, $expected:expr, $timeout:literal ms) => {
218        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis($timeout));
219    };
220    ($stream:expr, $expected:expr, $timeout:literal ms, $($msg:tt)*) => {
221        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis($timeout), $($msg)*);
222    };
223    ($stream:expr, $expected:expr, $($msg:tt)*) => {
224        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis(100), $($msg)*);
225    };
226}
227
228/// Given a [`TimelineEvent`] assert that the event was decrypted and that the
229/// message matches the expected value.
230///
231/// # Examples
232///
233/// ```no_run
234/// # async {
235/// # let client: matrix_sdk::Client = unreachable!();
236/// # let room_id: ruma::OwnedRoomId = unreachable!();
237/// # let event_id: ruma::OwnedEventId = unreachable!();
238/// use matrix_sdk::assert_decrypted_message_eq;
239///
240/// let room =
241///     client.get_room(&room_id).expect("Bob should have received the invite");
242///
243/// let event = room.event(&event_id, None).await?;
244///
245/// assert_decrypted_message_eq!(
246///     event,
247///     "It's a secret to everybody!",
248///     "The decrypted event should match the expected secret message"
249/// );
250/// # anyhow::Ok(()) };
251/// ```
252#[macro_export]
253macro_rules! assert_decrypted_message_eq {
254    ($event:expr, $expected:expr, $($msg:tt)*) => {{
255        assert_matches2::assert_let!($crate::deserialized_responses::TimelineEventKind::Decrypted(decrypted_event) = $event.kind);
256
257        let deserialized_event = decrypted_event
258            .event
259            .deserialize()
260            .expect("We should be able to deserialize the decrypted event");
261
262        assert_matches2::assert_let!(
263            $crate::ruma::events::AnyTimelineEvent::MessageLike(deserialized_event) = deserialized_event
264        );
265
266        let content =
267            deserialized_event.original_content().expect("The event should not have been redacted");
268        assert_matches2::assert_let!($crate::ruma::events::AnyMessageLikeEventContent::RoomMessage(content) = content);
269        assert_eq!(content.body(), $expected, $($msg)*);
270    }};
271    ($event:expr, $expected:expr) => {{
272        assert_decrypted_message_eq!($event, $expected, "The decrypted content did not match to the expected value");
273    }};
274}
275
276/// Given a [`TimelineEvent`], assert that the event is a decrypted state
277/// event, and that its content matches the given pattern via a let binding.
278///
279/// If more than one argument is provided, these will be used as an error
280/// message if the content does not match the provided pattern.
281///
282/// # Examples
283///
284/// ```no_run
285/// # async {
286/// # let client: matrix_sdk::Client = unreachable!();
287/// # let room_id: ruma::OwnedRoomId = unreachable!();
288/// # let event_id: ruma::OwnedEventId = unreachable!();
289/// use matrix_sdk::assert_let_decrypted_state_event_content;
290///
291/// let room =
292///     client.get_room(&room_id).expect("Bob should have received the invite");
293///
294/// let event = room.event(&event_id, None).await?;
295///
296/// assert_let_decrypted_state_event_content!(
297///     ruma::events::AnyStateEventContent::RoomTopic(
298///         ruma::events::room::topic::RoomTopicEventContent { topic, .. }
299///     ) = event
300/// );
301/// assert_eq!(topic, "Encrypted topic!");
302/// # anyhow::Ok(()) };
303/// ```
304#[macro_export]
305macro_rules! assert_let_decrypted_state_event_content {
306    ($pat:pat = $event:expr, $($msg:tt)*) => {
307        assert_matches2::assert_let!(
308            $crate::deserialized_responses::TimelineEventKind::Decrypted(decrypted_event) =
309                $event.kind,
310            "Event was not decrypted"
311        );
312
313        let deserialized_event = decrypted_event
314            .event
315            .deserialize_as_unchecked::<$crate::ruma::events::AnyStateEvent>()
316            .expect("We should be able to deserialize the decrypted event");
317
318        let content =
319            deserialized_event.original_content().expect("The event should not have been redacted");
320
321        assert_matches2::assert_let!($pat = content, $($msg)*);
322    };
323    ($pat:pat = $event:expr) => {
324        assert_let_decrypted_state_event_content!(
325            $pat = $event,
326            "The decrypted event did not match the expected value"
327        );
328    };
329}
330
331#[doc(hidden)]
332#[macro_export]
333macro_rules! assert_next_eq_with_timeout_impl {
334    ($stream:expr, $expected:expr, $timeout:expr) => {
335        let next_value = tokio::time::timeout(
336            $timeout,
337            $stream.next()
338        )
339        .await
340        .expect("We should be able to get the next value out of the stream by now")
341        .expect("The stream should have given us a new value instead of None");
342
343        assert_eq!(next_value, $expected);
344    };
345    ($stream:expr, $expected:expr, $timeout:expr, $($msg:tt)*) => {{
346        let next_value = tokio::time::timeout(
347            $timeout,
348            futures_util::StreamExt::next(&mut $stream)
349        )
350        .await
351        .expect("We should be able to get the next value out of the stream by now")
352        .expect("The stream should have given us a new value instead of None");
353
354        assert_eq!(next_value, $expected, $($msg)*);
355    }};
356}
357
358/// Like `assert_let`, but with the possibility to add an optional timeout.
359///
360/// If not provided, a timeout value of 100 milliseconds is used.
361#[macro_export]
362macro_rules! assert_let_timeout {
363    ($timeout:expr, $pat:pat = $future:expr) => {
364        assert_matches2::assert_let!(Ok($pat) = tokio::time::timeout($timeout, $future).await);
365    };
366
367    ($pat:pat = $future:expr) => {
368        assert_let_timeout!(std::time::Duration::from_millis(100), $pat = $future);
369    };
370}