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::{
18    store::{RoomLoadSettings, StoreConfig},
19    SessionMeta,
20};
21use ruma::{api::MatrixVersion, owned_device_id, owned_user_id};
22
23use crate::{
24    authentication::matrix::MatrixSession, config::RequestConfig, Client, ClientBuilder,
25    SessionTokens,
26};
27
28/// An augmented [`ClientBuilder`] that also allows for handling session login.
29#[allow(missing_debug_implementations)]
30pub struct MockClientBuilder {
31    builder: ClientBuilder,
32    auth_state: AuthState,
33}
34
35impl MockClientBuilder {
36    /// Create a new [`MockClientBuilder`] connected to the given homeserver,
37    /// using Matrix V1.12, and which will not attempt any network retry (by
38    /// default).
39    pub(crate) fn new(homeserver: String) -> Self {
40        let default_builder = Client::builder()
41            .homeserver_url(&homeserver)
42            .server_versions([MatrixVersion::V1_12])
43            .request_config(RequestConfig::new().disable_retry());
44
45        Self { builder: default_builder, auth_state: AuthState::LoggedInWithMatrixAuth }
46    }
47
48    /// Doesn't log-in a user.
49    ///
50    /// Authenticated requests will fail if this is called.
51    pub fn unlogged(mut self) -> Self {
52        self.auth_state = AuthState::None;
53        self
54    }
55
56    /// The client is registered with the OAuth 2.0 API.
57    pub fn registered_with_oauth(mut self, issuer: impl Into<String>) -> Self {
58        self.auth_state = AuthState::RegisteredWithOAuth { issuer: issuer.into() };
59        self
60    }
61
62    /// The user is already logged in with the OAuth 2.0 API.
63    pub fn logged_in_with_oauth(mut self, issuer: impl Into<String>) -> Self {
64        self.auth_state = AuthState::LoggedInWithOAuth { issuer: issuer.into() };
65        self
66    }
67
68    /// Provides another [`StoreConfig`] for the underlying [`ClientBuilder`].
69    pub fn store_config(mut self, store_config: StoreConfig) -> Self {
70        self.builder = self.builder.store_config(store_config);
71        self
72    }
73
74    /// Use an SQLite store at the given path for the underlying
75    /// [`ClientBuilder`].
76    #[cfg(feature = "sqlite")]
77    pub fn sqlite_store(mut self, path: impl AsRef<std::path::Path>) -> Self {
78        self.builder = self.builder.sqlite_store(path, None);
79        self
80    }
81
82    /// Handle refreshing access tokens automatically.
83    pub fn handle_refresh_tokens(mut self) -> Self {
84        self.builder = self.builder.handle_refresh_tokens();
85        self
86    }
87
88    /// Finish building the client into the final [`Client`] instance.
89    pub async fn build(self) -> Client {
90        let client = self.builder.build().await.expect("building client failed");
91        self.auth_state.maybe_restore_client(&client).await;
92
93        client
94    }
95}
96
97/// The possible authentication states of a [`Client`] built with
98/// [`MockClientBuilder`].
99enum AuthState {
100    /// The client is not logged in.
101    None,
102    /// The client is logged in with the native Matrix API.
103    LoggedInWithMatrixAuth,
104    /// The client is registered with the OAuth 2.0 API.
105    RegisteredWithOAuth { issuer: String },
106    /// The client is logged in with the OAuth 2.0 API.
107    LoggedInWithOAuth { issuer: String },
108}
109
110impl AuthState {
111    /// Restore the given [`Client`] according to this [`AuthState`], if
112    /// necessary.
113    async fn maybe_restore_client(self, client: &Client) {
114        match self {
115            AuthState::None => {}
116            AuthState::LoggedInWithMatrixAuth => {
117                client
118                    .matrix_auth()
119                    .restore_session(mock_matrix_session(), RoomLoadSettings::default())
120                    .await
121                    .unwrap();
122            }
123            AuthState::RegisteredWithOAuth { issuer } => {
124                let issuer = url::Url::parse(&issuer).unwrap();
125                client.oauth().restore_registered_client(issuer, oauth::mock_client_id());
126            }
127            AuthState::LoggedInWithOAuth { issuer } => {
128                client
129                    .oauth()
130                    .restore_session(
131                        oauth::mock_session(mock_session_tokens_with_refresh(), issuer),
132                        RoomLoadSettings::default(),
133                    )
134                    .await
135                    .unwrap();
136            }
137        }
138    }
139}
140
141/// A [`SessionMeta`], for unit or integration tests.
142pub fn mock_session_meta() -> SessionMeta {
143    SessionMeta {
144        user_id: owned_user_id!("@example:localhost"),
145        device_id: owned_device_id!("DEVICEID"),
146    }
147}
148
149/// A [`SessionTokens`] including only an access token, for unit or integration
150/// tests.
151pub fn mock_session_tokens() -> SessionTokens {
152    SessionTokens { access_token: "1234".to_owned(), refresh_token: None }
153}
154
155/// A [`SessionTokens`] including an access token and a refresh token, for unit
156/// or integration tests.
157pub fn mock_session_tokens_with_refresh() -> SessionTokens {
158    SessionTokens { access_token: "1234".to_owned(), refresh_token: Some("ZYXWV".to_owned()) }
159}
160
161/// Different session tokens than the ones returned by
162/// [`mock_session_tokens_with_refresh()`].
163pub fn mock_prev_session_tokens_with_refresh() -> SessionTokens {
164    SessionTokens {
165        access_token: "prev-access-token".to_owned(),
166        refresh_token: Some("prev-refresh-token".to_owned()),
167    }
168}
169
170/// A [`MatrixSession`], for unit or integration tests.
171pub fn mock_matrix_session() -> MatrixSession {
172    MatrixSession { meta: mock_session_meta(), tokens: mock_session_tokens() }
173}
174
175/// Mock client data for the OAuth 2.0 API.
176pub mod oauth {
177    use ruma::serde::Raw;
178    use url::Url;
179
180    use crate::{
181        authentication::oauth::{
182            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
183            ClientId, OAuthSession, UserSession,
184        },
185        SessionTokens,
186    };
187
188    /// An OAuth 2.0 `ClientId`, for unit or integration tests.
189    pub fn mock_client_id() -> ClientId {
190        ClientId::new("test_client_id".to_owned())
191    }
192
193    /// A redirect URI, for unit or integration tests.
194    pub fn mock_redirect_uri() -> Url {
195        Url::parse("http://127.0.0.1/").expect("redirect URI should be valid")
196    }
197
198    /// `VerifiedClientMetadata` that should be valid in most cases, for unit or
199    /// integration tests.
200    pub fn mock_client_metadata() -> Raw<ClientMetadata> {
201        let client_uri = Url::parse("https://github.com/matrix-org/matrix-rust-sdk")
202            .expect("client URI should be valid");
203
204        let mut metadata = ClientMetadata::new(
205            ApplicationType::Native,
206            vec![
207                OAuthGrantType::AuthorizationCode { redirect_uris: vec![mock_redirect_uri()] },
208                OAuthGrantType::DeviceCode,
209            ],
210            Localized::new(client_uri, None),
211        );
212        metadata.client_name = Some(Localized::new("matrix-rust-sdk-test".to_owned(), None));
213
214        Raw::new(&metadata).expect("client metadata should serialize successfully")
215    }
216
217    /// An [`OAuthSession`] to restore, for unit or integration tests.
218    pub fn mock_session(tokens: SessionTokens, issuer: impl AsRef<str>) -> OAuthSession {
219        let issuer = Url::parse(issuer.as_ref()).unwrap();
220
221        OAuthSession {
222            client_id: mock_client_id(),
223            user: UserSession { meta: super::mock_session_meta(), tokens, issuer },
224        }
225    }
226}