atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git
testing/matrix-sdk-integration-testing/src/helpers.rsRust299 lines10.0 KB
1
use std::{
2
    future::Future,
3
    ops::Deref,
4
    option_env,
5
    path::{Path, PathBuf},
6
    sync::{Arc, LazyLock, Mutex as StdMutex},
7
    time::Duration,
8
};
9
10
use anyhow::Result;
11
use assign::assign;
12
use matrix_sdk::{
13
    Client, ClientBuilder, Room, ThreadingSupport,
14
    config::{RequestConfig, SyncSettings},
15
    encryption::EncryptionSettings,
16
    ruma::{
17
        RoomId,
18
        api::client::{account::register::v3::Request as RegistrationRequest, uiaa},
19
        time::Instant,
20
    },
21
    sliding_sync::VersionBuilder,
22
    sync::SyncResponse,
23
    timeout::ElapsedError,
24
};
25
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
26
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
27
use rand::RngExt;
28
use tempfile::{TempDir, tempdir};
29
use tokio::{sync::Mutex, time::sleep};
30
31
/// This global maintains temp directories alive for the whole lifetime of the
32
/// process.
33
static TMP_DIRS: LazyLock<Mutex<Vec<TempDir>>> = LazyLock::new(Mutex::default);
34
35
enum SqlitePath {
36
    Random,
37
    Path(PathBuf),
38
}
39
40
pub struct TestClientBuilder {
41
    username: String,
42
    use_sqlite_dir: Option<SqlitePath>,
43
    decryption_settings: Option<DecryptionSettings>,
44
    room_key_recipient_strategy: CollectStrategy,
45
    encryption_settings: EncryptionSettings,
46
    enable_share_history_on_invite: bool,
47
    threading_support: ThreadingSupport,
48
    http_proxy: Option<String>,
49
    cross_process_lock_config: CrossProcessLockConfig,
50
}
51
52
impl TestClientBuilder {
53
    pub fn new(username: impl AsRef<str>) -> Self {
54
        let suffix: u128 = rand::rng().random();
55
        let randomized_username = format!("{}{}", username.as_ref(), suffix);
56
        Self::with_exact_username(randomized_username)
57
    }
58
59
    pub fn with_exact_username(username: String) -> Self {
60
        Self {
61
            username,
62
            use_sqlite_dir: None,
63
            decryption_settings: None,
64
            encryption_settings: Default::default(),
65
            room_key_recipient_strategy: Default::default(),
66
            enable_share_history_on_invite: false,
67
            threading_support: ThreadingSupport::Disabled,
68
            http_proxy: None,
69
            cross_process_lock_config: CrossProcessLockConfig::SingleProcess,
70
        }
71
    }
72
73
    pub fn use_sqlite(mut self) -> Self {
74
        self.use_sqlite_dir = Some(SqlitePath::Random);
75
        self
76
    }
77
78
    /// Create or re-use a Sqlite store (with no passphrase) in the supplied
79
    /// directory. Note: this path must remain valid throughout the use of
80
    /// the constructed Client, so if you created a TempDir you must hang on
81
    /// to a reference to it throughout the test.
82
    pub fn use_sqlite_dir(mut self, path: &Path) -> Self {
83
        self.use_sqlite_dir = Some(SqlitePath::Path(path.to_owned()));
84
        self
85
    }
86
87
    pub fn encryption_settings(mut self, encryption_settings: EncryptionSettings) -> Self {
88
        self.encryption_settings = encryption_settings;
89
        self
90
    }
91
92
    pub fn enable_share_history_on_invite(mut self, enable_share_history_on_invite: bool) -> Self {
93
        self.enable_share_history_on_invite = enable_share_history_on_invite;
94
        self
95
    }
96
97
    pub fn enable_threading_support(mut self, thread_support: ThreadingSupport) -> Self {
98
        self.threading_support = thread_support;
99
        self
100
    }
101
102
    /// Simulate the behaviour of the clients when the "exclude insecure
103
    /// devices" (MSC4153) labs flag is enabled.
104
    pub fn exclude_insecure_devices(mut self, exclude_insecure_devices: bool) -> Self {
105
        let (sender_device_trust_requirement, room_key_recipient_strategy) =
106
            if exclude_insecure_devices {
107
                (TrustRequirement::CrossSignedOrLegacy, CollectStrategy::IdentityBasedStrategy)
108
            } else {
109
                (TrustRequirement::Untrusted, CollectStrategy::AllDevices)
110
            };
111
        self.decryption_settings = Some(DecryptionSettings { sender_device_trust_requirement });
112
        self.room_key_recipient_strategy = room_key_recipient_strategy;
113
114
        self
115
    }
116
117
    pub fn cross_process_lock_config(
118
        mut self,
119
        cross_process_lock_config: CrossProcessLockConfig,
120
    ) -> Self {
121
        self.cross_process_lock_config = cross_process_lock_config;
122
        self
123
    }
124
125
    fn common_client_builder(&self) -> ClientBuilder {
126
        let homeserver_url =
127
            option_env!("HOMESERVER_URL").unwrap_or("http://localhost:8228").to_owned();
128
129
        let mut client_builder = Client::builder()
130
            .user_agent("matrix-sdk-integration-tests")
131
            .homeserver_url(homeserver_url)
132
            .sliding_sync_version_builder(VersionBuilder::Native)
133
            .with_encryption_settings(self.encryption_settings)
134
            .with_room_key_recipient_strategy(self.room_key_recipient_strategy.clone())
135
            .with_enable_share_history_on_invite(self.enable_share_history_on_invite)
136
            .with_threading_support(self.threading_support)
137
            .request_config(RequestConfig::short_retry())
138
            .cross_process_store_config(self.cross_process_lock_config.clone());
139
140
        if let Some(decryption_settings) = &self.decryption_settings {
141
            client_builder = client_builder.with_decryption_settings(decryption_settings.clone())
142
        }
143
144
        if let Some(proxy) = &self.http_proxy {
145
            client_builder = client_builder.proxy(proxy);
146
        }
147
148
        client_builder
149
    }
150
151
    /// Create a new Client that is a copy of the supplied one, created using
152
    /// [`Client::restore_session`].
153
    pub async fn duplicate(self, other: &Client) -> Result<Client> {
154
        let client_builder = self.common_client_builder();
155
156
        let client = match self.use_sqlite_dir {
157
            Some(SqlitePath::Path(path_buf)) => {
158
                client_builder.sqlite_store(&path_buf, None).build().await?
159
            }
160
            _ => {
161
                panic!("You must call use_sqlite_dir for a duplicate client!");
162
            }
163
        };
164
165
        client
166
            .restore_session(
167
                other.session().expect("Session must be logged in before we can duplicate it"),
168
            )
169
            .await
170
            .expect("Failed to restore session");
171
172
        Ok(client)
173
    }
174
175
    pub async fn build(self) -> Result<Client> {
176
        let client_builder = self.common_client_builder();
177
        let client = match self.use_sqlite_dir {
178
            None => client_builder.build().await?,
179
            Some(SqlitePath::Random) => {
180
                let tmp_dir = tempdir()?;
181
                let client = client_builder.sqlite_store(tmp_dir.path(), None).build().await?;
182
                TMP_DIRS.lock().await.push(tmp_dir);
183
                client
184
            }
185
            Some(SqlitePath::Path(path_buf)) => {
186
                client_builder.sqlite_store(&path_buf, None).build().await?
187
            }
188
        };
189
190
        // safe to assume we have not registered this user yet, but ignore if we did
191
192
        let auth = client.matrix_auth();
193
        let mut try_login = true;
194
        if let Err(resp) = auth.register(RegistrationRequest::new()).await {
195
            // FIXME: do actually check the registration types...
196
            if let Some(_response) = resp.as_uiaa_response() {
197
                let request = assign!(RegistrationRequest::new(), {
198
                    username: Some(self.username.clone()),
199
                    password: Some(self.username.clone()),
200
201
                    auth: Some(uiaa::AuthData::Dummy(uiaa::Dummy::new())),
202
                });
203
                // if this failed, we will attempt to login after.
204
                try_login = auth.register(request).await.is_err();
205
            }
206
        }
207
        if try_login {
208
            auth.login_username(&self.username, &self.username).await?;
209
        }
210
211
        Ok(client)
212
    }
213
}
214
215
/// Client that correctly maintains and propagates sync token values.
216
#[derive(Clone)]
217
pub struct SyncTokenAwareClient {
218
    client: Client,
219
    token: Arc<StdMutex<Option<String>>>,
220
}
221
222
impl SyncTokenAwareClient {
223
    pub fn new(client: Client) -> Self {
224
        Self { client, token: Arc::new(None.into()) }
225
    }
226
227
    pub async fn sync_once(&self) -> Result<SyncResponse> {
228
        let mut settings = SyncSettings::default().timeout(Duration::from_secs(1));
229
230
        let token = { self.token.lock().unwrap().clone() };
231
        if let Some(token) = token {
232
            settings = settings.token(token);
233
        }
234
235
        let response = self.client.sync_once(settings).await?;
236
237
        let mut prev_token = self.token.lock().unwrap();
238
        if prev_token.as_ref() != Some(&response.next_batch) {
239
            *prev_token = Some(response.next_batch.clone());
240
        }
241
        Ok(response)
242
    }
243
}
244
245
impl Deref for SyncTokenAwareClient {
246
    type Target = Client;
247
248
    fn deref(&self) -> &Self::Target {
249
        &self.client
250
    }
251
}
252
253
/// Repeatedly run the given callback, at increasing intervals, until it returns
254
/// `Some`, or the given timeout has elapsed.
255
///
256
/// The callback `cb` is called with the attempt number as an argument.
257
///
258
/// # Returns
259
///
260
/// The result of `cb` if it returned `Some`. If the timeout elapses, an error
261
/// of type [`ElapsedError`].
262
pub async fn wait_until_some<F, Fut, R>(
263
    mut cb: F,
264
    timeout_duration: Duration,
265
) -> Result<R, ElapsedError>
266
where
267
    F: FnMut(u32) -> Fut,
268
    Fut: Future<Output = Option<R>>,
269
{
270
    let start = Instant::now();
271
    let mut attempt_count = 1;
272
    loop {
273
        if let Some(r) = cb(attempt_count).await {
274
            return Ok(r);
275
        }
276
        if start - Instant::now() > timeout_duration {
277
            return Err(ElapsedError());
278
        }
279
        sleep(Duration::from_millis(125) * attempt_count).await;
280
        attempt_count += 1;
281
    }
282
}
283
284
/// Waits for a room to arrive from a sync, for ~2 seconds.
285
///
286
/// Note: this doesn't run any sync, it assumes a sync has been running in the
287
/// background.
288
pub async fn wait_for_room(client: &Client, room_id: &RoomId) -> Room {
289
    let cb = async |i| {
290
        client.get_room(room_id).or_else(|| {
291
            tracing::warn!("attempt #{i}, alice couldn't see the room, retrying in a few…");
292
            None
293
        })
294
    };
295
296
    wait_until_some(cb, Duration::from_secs(2))
297
        .await
298
        .expect("room couldn't be fetched after all attempts")
299
}