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, SessionMeta};
7use ruma::{
8    api::MatrixVersion,
9    device_id,
10    events::{room::message::MessageType, AnySyncMessageLikeEvent, AnySyncTimelineEvent},
11    user_id,
12};
13use url::Url;
14
15pub mod client;
16#[cfg(not(target_arch = "wasm32"))]
17pub mod mocks;
18
19use crate::{
20    authentication::matrix::{MatrixSession, MatrixSessionTokens},
21    config::RequestConfig,
22    Client, ClientBuilder,
23};
24
25/// Checks that an event is a message-like text event with the given text.
26#[track_caller]
27pub fn assert_event_matches_msg<E: Clone + Into<TimelineEvent>>(event: &E, expected: &str) {
28    let event: TimelineEvent = event.clone().into();
29    let event = event.raw().deserialize().unwrap();
30    assert_let!(
31        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) = event
32    );
33    let message = message.as_original().unwrap();
34    assert_let!(MessageType::Text(text) = &message.content.msgtype);
35    assert_eq!(text.body, expected);
36}
37
38/// A [`ClientBuilder`] fit for testing, using the given `homeserver_url` (or
39/// localhost:1234).
40pub fn test_client_builder(homeserver_url: Option<String>) -> ClientBuilder {
41    let homeserver = homeserver_url
42        .map(|url| Url::try_from(url.as_str()).unwrap())
43        .unwrap_or_else(|| Url::try_from("http://localhost:1234").unwrap());
44    Client::builder().homeserver_url(homeserver).server_versions([MatrixVersion::V1_0])
45}
46
47/// A [`Client`] using the given `homeserver_url` (or localhost:1234), that will
48/// never retry any failed requests.
49pub async fn no_retry_test_client(homeserver_url: Option<String>) -> Client {
50    test_client_builder(homeserver_url)
51        .request_config(RequestConfig::new().disable_retry())
52        .build()
53        .await
54        .unwrap()
55}
56
57/// Restore the common (Matrix-auth) user session for a client.
58pub async fn set_client_session(client: &Client) {
59    client
60        .matrix_auth()
61        .restore_session(MatrixSession {
62            meta: SessionMeta {
63                user_id: user_id!("@example:localhost").to_owned(),
64                device_id: device_id!("DEVICEID").to_owned(),
65            },
66            tokens: MatrixSessionTokens { access_token: "1234".to_owned(), refresh_token: None },
67        })
68        .await
69        .unwrap();
70}
71
72/// A [`Client`] using the given `homeserver_url` (or localhost:1234), that will
73/// never retry any failed requests, and already logged in with an hardcoded
74/// Matrix authentication session (the user id and device id are hardcoded too).
75pub async fn logged_in_client(homeserver_url: Option<String>) -> Client {
76    let client = no_retry_test_client(homeserver_url).await;
77    set_client_session(&client).await;
78    client
79}
80
81/// Like [`test_client_builder`], but with a mocked server too.
82#[cfg(not(target_arch = "wasm32"))]
83pub async fn test_client_builder_with_server() -> (ClientBuilder, wiremock::MockServer) {
84    let server = wiremock::MockServer::start().await;
85    let builder = test_client_builder(Some(server.uri()));
86    (builder, server)
87}
88
89/// Like [`no_retry_test_client`], but with a mocked server too.
90#[cfg(not(target_arch = "wasm32"))]
91pub async fn no_retry_test_client_with_server() -> (Client, wiremock::MockServer) {
92    let server = wiremock::MockServer::start().await;
93    let client = no_retry_test_client(Some(server.uri().to_string())).await;
94    (client, server)
95}
96
97/// Like [`logged_in_client`], but with a mocked server too.
98#[cfg(not(target_arch = "wasm32"))]
99pub async fn logged_in_client_with_server() -> (Client, wiremock::MockServer) {
100    let server = wiremock::MockServer::start().await;
101    let client = logged_in_client(Some(server.uri().to_string())).await;
102    (client, server)
103}
104
105/// Asserts that the next item in a `Stream` is received within a given timeout.
106///
107/// This macro waits for the next item from an asynchronous `Stream` or, if no
108/// item is received within the specified timeout, the macro panics.
109///
110/// # Parameters
111///
112/// - `$stream`: The `Stream` or `Subscriber` to poll for the next item.
113/// - `$timeout_ms` (optional): The timeout in milliseconds to wait for the next
114///   item. Defaults to 500ms if not provided.
115///
116/// # Example
117///
118/// ```rust
119/// use futures_util::{stream, StreamExt};
120/// use matrix_sdk::assert_next_with_timeout;
121///
122/// # async {
123/// let mut stream = stream::iter(vec![1, 2, 3]);
124/// let next_item = assert_next_with_timeout!(stream, 1000); // Waits up to 1000ms
125/// assert_eq!(next_item, 1);
126///
127/// // The timeout can be omitted, in which case it defaults to 500 ms.
128/// let next_item = assert_next_with_timeout!(stream); // Waits up to 500ms
129/// assert_eq!(next_item, 2);
130/// # };
131/// ```
132#[macro_export]
133macro_rules! assert_next_with_timeout {
134    ($stream:expr) => {
135        $crate::assert_next_with_timeout!($stream, 500)
136    };
137    ($stream:expr, $timeout_ms:expr) => {{
138        // Needed for subscribers, as they won't use the StreamExt features
139        #[allow(unused_imports)]
140        use futures_util::StreamExt as _;
141        tokio::time::timeout(std::time::Duration::from_millis($timeout_ms), $stream.next())
142            .await
143            .expect("Next event timed out")
144            .expect("No next event received")
145    }};
146}
147
148/// Asserts the next item in a `Receiver` can be loaded in the given timeout in
149/// milliseconds.
150#[macro_export]
151macro_rules! assert_recv_with_timeout {
152    ($receiver:expr, $timeout_ms:expr) => {{
153        tokio::time::timeout(std::time::Duration::from_millis($timeout_ms), $receiver.recv())
154            .await
155            .expect("Next event timed out")
156            .expect("No next event received")
157    }};
158}
159
160/// Assert the next item in a `Stream` or `Subscriber` matches the provided
161/// pattern in the given timeout in milliseconds.
162///
163/// If no timeout is provided, a default `100ms` value will be used.
164#[macro_export]
165macro_rules! assert_next_matches_with_timeout {
166    ($stream:expr, $pat:pat) => {
167        $crate::assert_next_matches_with_timeout!($stream, $pat => {})
168    };
169    ($stream:expr, $pat:pat => $arm:expr) => {
170        $crate::assert_next_matches_with_timeout!($stream, 100, $pat => $arm)
171    };
172    ($stream:expr, $timeout_ms:expr, $pat:pat => $arm:expr) => {
173        match $crate::assert_next_with_timeout!(&mut $stream, $timeout_ms) {
174            $pat => $arm,
175            val => {
176                ::core::panic!(
177                    "assertion failed: `{:?}` does not match `{}`",
178                    val, ::core::stringify!($pat)
179                );
180            }
181        }
182    };
183}
184
185/// Asserts that the next item from an asynchronous stream is equal to the
186/// expected value, with an optional timeout and custom error message.
187///
188/// # Arguments
189///
190/// * `$stream` - The asynchronous stream to retrieve the next item from.
191/// * `$expected` - The expected value to assert against.
192/// * `$timeout ms` (optional) - A timeout in milliseconds (e.g., `200ms`).
193///   Defaults to `100ms`.
194/// * `$msg` (optional) - A formatted message string for assertion failure.
195///
196/// # Examples
197///
198/// ```
199/// # async {
200/// # use matrix_sdk::assert_next_eq_with_timeout;
201/// # use tokio_stream::StreamExt;
202///
203/// let mut stream = tokio_stream::iter(vec![42]);
204/// assert_next_eq_with_timeout!(stream, 42);
205///
206/// let mut stream = tokio_stream::iter(vec![42]);
207/// assert_next_eq_with_timeout!(stream, 42, 200 ms);
208///
209/// let mut stream = tokio_stream::iter(vec![42]);
210/// assert_next_eq_with_timeout!(
211///     stream,
212///     42,
213///     "Expected 42 but got something else"
214/// );
215///
216/// let mut stream = tokio_stream::iter(vec![42]);
217/// assert_next_eq_with_timeout!(stream, 42, 200 ms, "Expected 42 within 200ms");
218/// # };
219/// ```
220#[macro_export]
221macro_rules! assert_next_eq_with_timeout {
222    ($stream:expr, $expected:expr) => {
223        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis(100));
224    };
225    ($stream:expr, $expected:expr, $timeout:literal ms) => {
226        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis($timeout));
227    };
228    ($stream:expr, $expected:expr, $timeout:literal ms, $($msg:tt)*) => {
229        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis($timeout), $($msg)*);
230    };
231    ($stream:expr, $expected:expr, $($msg:tt)*) => {
232        $crate::assert_next_eq_with_timeout_impl!($stream, $expected, std::time::Duration::from_millis(100), $($msg)*);
233    };
234}
235
236#[doc(hidden)]
237#[macro_export]
238macro_rules! assert_next_eq_with_timeout_impl {
239    ($stream:expr, $expected:expr, $timeout:expr) => {
240        let next_value = tokio::time::timeout(
241            $timeout,
242            $stream.next()
243        )
244        .await
245        .expect("We should be able to get the next value out of the stream by now")
246        .expect("The stream should have given us a new value instead of None");
247
248        assert_eq!(next_value, $expected);
249    };
250    ($stream:expr, $expected:expr, $timeout:expr, $($msg:tt)*) => {{
251        let next_value = tokio::time::timeout(
252            $timeout,
253            futures_util::StreamExt::next(&mut $stream)
254        )
255        .await
256        .expect("We should be able to get the next value out of the stream by now")
257        .expect("The stream should have given us a new value instead of None");
258
259        assert_eq!(next_value, $expected, $($msg)*);
260    }};
261}
262
263/// Like `assert_let`, but with the possibility to add an optional timeout.
264///
265/// If not provided, a timeout value of 100 milliseconds is used.
266#[macro_export]
267macro_rules! assert_let_timeout {
268    ($timeout:expr, $pat:pat = $future:expr) => {
269        assert_matches2::assert_let!(Ok($pat) = tokio::time::timeout($timeout, $future).await);
270    };
271
272    ($pat:pat = $future:expr) => {
273        assert_let_timeout!(std::time::Duration::from_millis(100), $pat = $future);
274    };
275}