diff --git a/src/app.rs b/src/app.rs index 59910b9..1c22853 100644 --- a/src/app.rs +++ b/src/app.rs @@ -389,6 +389,80 @@ impl Carrier { } Task::none() } + // Results pass regardless of the busy flag: each one clears the + // operation it belongs to, so swallowing it would deadlock. + Message::PinOffered(result) => { + self.done(); + match result { + Ok(Some((addr, key))) => { + self.pin_offer = Some((addr, key)); + self.note( + "The server presented this key. Pin it only if the fingerprint \ + matches what the operator published." + .into(), + ); + Task::none() + } + Ok(None) => { + self.note("This server's key is already pinned.".into()); + Task::none() + } + Err(e) => { + self.fail(e); + Task::none() + } + } + } + Message::Pinned(result) => { + self.done(); + match result { + Ok(()) => self.note("server key pinned".into()), + Err(e) => self.fail(e), + } + Task::none() + } + Message::Exported(result) => { + self.done(); + match result { + Ok((note, text)) => { + self.backup_text = Some(text); + self.note(note); + Task::none() + } + Err(e) => { + self.fail(e); + Task::none() + } + } + } + Message::SnapshotLoaded(result) => { + self.done(); + match result { + Ok(snap) => { + self.share_qr = qr_code::Data::new( + format!("smol://{}/{}", snap.who.address, snap.who.key).as_bytes(), + ) + .ok(); + self.lists = snap.lists.clone(); + self.snapshot = Some(snap); + Task::none() + } + Err(e) => { + self.fail(e); + Task::none() + } + } + } + Message::Generic(result) => { + self.done(); + match result { + Ok(note) => self.refresh_after(note), + Err(e) => { + self.fail(e); + Task::none() + } + } + } _ if self.busy.is_some() => Task::none(), Message::OnboardCreate => { let master = ops::new_master(); @@ -477,28 +551,6 @@ impl Carrier { self.pin_offer = None; Task::none() } - Message::PinOffered(result) => { - self.done(); - match result { - Ok(Some((addr, key))) => { - self.pin_offer = Some((addr, key)); - self.note( - "The server presented this key. Pin it only if the fingerprint \ - matches what the operator published." - .into(), - ); - Task::none() - } - Ok(None) => { - self.note("This server's key is already pinned.".into()); - Task::none() - } - Err(e) => { - self.fail(e); - Task::none() - } - } - } Message::PinAccept => { let Some((addr, key)) = self.pin_offer.take() else { return Task::none(); @@ -507,14 +559,6 @@ impl Carrier { ops::pin(&store, &addr, &key) }) } - Message::Pinned(result) => { - self.done(); - match result { - Ok(()) => self.note("server key pinned".into()), - Err(e) => self.fail(e), - } - Task::none() - } Message::SignupRegister => { let Some(addr) = self.signup_address() else { self.fail("enter user and host".into()); @@ -878,20 +922,6 @@ impl Carrier { .map(|(path, text)| (format!("backup written to {}", path.display()), text)) }) } - Message::Exported(result) => { - self.done(); - match result { - Ok((note, text)) => { - self.backup_text = Some(text); - self.note(note); - Task::none() - } - Err(e) => { - self.fail(e); - Task::none() - } - } - } Message::ImportPressed => { let text = self.import_body.text(); if text.trim().is_empty() { @@ -901,34 +931,46 @@ impl Carrier { ops::import_backup(&store, &master, &text) }) } - Message::SnapshotLoaded(result) => { - self.done(); - match result { - Ok(snap) => { - self.share_qr = qr_code::Data::new( - format!("smol://{}/{}", snap.who.address, snap.who.key).as_bytes(), - ) - .ok(); - self.lists = snap.lists.clone(); - self.snapshot = Some(snap); - Task::none() - } - Err(e) => { - self.fail(e); - Task::none() - } - } - } - Message::Generic(result) => { - self.done(); - match result { - Ok(note) => self.refresh_after(note), - Err(e) => { - self.fail(e); - Task::none() - } - } - } } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn app() -> Carrier { + let dir = tempfile::tempdir().unwrap().keep(); + let (app, _task) = Carrier::boot(dir); + app + } + + #[test] + fn results_clear_the_busy_flag_instead_of_deadlocking() { + let mut app = app(); + app.busy = Some("registering".into()); + let _ = app.update(Message::Generic(Err("boom".into()))); + assert!(app.busy.is_none()); + assert!(app.status_error); + + app.busy = Some("registering".into()); + let _ = app.update(Message::SnapshotLoaded(Err("boom".into()))); + assert!(app.busy.is_none()); + + app.busy = Some("registering".into()); + let _ = app.update(Message::Generic(Ok("registered".into()))); + // The result was handled: the note landed and the refresh it + // triggers is the new busy operation. + assert_eq!(app.status.as_deref(), Some("registered")); + assert_eq!(app.busy.as_deref(), Some("refreshing")); + } + + #[test] + fn actions_are_swallowed_while_busy() { + let mut app = app(); + app.master = Some([0u8; KEY_LEN]); + app.busy = Some("fetching".into()); + let _ = app.update(Message::FetchPressed); + assert_eq!(app.busy.as_deref(), Some("fetching")); + } +}