atagen

macltok

public
Star0
Watch0
417da9e719bcfbb8b60e4ff4f1a09f3d6b0d0160
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: tests/account.rs

96fdf90aacd6atagen2026-09-08 08:24 UTC1
#![cfg(feature = "sqlite")]
31ce2453c82fatagen2026-09-24 13:07 UTC2
mod support;
96fdf90aacd6atagen2026-09-08 08:24 UTC3
f12377bce9b9atagen2026-09-26 12:32 UTC4
use std::{path::PathBuf, sync::Arc, time::SystemTime};
31ce2453c82fatagen2026-09-24 13:07 UTC5
713205a95af2atagen2026-09-26 09:24 UTC6
use macltok::{
d8d8a9fe770aatagen2026-10-22 07:44 UTC7
    AccountError, Accounts, EncryptedSessionStore, EngineConfig, Event, OAuthApplicationType,
d8d8a9fe770aatagen2026-10-22 07:44 UTC8
    OAuthLogin, PasswordLogin, Restore, Session, SessionRecord, SessionStore, StoreConfig,
713205a95af2atagen2026-09-26 09:24 UTC9
};
96fdf90aacd6atagen2026-09-08 08:24 UTC10
use matrix_sdk::{ruma::api::client::sync::sync_events::v5, test_utils::mocks::MatrixMockServer};
96fdf90aacd6atagen2026-09-08 08:24 UTC11
96fdf90aacd6atagen2026-09-08 08:24 UTC12
#[tokio::test]
713205a95af2atagen2026-09-26 09:24 UTC13
async fn password_session_round_trips_and_discards_with_its_store() {
96fdf90aacd6atagen2026-09-08 08:24 UTC14
    let server = MatrixMockServer::new().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC15
    server
96fdf90aacd6atagen2026-09-08 08:24 UTC16
        .mock_versions()
96fdf90aacd6atagen2026-09-08 08:24 UTC17
        .with_simplified_sliding_sync()
96fdf90aacd6atagen2026-09-08 08:24 UTC18
        .ok()
96fdf90aacd6atagen2026-09-08 08:24 UTC19
        .mount()
96fdf90aacd6atagen2026-09-08 08:24 UTC20
        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC21
    server.mock_login().ok().mount().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC22
    server
96fdf90aacd6atagen2026-09-08 08:24 UTC23
        .mock_sliding_sync()
96fdf90aacd6atagen2026-09-08 08:24 UTC24
        .ok(v5::Response::new("1".to_owned()))
96fdf90aacd6atagen2026-09-08 08:24 UTC25
        .mount()
96fdf90aacd6atagen2026-09-08 08:24 UTC26
        .await;
713205a95af2atagen2026-09-26 09:24 UTC27
    let root = temporary_path();
713205a95af2atagen2026-09-26 09:24 UTC28
    let store_path = root.join("store");
713205a95af2atagen2026-09-26 09:24 UTC29
    let record_path = root.join("records").join("session");
713205a95af2atagen2026-09-26 09:24 UTC30
    let sessions = || {
713205a95af2atagen2026-09-26 09:24 UTC31
        EncryptedSessionStore::file(
713205a95af2atagen2026-09-26 09:24 UTC32
            &record_path,
713205a95af2atagen2026-09-26 09:24 UTC33
            StoreConfig::sqlite(&store_path, "test store passphrase"),
713205a95af2atagen2026-09-26 09:24 UTC34
        )
713205a95af2atagen2026-09-26 09:24 UTC35
    };
96fdf90aacd6atagen2026-09-08 08:24 UTC36
    let config = EngineConfig::default().with_initial_history_limit(0);
96fdf90aacd6atagen2026-09-08 08:24 UTC37
    let session = Session::login_password(
31ce2453c82fatagen2026-09-24 13:07 UTC38
        PasswordLogin::new(server.uri(), "example", "wordpass")
9c05b1dd327aatagen2026-10-08 11:27 UTC39
            .allow_insecure_loopback(true)
31ce2453c82fatagen2026-09-24 13:07 UTC40
            .with_device_display_name("macltok test"),
f12377bce9b9atagen2026-09-26 12:32 UTC41
        Arc::new(sessions()),
96fdf90aacd6atagen2026-09-08 08:24 UTC42
    )
96fdf90aacd6atagen2026-09-08 08:24 UTC43
    .await
96fdf90aacd6atagen2026-09-08 08:24 UTC44
    .unwrap();
96fdf90aacd6atagen2026-09-08 08:24 UTC45
    let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
96fdf90aacd6atagen2026-09-08 08:24 UTC46
    let running = tokio::spawn(engine.run());
31ce2453c82fatagen2026-09-24 13:07 UTC47
    support::receive_until(&mut events, |event| {
31ce2453c82fatagen2026-09-24 13:07 UTC48
        matches!(event, Event::Authenticated { .. })
31ce2453c82fatagen2026-09-24 13:07 UTC49
    })
31ce2453c82fatagen2026-09-24 13:07 UTC50
    .await;
31ce2453c82fatagen2026-09-24 13:07 UTC51
    support::shut_down(&handle, &mut events, running).await;
713205a95af2atagen2026-09-26 09:24 UTC52
    drop(handle);
96fdf90aacd6atagen2026-09-08 08:24 UTC53
713205a95af2atagen2026-09-26 09:24 UTC54
    let wrong = EncryptedSessionStore::file(
713205a95af2atagen2026-09-26 09:24 UTC55
        &record_path,
713205a95af2atagen2026-09-26 09:24 UTC56
        StoreConfig::sqlite(&store_path, "someone else's passphrase"),
713205a95af2atagen2026-09-26 09:24 UTC57
    );
713205a95af2atagen2026-09-26 09:24 UTC58
    assert!(wrong.load().await.is_err());
713205a95af2atagen2026-09-26 09:24 UTC59
f12377bce9b9atagen2026-09-26 12:32 UTC60
    let sessions = Arc::new(sessions());
f12377bce9b9atagen2026-09-26 12:32 UTC61
    let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {
6f1d6591c10datagen2026-09-26 09:52 UTC62
        panic!("the saved login didn't resume");
6f1d6591c10datagen2026-09-26 09:52 UTC63
    };
96fdf90aacd6atagen2026-09-08 08:24 UTC64
    let (engine, handle, mut events) = session.build(config).await.unwrap();
96fdf90aacd6atagen2026-09-08 08:24 UTC65
    let running = tokio::spawn(engine.run());
31ce2453c82fatagen2026-09-24 13:07 UTC66
    support::receive_until(&mut events, |event| {
31ce2453c82fatagen2026-09-24 13:07 UTC67
        matches!(event, Event::Authenticated { .. })
31ce2453c82fatagen2026-09-24 13:07 UTC68
    })
31ce2453c82fatagen2026-09-24 13:07 UTC69
    .await;
31ce2453c82fatagen2026-09-24 13:07 UTC70
    support::shut_down(&handle, &mut events, running).await;
713205a95af2atagen2026-09-26 09:24 UTC71
    drop(handle);
713205a95af2atagen2026-09-26 09:24 UTC72
713205a95af2atagen2026-09-26 09:24 UTC73
    sessions.discard().await.unwrap();
713205a95af2atagen2026-09-26 09:24 UTC74
    assert!(!record_path.exists());
713205a95af2atagen2026-09-26 09:24 UTC75
    assert!(!store_path.exists());
f12377bce9b9atagen2026-09-26 12:32 UTC76
    assert!(Session::restore(sessions).await.unwrap().is_none());
713205a95af2atagen2026-09-26 09:24 UTC77
    std::fs::remove_dir_all(root).unwrap();
713205a95af2atagen2026-09-26 09:24 UTC78
}
713205a95af2atagen2026-09-26 09:24 UTC79
713205a95af2atagen2026-09-26 09:24 UTC80
#[tokio::test]
6f1d6591c10datagen2026-09-26 09:52 UTC81
async fn a_restart_after_a_soft_logout_still_knows_the_device() {
6f1d6591c10datagen2026-09-26 09:52 UTC82
    let server = MatrixMockServer::new().await;
6f1d6591c10datagen2026-09-26 09:52 UTC83
    server.mock_versions().ok().mount().await;
6f1d6591c10datagen2026-09-26 09:52 UTC84
    server.mock_login().ok().mount().await;
6f1d6591c10datagen2026-09-26 09:52 UTC85
    let root = temporary_path();
6f1d6591c10datagen2026-09-26 09:52 UTC86
    let sessions = || {
6f1d6591c10datagen2026-09-26 09:52 UTC87
        EncryptedSessionStore::file(
6f1d6591c10datagen2026-09-26 09:52 UTC88
            root.join("session"),
6f1d6591c10datagen2026-09-26 09:52 UTC89
            StoreConfig::sqlite(root.join("store"), "test store passphrase"),
6f1d6591c10datagen2026-09-26 09:52 UTC90
        )
6f1d6591c10datagen2026-09-26 09:52 UTC91
    };
6f1d6591c10datagen2026-09-26 09:52 UTC92
    Session::login_password(
9c05b1dd327aatagen2026-10-08 11:27 UTC93
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
f12377bce9b9atagen2026-09-26 12:32 UTC94
        Arc::new(sessions()),
6f1d6591c10datagen2026-09-26 09:52 UTC95
    )
6f1d6591c10datagen2026-09-26 09:52 UTC96
    .await
6f1d6591c10datagen2026-09-26 09:52 UTC97
    .unwrap();
6f1d6591c10datagen2026-09-26 09:52 UTC98
    sessions().soft_log_out().await.unwrap();
6f1d6591c10datagen2026-09-26 09:52 UTC99
f12377bce9b9atagen2026-09-26 12:32 UTC100
    let Some(Restore::SoftLoggedOut(soft)) = Session::restore(Arc::new(sessions())).await.unwrap()
f12377bce9b9atagen2026-09-26 12:32 UTC101
    else {
6f1d6591c10datagen2026-09-26 09:52 UTC102
        panic!("a soft logged out record came back as something else");
6f1d6591c10datagen2026-09-26 09:52 UTC103
    };
6f1d6591c10datagen2026-09-26 09:52 UTC104
    // the device matrix-sdk-test's canned login response hands out
6f1d6591c10datagen2026-09-26 09:52 UTC105
    assert_eq!(soft.device_id, "GHTYAJCE");
f12377bce9b9atagen2026-09-26 12:32 UTC106
    Session::login_password(soft.password_login("wordpass"), Arc::new(sessions()))
6f1d6591c10datagen2026-09-26 09:52 UTC107
        .await
6f1d6591c10datagen2026-09-26 09:52 UTC108
        .unwrap();
6f1d6591c10datagen2026-09-26 09:52 UTC109
    let requests = server.server().received_requests().await.unwrap();
6f1d6591c10datagen2026-09-26 09:52 UTC110
    let relogin: serde_json::Value = requests
6f1d6591c10datagen2026-09-26 09:52 UTC111
        .iter()
6f1d6591c10datagen2026-09-26 09:52 UTC112
        .rfind(|request| request.url.path().ends_with("/login"))
6f1d6591c10datagen2026-09-26 09:52 UTC113
        .unwrap()
6f1d6591c10datagen2026-09-26 09:52 UTC114
        .body_json()
6f1d6591c10datagen2026-09-26 09:52 UTC115
        .unwrap();
6f1d6591c10datagen2026-09-26 09:52 UTC116
    assert_eq!(relogin["device_id"], "GHTYAJCE");
6f1d6591c10datagen2026-09-26 09:52 UTC117
    assert!(matches!(
f12377bce9b9atagen2026-09-26 12:32 UTC118
        Session::restore(Arc::new(sessions())).await.unwrap(),
6f1d6591c10datagen2026-09-26 09:52 UTC119
        Some(Restore::Resumed(_))
6f1d6591c10datagen2026-09-26 09:52 UTC120
    ));
6f1d6591c10datagen2026-09-26 09:52 UTC121
    std::fs::remove_dir_all(root).unwrap();
6f1d6591c10datagen2026-09-26 09:52 UTC122
}
6f1d6591c10datagen2026-09-26 09:52 UTC123
6f1d6591c10datagen2026-09-26 09:52 UTC124
#[tokio::test]
a250c7fa43fbatagen2026-09-26 13:35 UTC125
async fn a_login_whose_first_save_fails_can_still_be_saved() {
a250c7fa43fbatagen2026-09-26 13:35 UTC126
    let server = MatrixMockServer::new().await;
a250c7fa43fbatagen2026-09-26 13:35 UTC127
    server.mock_versions().ok().mount().await;
a250c7fa43fbatagen2026-09-26 13:35 UTC128
    server.mock_login().ok().mount().await;
a250c7fa43fbatagen2026-09-26 13:35 UTC129
    let root = temporary_path();
a250c7fa43fbatagen2026-09-26 13:35 UTC130
    let records = root.join("records");
a250c7fa43fbatagen2026-09-26 13:35 UTC131
    std::fs::create_dir_all(&records).unwrap();
a250c7fa43fbatagen2026-09-26 13:35 UTC132
    let sessions = || {
a250c7fa43fbatagen2026-09-26 13:35 UTC133
        EncryptedSessionStore::file(
a250c7fa43fbatagen2026-09-26 13:35 UTC134
            records.join("session"),
a250c7fa43fbatagen2026-09-26 13:35 UTC135
            StoreConfig::sqlite(root.join("store"), "test store passphrase"),
a250c7fa43fbatagen2026-09-26 13:35 UTC136
        )
a250c7fa43fbatagen2026-09-26 13:35 UTC137
    };
a250c7fa43fbatagen2026-09-26 13:35 UTC138
    let writable = std::fs::metadata(&records).unwrap().permissions();
a250c7fa43fbatagen2026-09-26 13:35 UTC139
    let mut read_only = writable.clone();
a250c7fa43fbatagen2026-09-26 13:35 UTC140
    read_only.set_readonly(true);
a250c7fa43fbatagen2026-09-26 13:35 UTC141
    std::fs::set_permissions(&records, read_only).unwrap();
a250c7fa43fbatagen2026-09-26 13:35 UTC142
a250c7fa43fbatagen2026-09-26 13:35 UTC143
    let login = Session::login_password(
9c05b1dd327aatagen2026-10-08 11:27 UTC144
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
a250c7fa43fbatagen2026-09-26 13:35 UTC145
        Arc::new(sessions()),
a250c7fa43fbatagen2026-09-26 13:35 UTC146
    )
a250c7fa43fbatagen2026-09-26 13:35 UTC147
    .await;
a250c7fa43fbatagen2026-09-26 13:35 UTC148
    let Err(AccountError::Unsaved { session, .. }) = login else {
a250c7fa43fbatagen2026-09-26 13:35 UTC149
        panic!("a login that couldn't save its record didn't hand the session back");
a250c7fa43fbatagen2026-09-26 13:35 UTC150
    };
a250c7fa43fbatagen2026-09-26 13:35 UTC151
    std::fs::set_permissions(&records, writable).unwrap();
a250c7fa43fbatagen2026-09-26 13:35 UTC152
    drop(session.save().await.unwrap());
a250c7fa43fbatagen2026-09-26 13:35 UTC153
    assert!(matches!(
a250c7fa43fbatagen2026-09-26 13:35 UTC154
        Session::restore(Arc::new(sessions())).await.unwrap(),
a250c7fa43fbatagen2026-09-26 13:35 UTC155
        Some(Restore::Resumed(_))
a250c7fa43fbatagen2026-09-26 13:35 UTC156
    ));
a250c7fa43fbatagen2026-09-26 13:35 UTC157
    std::fs::remove_dir_all(root).unwrap();
a250c7fa43fbatagen2026-09-26 13:35 UTC158
}
a250c7fa43fbatagen2026-09-26 13:35 UTC159
a250c7fa43fbatagen2026-09-26 13:35 UTC160
#[tokio::test]
a250c7fa43fbatagen2026-09-26 13:35 UTC161
async fn a_store_takes_one_session_at_a_time() {
a250c7fa43fbatagen2026-09-26 13:35 UTC162
    let server = MatrixMockServer::new().await;
a250c7fa43fbatagen2026-09-26 13:35 UTC163
    server.mock_versions().ok().mount().await;
a250c7fa43fbatagen2026-09-26 13:35 UTC164
    server.mock_login().ok().mount().await;
a250c7fa43fbatagen2026-09-26 13:35 UTC165
    let root = temporary_path();
a250c7fa43fbatagen2026-09-26 13:35 UTC166
    let sessions = || {
a250c7fa43fbatagen2026-09-26 13:35 UTC167
        EncryptedSessionStore::file(
a250c7fa43fbatagen2026-09-26 13:35 UTC168
            root.join("session"),
a250c7fa43fbatagen2026-09-26 13:35 UTC169
            StoreConfig::sqlite(root.join("store"), "test store passphrase"),
a250c7fa43fbatagen2026-09-26 13:35 UTC170
        )
a250c7fa43fbatagen2026-09-26 13:35 UTC171
    };
a250c7fa43fbatagen2026-09-26 13:35 UTC172
    let (engine, handle, events) = Session::login_password(
9c05b1dd327aatagen2026-10-08 11:27 UTC173
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
a250c7fa43fbatagen2026-09-26 13:35 UTC174
        Arc::new(sessions()),
a250c7fa43fbatagen2026-09-26 13:35 UTC175
    )
a250c7fa43fbatagen2026-09-26 13:35 UTC176
    .await
a250c7fa43fbatagen2026-09-26 13:35 UTC177
    .unwrap()
a250c7fa43fbatagen2026-09-26 13:35 UTC178
    .build(EngineConfig::default())
a250c7fa43fbatagen2026-09-26 13:35 UTC179
    .await
a250c7fa43fbatagen2026-09-26 13:35 UTC180
    .unwrap();
a250c7fa43fbatagen2026-09-26 13:35 UTC181
a250c7fa43fbatagen2026-09-26 13:35 UTC182
    assert!(matches!(
a250c7fa43fbatagen2026-09-26 13:35 UTC183
        Session::restore(Arc::new(sessions())).await,
a250c7fa43fbatagen2026-09-26 13:35 UTC184
        Err(AccountError::StoreInUse)
a250c7fa43fbatagen2026-09-26 13:35 UTC185
    ));
a250c7fa43fbatagen2026-09-26 13:35 UTC186
    drop((engine, events));
a250c7fa43fbatagen2026-09-26 13:35 UTC187
    assert!(matches!(
a250c7fa43fbatagen2026-09-26 13:35 UTC188
        Session::restore(Arc::new(sessions())).await,
a250c7fa43fbatagen2026-09-26 13:35 UTC189
        Err(AccountError::StoreInUse)
a250c7fa43fbatagen2026-09-26 13:35 UTC190
    ));
a250c7fa43fbatagen2026-09-26 13:35 UTC191
    drop(handle);
a250c7fa43fbatagen2026-09-26 13:35 UTC192
    let resumed = Session::restore(Arc::new(sessions())).await.unwrap();
a250c7fa43fbatagen2026-09-26 13:35 UTC193
    assert!(matches!(resumed, Some(Restore::Resumed(_))));
a250c7fa43fbatagen2026-09-26 13:35 UTC194
    drop(resumed);
a250c7fa43fbatagen2026-09-26 13:35 UTC195
    sessions().discard().await.unwrap();
a250c7fa43fbatagen2026-09-26 13:35 UTC196
    assert!(!root.join("store").exists());
a250c7fa43fbatagen2026-09-26 13:35 UTC197
    std::fs::remove_dir_all(root).unwrap();
a250c7fa43fbatagen2026-09-26 13:35 UTC198
}
a250c7fa43fbatagen2026-09-26 13:35 UTC199
a250c7fa43fbatagen2026-09-26 13:35 UTC200
#[tokio::test]
713205a95af2atagen2026-09-26 09:24 UTC201
async fn a_torn_or_failed_save_leaves_the_old_record() {
713205a95af2atagen2026-09-26 09:24 UTC202
    let root = temporary_path();
713205a95af2atagen2026-09-26 09:24 UTC203
    let record_path = root.join("session");
713205a95af2atagen2026-09-26 09:24 UTC204
    let sessions = || {
713205a95af2atagen2026-09-26 09:24 UTC205
        EncryptedSessionStore::file(
713205a95af2atagen2026-09-26 09:24 UTC206
            &record_path,
713205a95af2atagen2026-09-26 09:24 UTC207
            StoreConfig::sqlite(root.join("store"), "test store passphrase"),
713205a95af2atagen2026-09-26 09:24 UTC208
        )
713205a95af2atagen2026-09-26 09:24 UTC209
    };
6db49c0ecf71atagen2026-09-26 12:41 UTC210
    let temporary = root.join("session.1.0.tmp");
713205a95af2atagen2026-09-26 09:24 UTC211
    sessions()
713205a95af2atagen2026-09-26 09:24 UTC212
        .save(&record("https://old.example"))
713205a95af2atagen2026-09-26 09:24 UTC213
        .await
713205a95af2atagen2026-09-26 09:24 UTC214
        .unwrap();
713205a95af2atagen2026-09-26 09:24 UTC215
713205a95af2atagen2026-09-26 09:24 UTC216
    std::fs::write(&temporary, b"half a rec").unwrap();
713205a95af2atagen2026-09-26 09:24 UTC217
    let loaded = sessions().load().await.unwrap().unwrap();
713205a95af2atagen2026-09-26 09:24 UTC218
    assert_eq!(loaded.homeserver(), "https://old.example");
713205a95af2atagen2026-09-26 09:24 UTC219
6db49c0ecf71atagen2026-09-26 12:41 UTC220
    let writable = std::fs::metadata(&root).unwrap().permissions();
6db49c0ecf71atagen2026-09-26 12:41 UTC221
    let mut read_only = writable.clone();
6db49c0ecf71atagen2026-09-26 12:41 UTC222
    read_only.set_readonly(true);
6db49c0ecf71atagen2026-09-26 12:41 UTC223
    std::fs::set_permissions(&root, read_only).unwrap();
713205a95af2atagen2026-09-26 09:24 UTC224
    assert!(
713205a95af2atagen2026-09-26 09:24 UTC225
        sessions()
713205a95af2atagen2026-09-26 09:24 UTC226
            .save(&record("https://new.example"))
713205a95af2atagen2026-09-26 09:24 UTC227
            .await
713205a95af2atagen2026-09-26 09:24 UTC228
            .is_err()
713205a95af2atagen2026-09-26 09:24 UTC229
    );
713205a95af2atagen2026-09-26 09:24 UTC230
    let loaded = sessions().load().await.unwrap().unwrap();
713205a95af2atagen2026-09-26 09:24 UTC231
    assert_eq!(loaded.homeserver(), "https://old.example");
713205a95af2atagen2026-09-26 09:24 UTC232
6db49c0ecf71atagen2026-09-26 12:41 UTC233
    std::fs::set_permissions(&root, writable).unwrap();
713205a95af2atagen2026-09-26 09:24 UTC234
    std::fs::remove_dir_all(root).unwrap();
713205a95af2atagen2026-09-26 09:24 UTC235
}
96fdf90aacd6atagen2026-09-08 08:24 UTC236
f12377bce9b9atagen2026-09-26 12:32 UTC237
#[tokio::test]
f12377bce9b9atagen2026-09-26 12:32 UTC238
async fn a_refresh_is_saved_before_anyone_hears_of_it() {
f12377bce9b9atagen2026-09-26 12:32 UTC239
    use matrix_sdk_test::test_json;
f12377bce9b9atagen2026-09-26 12:32 UTC240
    use wiremock::{
f12377bce9b9atagen2026-09-26 12:32 UTC241
        Mock, ResponseTemplate,
f12377bce9b9atagen2026-09-26 12:32 UTC242
        matchers::{header, method, path, path_regex},
f12377bce9b9atagen2026-09-26 12:32 UTC243
    };
f12377bce9b9atagen2026-09-26 12:32 UTC244
f12377bce9b9atagen2026-09-26 12:32 UTC245
    let server = MatrixMockServer::new().await;
f12377bce9b9atagen2026-09-26 12:32 UTC246
    server.mock_versions().ok().mount().await;
f12377bce9b9atagen2026-09-26 12:32 UTC247
    server
f12377bce9b9atagen2026-09-26 12:32 UTC248
        .mock_login()
f12377bce9b9atagen2026-09-26 12:32 UTC249
        .respond_with(
f12377bce9b9atagen2026-09-26 12:32 UTC250
            ResponseTemplate::new(200).set_body_json(&*test_json::LOGIN_WITH_REFRESH_TOKEN),
f12377bce9b9atagen2026-09-26 12:32 UTC251
        )
f12377bce9b9atagen2026-09-26 12:32 UTC252
        .mount()
f12377bce9b9atagen2026-09-26 12:32 UTC253
        .await;
f12377bce9b9atagen2026-09-26 12:32 UTC254
    Mock::given(method("POST"))
f12377bce9b9atagen2026-09-26 12:32 UTC255
        .and(path_regex("sync"))
f12377bce9b9atagen2026-09-26 12:32 UTC256
        .and(header("authorization", "Bearer abc123"))
f12377bce9b9atagen2026-09-26 12:32 UTC257
        .respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({
f12377bce9b9atagen2026-09-26 12:32 UTC258
            "errcode": "M_UNKNOWN_TOKEN",
f12377bce9b9atagen2026-09-26 12:32 UTC259
            "error": "expired",
f12377bce9b9atagen2026-09-26 12:32 UTC260
            "soft_logout": false,
f12377bce9b9atagen2026-09-26 12:32 UTC261
        })))
f12377bce9b9atagen2026-09-26 12:32 UTC262
        .with_priority(1)
f12377bce9b9atagen2026-09-26 12:32 UTC263
        .mount(server.server())
f12377bce9b9atagen2026-09-26 12:32 UTC264
        .await;
f12377bce9b9atagen2026-09-26 12:32 UTC265
    Mock::given(method("POST"))
f12377bce9b9atagen2026-09-26 12:32 UTC266
        .and(path("/_matrix/client/v3/refresh"))
f12377bce9b9atagen2026-09-26 12:32 UTC267
        .respond_with(
f12377bce9b9atagen2026-09-26 12:32 UTC268
            ResponseTemplate::new(200).set_body_json(&*test_json::REFRESH_TOKEN_WITH_REFRESH_TOKEN),
f12377bce9b9atagen2026-09-26 12:32 UTC269
        )
f12377bce9b9atagen2026-09-26 12:32 UTC270
        .mount(server.server())
f12377bce9b9atagen2026-09-26 12:32 UTC271
        .await;
f12377bce9b9atagen2026-09-26 12:32 UTC272
    server
f12377bce9b9atagen2026-09-26 12:32 UTC273
        .mock_sliding_sync()
f12377bce9b9atagen2026-09-26 12:32 UTC274
        .ok(v5::Response::new("1".to_owned()))
f12377bce9b9atagen2026-09-26 12:32 UTC275
        .mount()
f12377bce9b9atagen2026-09-26 12:32 UTC276
        .await;
f12377bce9b9atagen2026-09-26 12:32 UTC277
    let root = temporary_path();
f12377bce9b9atagen2026-09-26 12:32 UTC278
    let sessions = Arc::new(EncryptedSessionStore::file(
f12377bce9b9atagen2026-09-26 12:32 UTC279
        root.join("session"),
f12377bce9b9atagen2026-09-26 12:32 UTC280
        StoreConfig::sqlite(root.join("store"), "test store passphrase"),
f12377bce9b9atagen2026-09-26 12:32 UTC281
    ));
f12377bce9b9atagen2026-09-26 12:32 UTC282
    let session = Session::login_password(
9c05b1dd327aatagen2026-10-08 11:27 UTC283
        PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),
f12377bce9b9atagen2026-09-26 12:32 UTC284
        sessions.clone(),
f12377bce9b9atagen2026-09-26 12:32 UTC285
    )
f12377bce9b9atagen2026-09-26 12:32 UTC286
    .await
f12377bce9b9atagen2026-09-26 12:32 UTC287
    .unwrap();
f12377bce9b9atagen2026-09-26 12:32 UTC288
    let (engine, handle, mut events) = session
f12377bce9b9atagen2026-09-26 12:32 UTC289
        .build(EngineConfig::default().with_initial_history_limit(0))
f12377bce9b9atagen2026-09-26 12:32 UTC290
        .await
f12377bce9b9atagen2026-09-26 12:32 UTC291
        .unwrap();
f12377bce9b9atagen2026-09-26 12:32 UTC292
    let running = tokio::spawn(engine.run());
f12377bce9b9atagen2026-09-26 12:32 UTC293
    support::receive_until(&mut events, |event| {
f12377bce9b9atagen2026-09-26 12:32 UTC294
        matches!(event, Event::SessionTokensChanged)
f12377bce9b9atagen2026-09-26 12:32 UTC295
    })
f12377bce9b9atagen2026-09-26 12:32 UTC296
    .await;
f12377bce9b9atagen2026-09-26 12:32 UTC297
    let saved = serde_json::to_value(sessions.load().await.unwrap().unwrap()).unwrap();
f12377bce9b9atagen2026-09-26 12:32 UTC298
    assert_eq!(saved["authentication"]["session"]["refresh_token"], "wxyz");
f12377bce9b9atagen2026-09-26 12:32 UTC299
    support::shut_down(&handle, &mut events, running).await;
f12377bce9b9atagen2026-09-26 12:32 UTC300
    drop(handle);
f12377bce9b9atagen2026-09-26 12:32 UTC301
    std::fs::remove_dir_all(root).unwrap();
f12377bce9b9atagen2026-09-26 12:32 UTC302
}
f12377bce9b9atagen2026-09-26 12:32 UTC303
9c05b1dd327aatagen2026-10-08 11:27 UTC304
#[tokio::test]
cb0668d30c14atagen2026-10-08 11:34 UTC305
async fn logging_in_again_after_a_soft_logout_keeps_the_device_and_its_store() {
cb0668d30c14atagen2026-10-08 11:34 UTC306
    let server = MatrixMockServer::new().await;
cb0668d30c14atagen2026-10-08 11:34 UTC307
    server.mock_versions().ok().mount().await;
cb0668d30c14atagen2026-10-08 11:34 UTC308
    server.mock_login().ok().mount().await;
cb0668d30c14atagen2026-10-08 11:34 UTC309
    let root = temporary_path();
cb0668d30c14atagen2026-10-08 11:34 UTC310
    let accounts = Accounts::sqlite(&root, "test store passphrase");
cb0668d30c14atagen2026-10-08 11:34 UTC311
    let login = |username: &str| {
cb0668d30c14atagen2026-10-08 11:34 UTC312
        PasswordLogin::new(server.uri(), username, "wordpass").allow_insecure_loopback(true)
cb0668d30c14atagen2026-10-08 11:34 UTC313
    };
cb0668d30c14atagen2026-10-08 11:34 UTC314
    let device_ids = || async {
cb0668d30c14atagen2026-10-08 11:34 UTC315
        let requests = server.server().received_requests().await.unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC316
        requests
cb0668d30c14atagen2026-10-08 11:34 UTC317
            .iter()
cb0668d30c14atagen2026-10-08 11:34 UTC318
            .filter(|request| request.url.path().ends_with("/login"))
cb0668d30c14atagen2026-10-08 11:34 UTC319
            .map(|request| request.body_json::<serde_json::Value>().unwrap()["device_id"].clone())
cb0668d30c14atagen2026-10-08 11:34 UTC320
            .collect::<Vec<_>>()
cb0668d30c14atagen2026-10-08 11:34 UTC321
    };
cb0668d30c14atagen2026-10-08 11:34 UTC322
cb0668d30c14atagen2026-10-08 11:34 UTC323
    let (first, session) = accounts
cb0668d30c14atagen2026-10-08 11:34 UTC324
        .login_password(login("cheeky_monkey"))
cb0668d30c14atagen2026-10-08 11:34 UTC325
        .await
cb0668d30c14atagen2026-10-08 11:34 UTC326
        .unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC327
    drop(session);
cb0668d30c14atagen2026-10-08 11:34 UTC328
    assert!(matches!(
cb0668d30c14atagen2026-10-08 11:34 UTC329
        accounts.login_password(login("cheeky_monkey")).await,
cb0668d30c14atagen2026-10-08 11:34 UTC330
        Err(AccountError::AlreadyLoggedIn(_))
cb0668d30c14atagen2026-10-08 11:34 UTC331
    ));
cb0668d30c14atagen2026-10-08 11:34 UTC332
cb0668d30c14atagen2026-10-08 11:34 UTC333
    first.sessions().soft_log_out().await.unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC334
    let (resumed, session) = accounts
cb0668d30c14atagen2026-10-08 11:34 UTC335
        .login_password(login(" Cheeky_Monkey "))
cb0668d30c14atagen2026-10-08 11:34 UTC336
        .await
cb0668d30c14atagen2026-10-08 11:34 UTC337
        .unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC338
    drop(session);
cb0668d30c14atagen2026-10-08 11:34 UTC339
    assert_eq!(resumed.id(), first.id());
cb0668d30c14atagen2026-10-08 11:34 UTC340
    assert_eq!(device_ids().await.last().unwrap(), "GHTYAJCE");
cb0668d30c14atagen2026-10-08 11:34 UTC341
cb0668d30c14atagen2026-10-08 11:34 UTC342
    resumed.sessions().soft_log_out().await.unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC343
    let (other, session) = accounts
cb0668d30c14atagen2026-10-08 11:34 UTC344
        .login_password(login("someone_else"))
cb0668d30c14atagen2026-10-08 11:34 UTC345
        .await
cb0668d30c14atagen2026-10-08 11:34 UTC346
        .unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC347
    drop(session);
cb0668d30c14atagen2026-10-08 11:34 UTC348
    assert_ne!(other.id(), first.id());
cb0668d30c14atagen2026-10-08 11:34 UTC349
    assert!(device_ids().await.last().unwrap().is_null());
cb0668d30c14atagen2026-10-08 11:34 UTC350
    assert_eq!(accounts.list().await.unwrap().len(), 2);
cb0668d30c14atagen2026-10-08 11:34 UTC351
    std::fs::remove_dir_all(root).unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC352
}
cb0668d30c14atagen2026-10-08 11:34 UTC353
cb0668d30c14atagen2026-10-08 11:34 UTC354
#[tokio::test]
cb0668d30c14atagen2026-10-08 11:34 UTC355
async fn a_refused_login_leaves_no_account_and_says_why() {
cb0668d30c14atagen2026-10-08 11:34 UTC356
    use wiremock::ResponseTemplate;
cb0668d30c14atagen2026-10-08 11:34 UTC357
cb0668d30c14atagen2026-10-08 11:34 UTC358
    let server = MatrixMockServer::new().await;
cb0668d30c14atagen2026-10-08 11:34 UTC359
    server.mock_versions().ok().mount().await;
cb0668d30c14atagen2026-10-08 11:34 UTC360
    server
cb0668d30c14atagen2026-10-08 11:34 UTC361
        .mock_login()
cb0668d30c14atagen2026-10-08 11:34 UTC362
        .respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({
cb0668d30c14atagen2026-10-08 11:34 UTC363
            "errcode": "M_FORBIDDEN",
cb0668d30c14atagen2026-10-08 11:34 UTC364
            "error": "Invalid username or password",
cb0668d30c14atagen2026-10-08 11:34 UTC365
        })))
cb0668d30c14atagen2026-10-08 11:34 UTC366
        .mount()
cb0668d30c14atagen2026-10-08 11:34 UTC367
        .await;
cb0668d30c14atagen2026-10-08 11:34 UTC368
    let root = temporary_path();
cb0668d30c14atagen2026-10-08 11:34 UTC369
    let accounts = Accounts::sqlite(&root, "test store passphrase");
cb0668d30c14atagen2026-10-08 11:34 UTC370
    let Err(refused) = accounts
cb0668d30c14atagen2026-10-08 11:34 UTC371
        .login_password(
cb0668d30c14atagen2026-10-08 11:34 UTC372
            PasswordLogin::new(server.uri(), "example", "wrong").allow_insecure_loopback(true),
cb0668d30c14atagen2026-10-08 11:34 UTC373
        )
cb0668d30c14atagen2026-10-08 11:34 UTC374
        .await
cb0668d30c14atagen2026-10-08 11:34 UTC375
    else {
cb0668d30c14atagen2026-10-08 11:34 UTC376
        panic!("a refused login went through");
cb0668d30c14atagen2026-10-08 11:34 UTC377
    };
cb0668d30c14atagen2026-10-08 11:34 UTC378
    let server_error = refused.server().unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC379
    assert_eq!(server_error.errcode, "M_FORBIDDEN");
cb0668d30c14atagen2026-10-08 11:34 UTC380
    assert_eq!(server_error.message, "Invalid username or password");
cb0668d30c14atagen2026-10-08 11:34 UTC381
    assert!(accounts.list().await.unwrap().is_empty());
cb0668d30c14atagen2026-10-08 11:34 UTC382
    assert_eq!(std::fs::read_dir(&root).unwrap().count(), 0);
cb0668d30c14atagen2026-10-08 11:34 UTC383
    std::fs::remove_dir_all(root).unwrap();
cb0668d30c14atagen2026-10-08 11:34 UTC384
}
cb0668d30c14atagen2026-10-08 11:34 UTC385
cb0668d30c14atagen2026-10-08 11:34 UTC386
#[tokio::test]
9c05b1dd327aatagen2026-10-08 11:27 UTC387
async fn a_homeserver_that_cant_be_one_is_refused_before_anything_is_sent() {
9c05b1dd327aatagen2026-10-08 11:27 UTC388
    let root = temporary_path();
9c05b1dd327aatagen2026-10-08 11:27 UTC389
    let sessions: Arc<dyn SessionStore> = Arc::new(EncryptedSessionStore::file(
9c05b1dd327aatagen2026-10-08 11:27 UTC390
        root.join("session"),
9c05b1dd327aatagen2026-10-08 11:27 UTC391
        StoreConfig::sqlite(root.join("store"), "test store passphrase"),
9c05b1dd327aatagen2026-10-08 11:27 UTC392
    ));
9c05b1dd327aatagen2026-10-08 11:27 UTC393
    for (homeserver, loopback) in [
9c05b1dd327aatagen2026-10-08 11:27 UTC394
        ("", true),
9c05b1dd327aatagen2026-10-08 11:27 UTC395
        ("  ", true),
9c05b1dd327aatagen2026-10-08 11:27 UTC396
        ("example .org", true),
9c05b1dd327aatagen2026-10-08 11:27 UTC397
        ("ftp://example.org", true),
9c05b1dd327aatagen2026-10-08 11:27 UTC398
        ("https://user@example.org", true),
9c05b1dd327aatagen2026-10-08 11:27 UTC399
        ("https://", true),
9c05b1dd327aatagen2026-10-08 11:27 UTC400
        ("example.org/path", true),
9c05b1dd327aatagen2026-10-08 11:27 UTC401
        ("http://example.org", false),
9c05b1dd327aatagen2026-10-08 11:27 UTC402
        ("http://example.org", true),
9c05b1dd327aatagen2026-10-08 11:27 UTC403
        ("http://localhost:8008", false),
9c05b1dd327aatagen2026-10-08 11:27 UTC404
    ] {
9c05b1dd327aatagen2026-10-08 11:27 UTC405
        let login =
9c05b1dd327aatagen2026-10-08 11:27 UTC406
            PasswordLogin::new(homeserver, "example", "wordpass").allow_insecure_loopback(loopback);
9c05b1dd327aatagen2026-10-08 11:27 UTC407
        let refused = Session::login_password(login, sessions.clone()).await;
9c05b1dd327aatagen2026-10-08 11:27 UTC408
        assert!(
9c05b1dd327aatagen2026-10-08 11:27 UTC409
            matches!(refused, Err(AccountError::InvalidHomeserver(_))),
9c05b1dd327aatagen2026-10-08 11:27 UTC410
            "{homeserver:?} got through"
9c05b1dd327aatagen2026-10-08 11:27 UTC411
        );
9c05b1dd327aatagen2026-10-08 11:27 UTC412
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC413
    assert!(!root.exists());
9c05b1dd327aatagen2026-10-08 11:27 UTC414
}
9c05b1dd327aatagen2026-10-08 11:27 UTC415
713205a95af2atagen2026-09-26 09:24 UTC416
fn record(homeserver: &str) -> SessionRecord {
713205a95af2atagen2026-09-26 09:24 UTC417
    serde_json::from_value(serde_json::json!({
713205a95af2atagen2026-09-26 09:24 UTC418
        "homeserver": homeserver,
713205a95af2atagen2026-09-26 09:24 UTC419
        "authentication": {
713205a95af2atagen2026-09-26 09:24 UTC420
            "type": "matrix",
713205a95af2atagen2026-09-26 09:24 UTC421
            "session": {
713205a95af2atagen2026-09-26 09:24 UTC422
                "user_id": "@example:example.org",
713205a95af2atagen2026-09-26 09:24 UTC423
                "device_id": "DEVICE",
713205a95af2atagen2026-09-26 09:24 UTC424
                "access_token": "secret",
713205a95af2atagen2026-09-26 09:24 UTC425
            },
713205a95af2atagen2026-09-26 09:24 UTC426
        },
713205a95af2atagen2026-09-26 09:24 UTC427
    }))
713205a95af2atagen2026-09-26 09:24 UTC428
    .unwrap()
96fdf90aacd6atagen2026-09-08 08:24 UTC429
}
96fdf90aacd6atagen2026-09-08 08:24 UTC430
713205a95af2atagen2026-09-26 09:24 UTC431
fn temporary_path() -> PathBuf {
96fdf90aacd6atagen2026-09-08 08:24 UTC432
    let nonce = SystemTime::now()
96fdf90aacd6atagen2026-09-08 08:24 UTC433
        .duration_since(SystemTime::UNIX_EPOCH)
96fdf90aacd6atagen2026-09-08 08:24 UTC434
        .unwrap()
96fdf90aacd6atagen2026-09-08 08:24 UTC435
        .as_nanos();
713205a95af2atagen2026-09-26 09:24 UTC436
    PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(format!("macltok-account-{nonce}"))
96fdf90aacd6atagen2026-09-08 08:24 UTC437
}
d8d8a9fe770aatagen2026-10-22 07:44 UTC438
d8d8a9fe770aatagen2026-10-22 07:44 UTC439
#[tokio::test]
d8d8a9fe770aatagen2026-10-22 07:44 UTC440
async fn an_oauth_login_survives_the_page_going_away_for_its_redirect() {
d8d8a9fe770aatagen2026-10-22 07:44 UTC441
    let server = MatrixMockServer::new().await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC442
    server
d8d8a9fe770aatagen2026-10-22 07:44 UTC443
        .mock_versions()
d8d8a9fe770aatagen2026-10-22 07:44 UTC444
        .with_simplified_sliding_sync()
d8d8a9fe770aatagen2026-10-22 07:44 UTC445
        .ok()
d8d8a9fe770aatagen2026-10-22 07:44 UTC446
        .mount()
d8d8a9fe770aatagen2026-10-22 07:44 UTC447
        .await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC448
    server.mock_who_am_i().ok().mount().await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC449
    let oauth = server.oauth();
d8d8a9fe770aatagen2026-10-22 07:44 UTC450
    oauth.mock_server_metadata().ok().mount().await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC451
    oauth.mock_registration().ok().mount().await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC452
    oauth.mock_token().ok().mount().await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC453
    let root = temporary_path();
d8d8a9fe770aatagen2026-10-22 07:44 UTC454
    let accounts = Accounts::sqlite(&root, "test store passphrase");
d8d8a9fe770aatagen2026-10-22 07:44 UTC455
    let redirect_uri = "http://127.0.0.1/callback";
d8d8a9fe770aatagen2026-10-22 07:44 UTC456
d8d8a9fe770aatagen2026-10-22 07:44 UTC457
    let (account, pending) = accounts
d8d8a9fe770aatagen2026-10-22 07:44 UTC458
        .begin_oauth(
d8d8a9fe770aatagen2026-10-22 07:44 UTC459
            OAuthLogin::new(
d8d8a9fe770aatagen2026-10-22 07:44 UTC460
                server.uri(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC461
                redirect_uri,
d8d8a9fe770aatagen2026-10-22 07:44 UTC462
                "http://127.0.0.1/",
d8d8a9fe770aatagen2026-10-22 07:44 UTC463
                OAuthApplicationType::Native,
d8d8a9fe770aatagen2026-10-22 07:44 UTC464
            )
d8d8a9fe770aatagen2026-10-22 07:44 UTC465
            .allow_insecure_loopback(true),
d8d8a9fe770aatagen2026-10-22 07:44 UTC466
        )
d8d8a9fe770aatagen2026-10-22 07:44 UTC467
        .await
d8d8a9fe770aatagen2026-10-22 07:44 UTC468
        .unwrap();
d8d8a9fe770aatagen2026-10-22 07:44 UTC469
    let state = url::Url::parse(pending.authorization_url())
d8d8a9fe770aatagen2026-10-22 07:44 UTC470
        .unwrap()
d8d8a9fe770aatagen2026-10-22 07:44 UTC471
        .query_pairs()
d8d8a9fe770aatagen2026-10-22 07:44 UTC472
        .find(|(key, _)| key == "state")
d8d8a9fe770aatagen2026-10-22 07:44 UTC473
        .map(|(_, state)| state.into_owned())
d8d8a9fe770aatagen2026-10-22 07:44 UTC474
        .unwrap();
d8d8a9fe770aatagen2026-10-22 07:44 UTC475
    drop(pending);
d8d8a9fe770aatagen2026-10-22 07:44 UTC476
d8d8a9fe770aatagen2026-10-22 07:44 UTC477
    assert!(matches!(
d8d8a9fe770aatagen2026-10-22 07:44 UTC478
        account.restore().await.unwrap(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC479
        Some(Restore::OAuthPending)
d8d8a9fe770aatagen2026-10-22 07:44 UTC480
    ));
d8d8a9fe770aatagen2026-10-22 07:44 UTC481
    assert_eq!(
d8d8a9fe770aatagen2026-10-22 07:44 UTC482
        accounts.list().await.unwrap().len(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC483
        1,
d8d8a9fe770aatagen2026-10-22 07:44 UTC484
        "the redirect's store was swept"
d8d8a9fe770aatagen2026-10-22 07:44 UTC485
    );
d8d8a9fe770aatagen2026-10-22 07:44 UTC486
    assert!(matches!(
d8d8a9fe770aatagen2026-10-22 07:44 UTC487
        accounts
d8d8a9fe770aatagen2026-10-22 07:44 UTC488
            .finish_oauth(&format!("{redirect_uri}?code=42&state=someone-else"))
d8d8a9fe770aatagen2026-10-22 07:44 UTC489
            .await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC490
        Err(AccountError::NoPendingOAuth)
d8d8a9fe770aatagen2026-10-22 07:44 UTC491
    ));
d8d8a9fe770aatagen2026-10-22 07:44 UTC492
d8d8a9fe770aatagen2026-10-22 07:44 UTC493
    let (finished, session) = accounts
d8d8a9fe770aatagen2026-10-22 07:44 UTC494
        .finish_oauth(&format!("{redirect_uri}?code=42&state={state}"))
d8d8a9fe770aatagen2026-10-22 07:44 UTC495
        .await
d8d8a9fe770aatagen2026-10-22 07:44 UTC496
        .unwrap();
d8d8a9fe770aatagen2026-10-22 07:44 UTC497
    assert_eq!(finished.id(), account.id());
d8d8a9fe770aatagen2026-10-22 07:44 UTC498
    drop(session);
d8d8a9fe770aatagen2026-10-22 07:44 UTC499
    assert!(matches!(
d8d8a9fe770aatagen2026-10-22 07:44 UTC500
        account.restore().await.unwrap(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC501
        Some(Restore::Resumed(_))
d8d8a9fe770aatagen2026-10-22 07:44 UTC502
    ));
d8d8a9fe770aatagen2026-10-22 07:44 UTC503
}