atagen

macltok

public
Star0
Watch0
9c05b1dd327a57081652545c709cc8d7e961de15
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/account.rsRust355 lines11.8 KB
1
#![cfg(feature = "sqlite")]
2
mod support;
3
4
use std::{path::PathBuf, sync::Arc, time::SystemTime};
5
6
use macltok::{
7
    AccountError, EncryptedSessionStore, EngineConfig, Event, PasswordLogin, Restore, Session,
8
    SessionRecord, SessionStore, StoreConfig,
9
};
10
use matrix_sdk::{ruma::api::client::sync::sync_events::v5, test_utils::mocks::MatrixMockServer};
11
12
#[tokio::test]
13
async fn password_session_round_trips_and_discards_with_its_store() {
14
    let server = MatrixMockServer::new().await;
15
    server
16
        .mock_versions()
17
        .with_simplified_sliding_sync()
18
        .ok()
19
        .mount()
20
        .await;
21
    server.mock_login().ok().mount().await;
22
    server
23
        .mock_sliding_sync()
24
        .ok(v5::Response::new("1".to_owned()))
25
        .mount()
26
        .await;
27
    let root = temporary_path();
28
    let store_path = root.join("store");
29
    let record_path = root.join("records").join("session");
30
    let sessions = || {
31
        EncryptedSessionStore::file(
32
            &record_path,
33
            StoreConfig::sqlite(&store_path, "test store passphrase"),
34
        )
35
    };
36
    let config = EngineConfig::default().with_initial_history_limit(0);
37
    let session = Session::login_password(
38
        PasswordLogin::new(server.uri(), "example", "wordpass")
39
            .allow_insecure_loopback(true)
40
            .with_device_display_name("macltok test"),
41
        Arc::new(sessions()),
42
    )
43
    .await
44
    .unwrap();
45
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
46
    let running = tokio::spawn(engine.run());
47
    support::receive_until(&mut events, |event| {
48
        matches!(event, Event::Authenticated { .. })
49
    })
50
    .await;
51
    support::shut_down(&handle, &mut events, running).await;
52
    drop(handle);
53
54
    let wrong = EncryptedSessionStore::file(
55
        &record_path,
56
        StoreConfig::sqlite(&store_path, "someone else's passphrase"),
57
    );
58
    assert!(wrong.load().await.is_err());
59
60
    let sessions = Arc::new(sessions());
61
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
62
        panic!("the saved login didn't resume");
63
    };
64
    let (engine, handle, mut events) = session.build(config).await.unwrap();
65
    let running = tokio::spawn(engine.run());
66
    support::receive_until(&mut events, |event| {
67
        matches!(event, Event::Authenticated { .. })
68
    })
69
    .await;
70
    support::shut_down(&handle, &mut events, running).await;
71
    drop(handle);
72
73
    sessions.discard().await.unwrap();
74
    assert!(!record_path.exists());
75
    assert!(!store_path.exists());
76
    assert!(Session::restore(sessions).await.unwrap().is_none());
77
    std::fs::remove_dir_all(root).unwrap();
78
}
79
80
#[tokio::test]
81
async fn a_restart_after_a_soft_logout_still_knows_the_device() {
82
    let server = MatrixMockServer::new().await;
83
    server.mock_versions().ok().mount().await;
84
    server.mock_login().ok().mount().await;
85
    let root = temporary_path();
86
    let sessions = || {
87
        EncryptedSessionStore::file(
88
            root.join("session"),
89
            StoreConfig::sqlite(root.join("store"), "test store passphrase"),
90
        )
91
    };
92
    Session::login_password(
93
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
94
        Arc::new(sessions()),
95
    )
96
    .await
97
    .unwrap();
98
    sessions().soft_log_out().await.unwrap();
99
100
    let Some(Restore::SoftLoggedOut(soft)) = Session::restore(Arc::new(sessions())).await.unwrap()
101
    else {
102
        panic!("a soft logged out record came back as something else");
103
    };
104
    // the device matrix-sdk-test's canned login response hands out
105
    assert_eq!(soft.device_id, "GHTYAJCE");
106
    Session::login_password(soft.password_login("wordpass"), Arc::new(sessions()))
107
        .await
108
        .unwrap();
109
    let requests = server.server().received_requests().await.unwrap();
110
    let relogin: serde_json::Value = requests
111
        .iter()
112
        .rfind(|request| request.url.path().ends_with("/login"))
113
        .unwrap()
114
        .body_json()
115
        .unwrap();
116
    assert_eq!(relogin["device_id"], "GHTYAJCE");
117
    assert!(matches!(
118
        Session::restore(Arc::new(sessions())).await.unwrap(),
119
        Some(Restore::Resumed(_))
120
    ));
121
    std::fs::remove_dir_all(root).unwrap();
122
}
123
124
#[tokio::test]
125
async fn a_login_whose_first_save_fails_can_still_be_saved() {
126
    let server = MatrixMockServer::new().await;
127
    server.mock_versions().ok().mount().await;
128
    server.mock_login().ok().mount().await;
129
    let root = temporary_path();
130
    let records = root.join("records");
131
    std::fs::create_dir_all(&records).unwrap();
132
    let sessions = || {
133
        EncryptedSessionStore::file(
134
            records.join("session"),
135
            StoreConfig::sqlite(root.join("store"), "test store passphrase"),
136
        )
137
    };
138
    let writable = std::fs::metadata(&records).unwrap().permissions();
139
    let mut read_only = writable.clone();
140
    read_only.set_readonly(true);
141
    std::fs::set_permissions(&records, read_only).unwrap();
142
143
    let login = Session::login_password(
144
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
145
        Arc::new(sessions()),
146
    )
147
    .await;
148
    let Err(AccountError::Unsaved { session, .. }) = login else {
149
        panic!("a login that couldn't save its record didn't hand the session back");
150
    };
151
    std::fs::set_permissions(&records, writable).unwrap();
152
    drop(session.save().await.unwrap());
153
    assert!(matches!(
154
        Session::restore(Arc::new(sessions())).await.unwrap(),
155
        Some(Restore::Resumed(_))
156
    ));
157
    std::fs::remove_dir_all(root).unwrap();
158
}
159
160
#[tokio::test]
161
async fn a_store_takes_one_session_at_a_time() {
162
    let server = MatrixMockServer::new().await;
163
    server.mock_versions().ok().mount().await;
164
    server.mock_login().ok().mount().await;
165
    let root = temporary_path();
166
    let sessions = || {
167
        EncryptedSessionStore::file(
168
            root.join("session"),
169
            StoreConfig::sqlite(root.join("store"), "test store passphrase"),
170
        )
171
    };
172
    let (engine, handle, events) = Session::login_password(
173
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
174
        Arc::new(sessions()),
175
    )
176
    .await
177
    .unwrap()
178
    .build(EngineConfig::default())
179
    .await
180
    .unwrap();
181
182
    assert!(matches!(
183
        Session::restore(Arc::new(sessions())).await,
184
        Err(AccountError::StoreInUse)
185
    ));
186
    drop((engine, events));
187
    assert!(matches!(
188
        Session::restore(Arc::new(sessions())).await,
189
        Err(AccountError::StoreInUse)
190
    ));
191
    drop(handle);
192
    let resumed = Session::restore(Arc::new(sessions())).await.unwrap();
193
    assert!(matches!(resumed, Some(Restore::Resumed(_))));
194
    drop(resumed);
195
    sessions().discard().await.unwrap();
196
    assert!(!root.join("store").exists());
197
    std::fs::remove_dir_all(root).unwrap();
198
}
199
200
#[tokio::test]
201
async fn a_torn_or_failed_save_leaves_the_old_record() {
202
    let root = temporary_path();
203
    let record_path = root.join("session");
204
    let sessions = || {
205
        EncryptedSessionStore::file(
206
            &record_path,
207
            StoreConfig::sqlite(root.join("store"), "test store passphrase"),
208
        )
209
    };
210
    let temporary = root.join("session.1.0.tmp");
211
    sessions()
212
        .save(&record("https://old.example"))
213
        .await
214
        .unwrap();
215
216
    std::fs::write(&temporary, b"half a rec").unwrap();
217
    let loaded = sessions().load().await.unwrap().unwrap();
218
    assert_eq!(loaded.homeserver(), "https://old.example");
219
220
    let writable = std::fs::metadata(&root).unwrap().permissions();
221
    let mut read_only = writable.clone();
222
    read_only.set_readonly(true);
223
    std::fs::set_permissions(&root, read_only).unwrap();
224
    assert!(
225
        sessions()
226
            .save(&record("https://new.example"))
227
            .await
228
            .is_err()
229
    );
230
    let loaded = sessions().load().await.unwrap().unwrap();
231
    assert_eq!(loaded.homeserver(), "https://old.example");
232
233
    std::fs::set_permissions(&root, writable).unwrap();
234
    std::fs::remove_dir_all(root).unwrap();
235
}
236
237
#[tokio::test]
238
async fn a_refresh_is_saved_before_anyone_hears_of_it() {
239
    use matrix_sdk_test::test_json;
240
    use wiremock::{
241
        Mock, ResponseTemplate,
242
        matchers::{header, method, path, path_regex},
243
    };
244
245
    let server = MatrixMockServer::new().await;
246
    server.mock_versions().ok().mount().await;
247
    server
248
        .mock_login()
249
        .respond_with(
250
            ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_WITH_REFRESH_TOKEN),
251
        )
252
        .mount()
253
        .await;
254
    Mock::given(method("POST"))
255
        .and(path_regex("sync"))
256
        .and(header("authorization", "Bearer abc123"))
257
        .respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({
258
            "errcode": "M_UNKNOWN_TOKEN",
259
            "error": "expired",
260
            "soft_logout": false,
261
        })))
262
        .with_priority(1)
263
        .mount(server.server())
264
        .await;
265
    Mock::given(method("POST"))
266
        .and(path("/_matrix/client/v3/refresh"))
267
        .respond_with(
268
            ResponseTemplate::new(200).set_body_json(&*test_json::REFRESH_TOKEN_WITH_REFRESH_TOKEN),
269
        )
270
        .mount(server.server())
271
        .await;
272
    server
273
        .mock_sliding_sync()
274
        .ok(v5::Response::new("1".to_owned()))
275
        .mount()
276
        .await;
277
    let root = temporary_path();
278
    let sessions = Arc::new(EncryptedSessionStore::file(
279
        root.join("session"),
280
        StoreConfig::sqlite(root.join("store"), "test store passphrase"),
281
    ));
282
    let session = Session::login_password(
283
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
284
        sessions.clone(),
285
    )
286
    .await
287
    .unwrap();
288
    let (engine, handle, mut events) = session
289
        .build(EngineConfig::default().with_initial_history_limit(0))
290
        .await
291
        .unwrap();
292
    let running = tokio::spawn(engine.run());
293
    support::receive_until(&mut events, |event| {
294
        matches!(event, Event::SessionTokensChanged)
295
    })
296
    .await;
297
    let saved = serde_json::to_value(sessions.load().await.unwrap().unwrap()).unwrap();
298
    assert_eq!(saved["authentication"]["session"]["refresh_token"], "wxyz");
299
    support::shut_down(&handle, &mut events, running).await;
300
    drop(handle);
301
    std::fs::remove_dir_all(root).unwrap();
302
}
303
304
#[tokio::test]
305
async fn a_homeserver_that_cant_be_one_is_refused_before_anything_is_sent() {
306
    let root = temporary_path();
307
    let sessions: Arc<dyn SessionStore> = Arc::new(EncryptedSessionStore::file(
308
        root.join("session"),
309
        StoreConfig::sqlite(root.join("store"), "test store passphrase"),
310
    ));
311
    for (homeserver, loopback) in [
312
        ("", true),
313
        ("  ", true),
314
        ("example .org", true),
315
        ("ftp://example.org", true),
316
        ("https://user@example.org", true),
317
        ("https://", true),
318
        ("example.org/path", true),
319
        ("http://example.org", false),
320
        ("http://example.org", true),
321
        ("http://localhost:8008", false),
322
    ] {
323
        let login =
324
            PasswordLogin::new(homeserver, "example", "wordpass").allow_insecure_loopback(loopback);
325
        let refused = Session::login_password(login, sessions.clone()).await;
326
        assert!(
327
            matches!(refused, Err(AccountError::InvalidHomeserver(_))),
328
            "{homeserver:?} got through"
329
        );
330
    }
331
    assert!(!root.exists());
332
}
333
334
fn record(homeserver: &str) -> SessionRecord {
335
    serde_json::from_value(serde_json::json!({
336
        "homeserver": homeserver,
337
        "authentication": {
338
            "type": "matrix",
339
            "session": {
340
                "user_id": "@example:example.org",
341
                "device_id": "DEVICE",
342
                "access_token": "secret",
343
            },
344
        },
345
    }))
346
    .unwrap()
347
}
348
349
fn temporary_path() -> PathBuf {
350
    let nonce = SystemTime::now()
351
        .duration_since(SystemTime::UNIX_EPOCH)
352
        .unwrap()
353
        .as_nanos();
354
    PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(format!("macltok-account-{nonce}"))
355
}