Skip to main content

matrix_sdk/test_utils/
client.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Augmented [`ClientBuilder`] that can set up an already logged-in user.
16
17use matrix_sdk_base::{SessionMeta, store::RoomLoadSettings};
18use ruma::{OwnedDeviceId, OwnedUserId, api::MatrixVersion, owned_device_id, owned_user_id};
19
20use crate::{
21    Client, ClientBuilder, SessionTokens, authentication::matrix::MatrixSession,
22    config::RequestConfig,
23};
24
25/// An augmented [`ClientBuilder`] that also allows for handling session login.
26#[allow(missing_debug_implementations)]
27pub struct MockClientBuilder {
28    builder: ClientBuilder,
29    auth_state: AuthState,
30    server_versions: ServerVersions,
31}
32
33impl MockClientBuilder {
34    /// Create a new [`MockClientBuilder`] connected to the given homeserver,
35    /// using Matrix V1.12, and which will not attempt any network retry (by
36    /// default).
37    ///
38    /// If no homeserver is provided, `http://localhost` is used as a
39    /// homeserver.
40    pub fn new(homeserver: Option<&str>) -> Self {
41        let homeserver = homeserver.unwrap_or("http://localhost");
42
43        let default_builder = Client::builder()
44            .homeserver_url(homeserver)
45            .request_config(RequestConfig::new().disable_retry());
46
47        Self {
48            builder: default_builder,
49            auth_state: AuthState::LoggedInWithMatrixAuth {
50                token: None,
51                user_id: None,
52                device_id: None,
53            },
54            server_versions: ServerVersions::Default,
55        }
56    }
57
58    /// Don't use an initial, cached server versions list in the client.
59    pub fn no_server_versions(mut self) -> Self {
60        self.server_versions = ServerVersions::None;
61        self
62    }
63
64    /// Set the cached server versions in the client.
65    pub fn server_versions(mut self, versions: Vec<MatrixVersion>) -> Self {
66        self.server_versions = ServerVersions::Custom(versions);
67        self
68    }
69
70    /// Doesn't log-in a user.
71    ///
72    /// Authenticated requests will fail if this is called.
73    pub fn unlogged(mut self) -> Self {
74        self.auth_state = AuthState::None;
75        self
76    }
77
78    /// The client is registered with the OAuth 2.0 API.
79    pub fn registered_with_oauth(mut self) -> Self {
80        self.auth_state = AuthState::RegisteredWithOAuth;
81        self
82    }
83
84    /// The user is already logged in with the OAuth 2.0 API.
85    pub fn logged_in_with_oauth(mut self) -> Self {
86        self.auth_state = AuthState::LoggedInWithOAuth;
87        self
88    }
89
90    /// The user is already logged in with the Matrix Auth.
91    pub fn logged_in_with_token(
92        mut self,
93        token: String,
94        user_id: OwnedUserId,
95        device_id: OwnedDeviceId,
96    ) -> Self {
97        self.auth_state = AuthState::LoggedInWithMatrixAuth {
98            token: Some(token),
99            user_id: Some(user_id),
100            device_id: Some(device_id),
101        };
102        self
103    }
104
105    /// Apply changes to the underlying [`ClientBuilder`].
106    ///
107    /// ```
108    /// # use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
109    /// tokio_test::block_on(async {
110    /// use matrix_sdk::test_utils::client::MockClientBuilder;
111    ///
112    /// MockClientBuilder::new(None)
113    ///     .on_builder(|builder| {
114    ///         // Here it's possible to modify the underlying `ClientBuilder`.
115    ///         builder
116    ///             .handle_refresh_tokens()
117    ///             .cross_process_store_config(CrossProcessLockConfig::MultiProcess { holder_name: "hodor".to_owned()})
118    ///     })
119    ///     .build()
120    ///     .await;
121    /// # anyhow::Ok(()) });
122    /// ```
123    pub fn on_builder<F: FnOnce(ClientBuilder) -> ClientBuilder>(mut self, f: F) -> Self {
124        self.builder = f(self.builder);
125        self
126    }
127
128    /// Finish building the client into the final [`Client`] instance.
129    pub async fn build(self) -> Client {
130        let mut builder = self.builder;
131
132        if let Some(versions) = self.server_versions.into_vec() {
133            builder = builder.server_versions(versions);
134        }
135
136        let client = builder.build().await.expect("building client failed");
137
138        self.auth_state.maybe_restore_client(&client).await;
139
140        client
141    }
142}
143
144/// The possible authentication states of a [`Client`] built with
145/// [`MockClientBuilder`].
146enum AuthState {
147    /// The client is not logged in.
148    None,
149    /// The client is logged in with the native Matrix API.
150    LoggedInWithMatrixAuth {
151        token: Option<String>,
152        user_id: Option<OwnedUserId>,
153        device_id: Option<OwnedDeviceId>,
154    },
155    /// The client is registered with the OAuth 2.0 API.
156    RegisteredWithOAuth,
157    /// The client is logged in with the OAuth 2.0 API.
158    LoggedInWithOAuth,
159}
160
161impl AuthState {
162    /// Restore the given [`Client`] according to this [`AuthState`], if
163    /// necessary.
164    async fn maybe_restore_client(self, client: &Client) {
165        match self {
166            AuthState::None => {}
167            AuthState::LoggedInWithMatrixAuth { token, user_id, device_id } => {
168                client
169                    .matrix_auth()
170                    .restore_session(
171                        MatrixSession {
172                            meta: SessionMeta {
173                                user_id: user_id.unwrap_or(owned_user_id!("@example:localhost")),
174                                device_id: device_id.unwrap_or(owned_device_id!("DEVICEID")),
175                            },
176                            tokens: SessionTokens {
177                                access_token: token.unwrap_or("1234".to_owned()).to_owned(),
178                                refresh_token: None,
179                            },
180                        },
181                        RoomLoadSettings::default(),
182                    )
183                    .await
184                    .unwrap();
185            }
186            AuthState::RegisteredWithOAuth => {
187                client.oauth().restore_registered_client(oauth::mock_client_id());
188            }
189            AuthState::LoggedInWithOAuth => {
190                client
191                    .oauth()
192                    .restore_session(
193                        oauth::mock_session(mock_session_tokens_with_refresh()),
194                        RoomLoadSettings::default(),
195                    )
196                    .await
197                    .unwrap();
198            }
199        }
200    }
201}
202
203/// The server versions cached during client creation.
204enum ServerVersions {
205    /// Cache the default server version.
206    Default,
207    /// Don't cache any server versions.
208    None,
209    /// Cache the given server versions.
210    Custom(Vec<MatrixVersion>),
211}
212
213impl ServerVersions {
214    /// Convert these `ServerVersions` to a list of matrix versions.
215    ///
216    /// Returns `None` if no server versions should be cached in the client.
217    fn into_vec(self) -> Option<Vec<MatrixVersion>> {
218        match self {
219            Self::Default => Some(vec![MatrixVersion::V1_12]),
220            Self::None => None,
221            Self::Custom(versions) => Some(versions),
222        }
223    }
224}
225
226/// A [`SessionMeta`], for unit or integration tests.
227pub fn mock_session_meta() -> SessionMeta {
228    SessionMeta {
229        user_id: owned_user_id!("@example:localhost"),
230        device_id: owned_device_id!("DEVICEID"),
231    }
232}
233
234/// A [`SessionTokens`] including only an access token, for unit or integration
235/// tests.
236pub fn mock_session_tokens() -> SessionTokens {
237    SessionTokens { access_token: "1234".to_owned(), refresh_token: None }
238}
239
240/// A [`SessionTokens`] including an access token and a refresh token, for unit
241/// or integration tests.
242pub fn mock_session_tokens_with_refresh() -> SessionTokens {
243    SessionTokens { access_token: "1234".to_owned(), refresh_token: Some("ZYXWV".to_owned()) }
244}
245
246/// Different session tokens than the ones returned by
247/// [`mock_session_tokens_with_refresh()`].
248pub fn mock_prev_session_tokens_with_refresh() -> SessionTokens {
249    SessionTokens {
250        access_token: "prev-access-token".to_owned(),
251        refresh_token: Some("prev-refresh-token".to_owned()),
252    }
253}
254
255/// A [`MatrixSession`], for unit or integration tests.
256pub fn mock_matrix_session() -> MatrixSession {
257    MatrixSession { meta: mock_session_meta(), tokens: mock_session_tokens() }
258}
259
260/// Mock client data for the OAuth 2.0 API.
261pub mod oauth {
262    use ruma::serde::Raw;
263    use url::Url;
264
265    use crate::{
266        SessionTokens,
267        authentication::oauth::{
268            ClientId, OAuthSession, UserSession,
269            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
270        },
271    };
272
273    /// An OAuth 2.0 `ClientId`, for unit or integration tests.
274    pub fn mock_client_id() -> ClientId {
275        ClientId::new("test_client_id".to_owned())
276    }
277
278    /// A redirect URI, for unit or integration tests.
279    pub fn mock_redirect_uri() -> Url {
280        Url::parse("http://127.0.0.1/").expect("redirect URI should be valid")
281    }
282
283    /// `VerifiedClientMetadata` that should be valid in most cases, for unit or
284    /// integration tests.
285    pub fn mock_client_metadata() -> Raw<ClientMetadata> {
286        let client_uri = Url::parse("https://github.com/matrix-org/matrix-rust-sdk")
287            .expect("client URI should be valid");
288
289        let mut metadata = ClientMetadata::new(
290            ApplicationType::Native,
291            vec![
292                OAuthGrantType::AuthorizationCode { redirect_uris: vec![mock_redirect_uri()] },
293                OAuthGrantType::DeviceCode,
294            ],
295            Localized::new(client_uri, None),
296        );
297        metadata.client_name = Some(Localized::new("matrix-rust-sdk-test".to_owned(), None));
298
299        Raw::new(&metadata).expect("client metadata should serialize successfully")
300    }
301
302    /// An [`OAuthSession`] to restore, for unit or integration tests.
303    pub fn mock_session(tokens: SessionTokens) -> OAuthSession {
304        OAuthSession {
305            client_id: mock_client_id(),
306            user: UserSession { meta: super::mock_session_meta(), tokens },
307        }
308    }
309}