atagen

macltok

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