diff --git a/crates/didbot/tests/scenarios/restart.rs b/crates/didbot/tests/scenarios/restart.rs index 9f01dba1..caa6d682 100644 --- a/crates/didbot/tests/scenarios/restart.rs +++ b/crates/didbot/tests/scenarios/restart.rs @@ -641,3 +641,79 @@ fn vouch(human: &Human, name: &str) { }), ); } + +/// Ending an app's logins that the server cannot write down fails and says +/// they may return after a restart, and a restart brings them back. +/// +/// `pds.grants` is rewritten whole at every change, through a scratch name +/// beside it. Here a directory holds that name, so the write fails while the +/// file the next boot reads still holds the login. +#[tokio::test(flavor = "multi_thread")] +async fn logins_the_server_could_not_write_down_are_reported_and_come_back() { + use didbot::pds::oauth::{GrantRequest, Lifetimes, OAuthGrantStore}; + const APP: &str = "https://app.example/client.json"; + let mut stack = Stack::start_with(&["unkept"], Server::CONFIG, true).await; + let data = stack.server().data_dir().to_path_buf(); + let open = || { + didbot::pds::Durable::open(&data, time::Duration::days(30)).expect("the directory opens") + }; + + // One login for the app, minted into the stopped server's directory + // through the store the server keeps its logins in. + stack.servers[0].stop(); + let family = open() + .oauth_grants() + .mint( + GrantRequest { + did: stack.server().did.clone(), + client_id: APP.to_owned(), + scope: "atproto".to_owned(), + dpop_thumbprint: "thumbprint".to_owned(), + client_auth_key: None, + }, + Lifetimes { + access: time::Duration::minutes(15), + refresh: time::Duration::days(1), + }, + ) + .expect("the login is kept") + .grant + .family; + stack.servers[0].start_again().await; + stack.server().nudge().await; + stack.server().wait_claimed().await; + // The operator session lived in the process that was stopped. + stack.sign_in_as_operator().await; + + std::fs::create_dir(data.join(didbot::pds::oauth::GRANT_SCRATCH)) + .expect("the scratch name is taken"); + let refusal = stack + .laptop + .didbot( + &[ + "app", + "end-logins", + "--client", + APP, + "--server", + &stack.server().host, + ], + &[], + ) + .await + .refused(); + assert!( + refusal.contains("GrantStoreFailed") && refusal.contains("may return after a restart"), + "{refusal}" + ); + + stack.servers[0].stop(); + assert!( + open() + .oauth_grants() + .live() + .iter() + .any(|grant| grant.family == family), + "the login the file still held did not come back" + ); +}