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///
140/// This macro waits for the next item from a `Receiver` or, if no
141/// item is received within the specified timeout, the macro panics.
142///
143/// # Parameters
144///
145/// - `$receiver`: The receiver to poll for the next item.
146/// - `$timeout_ms` (optional): The timeout in milliseconds to wait for the next
147///   item. Defaults to 500ms if not provided.
148///
149/// # Example
150///
151/// ```rust
152/// use matrix_sdk::assert_recv_with_timeout;
153/// use tokio::sync::mpsc;
154///
155/// # async {
156/// let (tx, mut rx) = mpsc::channel(10);
157/// tx.send(1);
158///
159/// let next_item = assert_recv_with_timeout!(rx, 1000); // Waits up to 1000ms
160/// assert_eq!(next_item, 1);
161///
162/// let (tx, mut rx) = mpsc::channel(10);
163/// tx.send(2);
164///
165/// // The timeout can be omitted, in which case it defaults to 500 ms.
166/// let next_item = assert_recv_with_timeout!(rx); // Waits up to 500ms
167/// assert_eq!(next_item, 2);
168/// # };
169/// ```
170#[macro_export]
171macro_rules! assert_recv_with_timeout {
172    ($receiver:expr) => {
173        $crate::assert_recv_with_timeout!($receiver, 500)
174    };
175
176    ($receiver:expr, $timeout_ms:expr) => {{
177        tokio::time::timeout(std::time::Duration::from_millis($timeout_ms), $receiver.recv())
178            .await
179            .expect("Next event timed out")
180            .expect("No next event received")
181    }};
182}
183
184/// Assert the next item in a `Stream` or `Subscriber` matches the provided
185/// pattern in the given timeout in milliseconds.
186///
187/// If no timeout is provided, a default `100ms` value will be used.
188#[macro_export]
189macro_rules! assert_next_matches_with_timeout {
190    ($stream:expr, $pat:pat) => {
191        $crate::assert_next_matches_with_timeout!($stream, $pat => {})
192    };
193    ($stream:expr, $pat:pat => $arm:expr) => {
194        $crate::assert_next_matches_with_timeout!($stream, 100, $pat => $arm)
195    };
196    ($stream:expr, $timeout_ms:expr, $pat:pat) => {
197        $crate::assert_next_matches_with_timeout!($stream, $timeout_ms, $pat => {})
198    };
199    ($stream:expr, $timeout_ms:expr, $pat:pat => $arm:expr) => {
200        match $crate::assert_next_with_timeout!(&mut $stream, $timeout_ms) {
201            $pat => $arm,
202            val => {
203                ::core::panic!(
204                    "assertion failed: `{:?}` does not match `{}`",
205                    val, ::core::stringify!($pat)
206                );
207            }
208        }
209    };
210}
211
212/// Asserts that the next item from an asynchronous stream is equal to the
213/// expected value, with an optional timeout and custom error message.
214///
215/// # Arguments
216///
217/// * `$stream` - The asynchronous stream to retrieve the next item from.
218/// * `$expected` - The expected value to assert against.
219/// * `$timeout ms` (optional) - A timeout in milliseconds (e.g., `200ms`).
220///   Defaults to `100ms`.
221/// * `$msg` (optional) - A formatted message string for assertion failure.
222///
223/// # Examples
224///
225/// ```
226/// # async {
227/// # use matrix_sdk::assert_next_eq_with_timeout;
228/// # use tokio_stream::StreamExt;
229///
230/// let mut stream = tokio_stream::iter(vec![42]);
231/// assert_next_eq_with_timeout!(stream, 42);
232///
233/// let mut stream = tokio_stream::iter(vec![42]);
234/// assert_next_eq_with_timeout!(stream, 42, 200 ms);
235///
236/// let mut stream = tokio_stream::iter(vec![42]);
237/// assert_next_eq_with_timeout!(
238///     stream,
239///     42,
240///     "Expected 42 but got something else"
241/// );
242///
243/// let mut stream = tokio_stream::iter(vec![42]);
244/// assert_next_eq_with_timeout!(stream, 42, 200 ms, "Expected 42 within 200ms");
245/// # };
246/// ```
247#[macro_export]
248macro_rules! assert_next_eq_with_timeout {
249    ($stream:expr, $expected:expr) => {
250        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis(100));
251    };
252    ($stream:expr, $expected:expr, $timeout:literal ms) => {
253        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis($timeout));
254    };
255    ($stream:expr, $expected:expr, $timeout:literal ms, $($msg:tt)*) => {
256        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis($timeout), $($msg)*);
257    };
258    ($stream:expr, $expected:expr, $($msg:tt)*) => {
259        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis(100), $($msg)*);
260    };
261}
262
263/// Given a [`TimelineEvent`] assert that the event was decrypted and that the
264/// message matches the expected value.
265///
266/// # Examples
267///
268/// ```no_run
269/// # async {
270/// # let client: matrix_sdk::Client = unreachable!();
271/// # let room_id: ruma::OwnedRoomId = unreachable!();
272/// # let event_id: ruma::OwnedEventId = unreachable!();
273/// use matrix_sdk::assert_decrypted_message_eq;
274///
275/// let room =
276///     client.get_room(&room_id).expect("Bob should have received the invite");
277///
278/// let event = room.event(&event_id, None).await?;
279///
280/// assert_decrypted_message_eq!(
281///     event,
282///     "It's a secret to everybody!",
283///     "The decrypted event should match the expected secret message"
284/// );
285/// # anyhow::Ok(()) };
286/// ```
287#[macro_export]
288macro_rules! assert_decrypted_message_eq {
289    ($event:expr, $expected:expr, $($msg:tt)*) => {{
290        assert_matches2::assert_let!($crate::deserialized_responses::TimelineEventKind::Decrypted(decrypted_event) = $event.kind);
291
292        let deserialized_event = decrypted_event
293            .event
294            .deserialize()
295            .expect("We should be able to deserialize the decrypted event");
296
297        assert_matches2::assert_let!(
298            $crate::ruma::events::AnyTimelineEvent::MessageLike(deserialized_event) = deserialized_event
299        );
300
301        let content =
302            deserialized_event.original_content().expect("The event should not have been redacted");
303        assert_matches2::assert_let!($crate::ruma::events::AnyMessageLikeEventContent::RoomMessage(content) = content);
304        assert_eq!(content.body(), $expected, $($msg)*);
305    }};
306    ($event:expr, $expected:expr) => {{
307        assert_decrypted_message_eq!($event, $expected, "The decrypted content did not match to the expected value");
308    }};
309}
310
311/// Given a [`TimelineEvent`], assert that the event is a decrypted state
312/// event, and that its content matches the given pattern via a let binding.
313///
314/// If more than one argument is provided, these will be used as an error
315/// message if the content does not match the provided pattern.
316///
317/// # Examples
318///
319/// ```no_run
320/// # async {
321/// # let client: matrix_sdk::Client = unreachable!();
322/// # let room_id: ruma::OwnedRoomId = unreachable!();
323/// # let event_id: ruma::OwnedEventId = unreachable!();
324/// use matrix_sdk::assert_let_decrypted_state_event_content;
325///
326/// let room =
327///     client.get_room(&room_id).expect("Bob should have received the invite");
328///
329/// let event = room.event(&event_id, None).await?;
330///
331/// assert_let_decrypted_state_event_content!(
332///     ruma::events::AnyStateEventContent::RoomTopic(
333///         ruma::events::room::topic::RoomTopicEventContent { topic, .. }
334///     ) = event
335/// );
336/// assert_eq!(topic, "Encrypted topic!");
337/// # anyhow::Ok(()) };
338/// ```
339#[macro_export]
340macro_rules! assert_let_decrypted_state_event_content {
341    ($pat:pat = $event:expr, $($msg:tt)*) => {
342        assert_matches2::assert_let!(
343            $crate::deserialized_responses::TimelineEventKind::Decrypted(decrypted_event) =
344                $event.kind,
345            "Event was not decrypted"
346        );
347
348        let deserialized_event = decrypted_event
349            .event
350            .deserialize_as_unchecked::<$crate::ruma::events::AnyStateEvent>()
351            .expect("We should be able to deserialize the decrypted event");
352
353        let content =
354            deserialized_event.original_content().expect("The event should not have been redacted");
355
356        assert_matches2::assert_let!($pat = content, $($msg)*);
357    };
358    ($pat:pat = $event:expr) => {
359        assert_let_decrypted_state_event_content!(
360            $pat = $event,
361            "The decrypted event did not match the expected value"
362        );
363    };
364}
365
366#[doc(hidden)]
367#[macro_export]
368macro_rules! assert_next_eq_with_timeout_impl {
369    ($stream:expr, $expected:expr, $timeout:expr) => {
370        let next_value = tokio::time::timeout(
371            $timeout,
372            $stream.next()
373        )
374        .await
375        .expect("We should be able to get the next value out of the stream by now")
376        .expect("The stream should have given us a new value instead of None");
377
378        assert_eq!(next_value, $expected);
379    };
380    ($stream:expr, $expected:expr, $timeout:expr, $($msg:tt)*) => {{
381        let next_value = tokio::time::timeout(
382            $timeout,
383            futures_util::StreamExt::next(&mut $stream)
384        )
385        .await
386        .expect("We should be able to get the next value out of the stream by now")
387        .expect("The stream should have given us a new value instead of None");
388
389        assert_eq!(next_value, $expected, $($msg)*);
390    }};
391}
392
393/// Like `assert_let`, but with the possibility to add an optional timeout.
394///
395/// If not provided, a timeout value of 100 milliseconds is used.
396#[macro_export]
397macro_rules! assert_let_timeout {
398    ($timeout:expr, $pat:pat = $future:expr) => {
399        assert_matches2::assert_let!(Ok($pat) = tokio::time::timeout($timeout, $future).await);
400    };
401
402    ($pat:pat = $future:expr) => {
403        assert_let_timeout!(std::time::Duration::from_millis(100), $pat = $future);
404    };
405}