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::{store::RoomLoadSettings, SessionMeta};
18use ruma::{api::MatrixVersion, owned_device_id, owned_user_id, OwnedDeviceId, OwnedUserId};
19
20use crate::{
21    authentication::matrix::MatrixSession, config::RequestConfig, Client, ClientBuilder,
22    SessionTokens,
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 homeserver.
39    pub fn new(homeserver: Option<&str>) -> Self {
40        let homeserver = homeserver.unwrap_or("http://localhost");
41
42        let default_builder = Client::builder()
43            .homeserver_url(homeserver)
44            .request_config(RequestConfig::new().disable_retry());
45
46        Self {
47            builder: default_builder,
48            auth_state: AuthState::LoggedInWithMatrixAuth {
49                token: None,
50                user_id: None,
51                device_id: None,
52            },
53            server_versions: ServerVersions::Default,
54        }
55    }
56
57    /// Don't cache server versions in the client.
58    pub fn no_server_versions(mut self) -> Self {
59        self.server_versions = ServerVersions::None;
60        self
61    }
62
63    /// Set the cached server versions in the client.
64    pub fn server_versions(mut self, versions: Vec<MatrixVersion>) -> Self {
65        self.server_versions = ServerVersions::Custom(versions);
66        self
67    }
68
69    /// Doesn't log-in a user.
70    ///
71    /// Authenticated requests will fail if this is called.
72    pub fn unlogged(mut self) -> Self {
73        self.auth_state = AuthState::None;
74        self
75    }
76
77    /// The client is registered with the OAuth 2.0 API.
78    pub fn registered_with_oauth(mut self) -> Self {
79        self.auth_state = AuthState::RegisteredWithOAuth;
80        self
81    }
82
83    /// The user is already logged in with the OAuth 2.0 API.
84    pub fn logged_in_with_oauth(mut self) -> Self {
85        self.auth_state = AuthState::LoggedInWithOAuth;
86        self
87    }
88
89    /// The user is already logged in with the Matrix Auth.
90    pub fn logged_in_with_token(
91        mut self,
92        token: String,
93        user_id: OwnedUserId,
94        device_id: OwnedDeviceId,
95    ) -> Self {
96        self.auth_state = AuthState::LoggedInWithMatrixAuth {
97            token: Some(token),
98            user_id: Some(user_id),
99            device_id: Some(device_id),
100        };
101        self
102    }
103
104    /// Apply changes to the underlying [`ClientBuilder`].
105    ///
106    /// ```
107    /// # tokio_test::block_on(async {
108    /// use matrix_sdk::test_utils::client::MockClientBuilder;
109    ///
110    /// MockClientBuilder::new(None)
111    ///     .on_builder(|builder| {
112    ///         // Here it's possible to modify the underlying `ClientBuilder`.
113    ///         builder
114    ///             .handle_refresh_tokens()
115    ///             .cross_process_store_locks_holder_name("hodor".to_owned())
116    ///     })
117    ///     .build()
118    ///     .await;
119    /// # anyhow::Ok(()) });
120    /// ```
121    pub fn on_builder<F: FnOnce(ClientBuilder) -> ClientBuilder>(mut self, f: F) -> Self {
122        self.builder = f(self.builder);
123        self
124    }
125
126    /// Finish building the client into the final [`Client`] instance.
127    pub async fn build(self) -> Client {
128        let mut builder = self.builder;
129
130        if let Some(versions) = self.server_versions.into_vec() {
131            builder = builder.server_versions(versions);
132        }
133
134        let client = builder.build().await.expect("building client failed");
135
136        self.auth_state.maybe_restore_client(&client).await;
137
138        client
139    }
140}
141
142/// The possible authentication states of a [`Client`] built with
143/// [`MockClientBuilder`].
144enum AuthState {
145    /// The client is not logged in.
146    None,
147    /// The client is logged in with the native Matrix API.
148    LoggedInWithMatrixAuth {
149        token: Option<String>,
150        user_id: Option<OwnedUserId>,
151        device_id: Option<OwnedDeviceId>,
152    },
153    /// The client is registered with the OAuth 2.0 API.
154    RegisteredWithOAuth,
155    /// The client is logged in with the OAuth 2.0 API.
156    LoggedInWithOAuth,
157}
158
159impl AuthState {
160    /// Restore the given [`Client`] according to this [`AuthState`], if
161    /// necessary.
162    async fn maybe_restore_client(self, client: &Client) {
163        match self {
164            AuthState::None => {}
165            AuthState::LoggedInWithMatrixAuth { token, user_id, device_id } => {
166                client
167                    .matrix_auth()
168                    .restore_session(
169                        MatrixSession {
170                            meta: SessionMeta {
171                                user_id: user_id.unwrap_or(owned_user_id!("@example:localhost")),
172                                device_id: device_id.unwrap_or(owned_device_id!("DEVICEID")),
173                            },
174                            tokens: SessionTokens {
175                                access_token: token.unwrap_or("1234".to_owned()).to_owned(),
176                                refresh_token: None,
177                            },
178                        },
179                        RoomLoadSettings::default(),
180                    )
181                    .await
182                    .unwrap();
183            }
184            AuthState::RegisteredWithOAuth => {
185                client.oauth().restore_registered_client(oauth::mock_client_id());
186            }
187            AuthState::LoggedInWithOAuth => {
188                client
189                    .oauth()
190                    .restore_session(
191                        oauth::mock_session(mock_session_tokens_with_refresh()),
192                        RoomLoadSettings::default(),
193                    )
194                    .await
195                    .unwrap();
196            }
197        }
198    }
199}
200
201/// The server versions cached during client creation.
202enum ServerVersions {
203    /// Cache the default server version.
204    Default,
205    /// Don't cache any server versions.
206    None,
207    /// Cache the given server versions.
208    Custom(Vec<MatrixVersion>),
209}
210
211impl ServerVersions {
212    /// Convert these `ServerVersions` to a list of matrix versions.
213    ///
214    /// Returns `None` if no server versions should be cached in the client.
215    fn into_vec(self) -> Option<Vec<MatrixVersion>> {
216        match self {
217            Self::Default => Some(vec![MatrixVersion::V1_12]),
218            Self::None => None,
219            Self::Custom(versions) => Some(versions),
220        }
221    }
222}
223
224/// A [`SessionMeta`], for unit or integration tests.
225pub fn mock_session_meta() -> SessionMeta {
226    SessionMeta {
227        user_id: owned_user_id!("@example:localhost"),
228        device_id: owned_device_id!("DEVICEID"),
229    }
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        authentication::oauth::{
265            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
266            ClientId, OAuthSession, UserSession,
267        },
268        SessionTokens,
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}