Skip to main content

matrix_sdk/test_utils/
mod.rs

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