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