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 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 use an initial, cached server versions list 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    /// # use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
108    /// tokio_test::block_on(async {
109    /// use matrix_sdk::test_utils::client::MockClientBuilder;
110    ///
111    /// MockClientBuilder::new(None)
112    ///     .on_builder(|builder| {
113    ///         // Here it's possible to modify the underlying `ClientBuilder`.
114    ///         builder
115    ///             .handle_refresh_tokens()
116    ///             .cross_process_store_config(CrossProcessLockConfig::MultiProcess { holder_name: "hodor".to_owned()})
117    ///     })
118    ///     .build()
119    ///     .await;
120    /// # anyhow::Ok(()) });
121    /// ```
122    pub fn on_builder<F: FnOnce(ClientBuilder) -> ClientBuilder>(mut self, f: F) -> Self {
123        self.builder = f(self.builder);
124        self
125    }
126
127    /// Finish building the client into the final [`Client`] instance.
128    pub async fn build(self) -> Client {
129        let mut builder = self.builder;
130
131        if let Some(versions) = self.server_versions.into_vec() {
132            builder = builder.server_versions(versions);
133        }
134
135        let client = builder.build().await.expect("building client failed");
136
137        self.auth_state.maybe_restore_client(&client).await;
138
139        client
140    }
141}
142
143/// The possible authentication states of a [`Client`] built with
144/// [`MockClientBuilder`].
145enum AuthState {
146    /// The client is not logged in.
147    None,
148    /// The client is logged in with the native Matrix API.
149    LoggedInWithMatrixAuth {
150        token: Option<String>,
151        user_id: Option<OwnedUserId>,
152        device_id: Option<OwnedDeviceId>,
153    },
154    /// The client is registered with the OAuth 2.0 API.
155    RegisteredWithOAuth,
156    /// The client is logged in with the OAuth 2.0 API.
157    LoggedInWithOAuth,
158}
159
160impl AuthState {
161    /// Restore the given [`Client`] according to this [`AuthState`], if
162    /// necessary.
163    async fn maybe_restore_client(self, client: &Client) {
164        match self {
165            AuthState::None => {}
166            AuthState::LoggedInWithMatrixAuth { token, user_id, device_id } => {
167                client
168                    .matrix_auth()
169                    .restore_session(
170                        MatrixSession {
171                            meta: SessionMeta {
172                                user_id: user_id.unwrap_or(owned_user_id!("@example:localhost")),
173                                device_id: device_id.unwrap_or(owned_device_id!("DEVICEID")),
174                            },
175                            tokens: SessionTokens {
176                                access_token: token.unwrap_or("1234".to_owned()).to_owned(),
177                                refresh_token: None,
178                            },
179                        },
180                        RoomLoadSettings::default(),
181                    )
182                    .await
183                    .unwrap();
184            }
185            AuthState::RegisteredWithOAuth => {
186                client.oauth().restore_registered_client(oauth::mock_client_id());
187            }
188            AuthState::LoggedInWithOAuth => {
189                client
190                    .oauth()
191                    .restore_session(
192                        oauth::mock_session(mock_session_tokens_with_refresh()),
193                        RoomLoadSettings::default(),
194                    )
195                    .await
196                    .unwrap();
197            }
198        }
199    }
200}
201
202/// The server versions cached during client creation.
203enum ServerVersions {
204    /// Cache the default server version.
205    Default,
206    /// Don't cache any server versions.
207    None,
208    /// Cache the given server versions.
209    Custom(Vec<MatrixVersion>),
210}
211
212impl ServerVersions {
213    /// Convert these `ServerVersions` to a list of matrix versions.
214    ///
215    /// Returns `None` if no server versions should be cached in the client.
216    fn into_vec(self) -> Option<Vec<MatrixVersion>> {
217        match self {
218            Self::Default => Some(vec![MatrixVersion::V1_12]),
219            Self::None => None,
220            Self::Custom(versions) => Some(versions),
221        }
222    }
223}
224
225/// A [`SessionMeta`], for unit or integration tests.
226pub fn mock_session_meta() -> SessionMeta {
227    SessionMeta {
228        user_id: owned_user_id!("@example:localhost"),
229        device_id: owned_device_id!("DEVICEID"),
230    }
231}
232
233/// A [`SessionTokens`] including only an access token, for unit or integration
234/// tests.
235pub fn mock_session_tokens() -> SessionTokens {
236    SessionTokens { access_token: "1234".to_owned(), refresh_token: None }
237}
238
239/// A [`SessionTokens`] including an access token and a refresh token, for unit
240/// or integration tests.
241pub fn mock_session_tokens_with_refresh() -> SessionTokens {
242    SessionTokens { access_token: "1234".to_owned(), refresh_token: Some("ZYXWV".to_owned()) }
243}
244
245/// Different session tokens than the ones returned by
246/// [`mock_session_tokens_with_refresh()`].
247pub fn mock_prev_session_tokens_with_refresh() -> SessionTokens {
248    SessionTokens {
249        access_token: "prev-access-token".to_owned(),
250        refresh_token: Some("prev-refresh-token".to_owned()),
251    }
252}
253
254/// A [`MatrixSession`], for unit or integration tests.
255pub fn mock_matrix_session() -> MatrixSession {
256    MatrixSession { meta: mock_session_meta(), tokens: mock_session_tokens() }
257}
258
259/// Mock client data for the OAuth 2.0 API.
260pub mod oauth {
261    use ruma::serde::Raw;
262    use url::Url;
263
264    use crate::{
265        SessionTokens,
266        authentication::oauth::{
267            ClientId, OAuthSession, UserSession,
268            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
269        },
270    };
271
272    /// An OAuth 2.0 `ClientId`, for unit or integration tests.
273    pub fn mock_client_id() -> ClientId {
274        ClientId::new("test_client_id".to_owned())
275    }
276
277    /// A redirect URI, for unit or integration tests.
278    pub fn mock_redirect_uri() -> Url {
279        Url::parse("http://127.0.0.1/").expect("redirect URI should be valid")
280    }
281
282    /// `VerifiedClientMetadata` that should be valid in most cases, for unit or
283    /// integration tests.
284    pub fn mock_client_metadata() -> Raw<ClientMetadata> {
285        let client_uri = Url::parse("https://github.com/matrix-org/matrix-rust-sdk")
286            .expect("client URI should be valid");
287
288        let mut metadata = ClientMetadata::new(
289            ApplicationType::Native,
290            vec![
291                OAuthGrantType::AuthorizationCode { redirect_uris: vec![mock_redirect_uri()] },
292                OAuthGrantType::DeviceCode,
293            ],
294            Localized::new(client_uri, None),
295        );
296        metadata.client_name = Some(Localized::new("matrix-rust-sdk-test".to_owned(), None));
297
298        Raw::new(&metadata).expect("client metadata should serialize successfully")
299    }
300
301    /// An [`OAuthSession`] to restore, for unit or integration tests.
302    pub fn mock_session(tokens: SessionTokens) -> OAuthSession {
303        OAuthSession {
304            client_id: mock_client_id(),
305            user: UserSession { meta: super::mock_session_meta(), tokens },
306        }
307    }
308}