diff --git a/gui/src/error.rs b/gui/src/error.rs new file mode 100644 index 0000000..f43bb28 --- /dev/null +++ b/gui/src/error.rs @@ -0,0 +1,87 @@ +//! Map zbus errors to short, user-facing strings. The GUI surfaces these in +//! `adw::Toast` and inside the touch-prompt dialog's failure state. +//! +//! Pure logic; testable without a display server. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] // wired through keys_page.rs / enroll_dialog.rs in Tasks 4-6. +pub(crate) enum ErrorKind { + /// Daemon unreachable / bus dropped — caller should swap to disconnected banner. + Disconnected, + /// polkit denied. Daemon's message is usually fine to show verbatim. + AccessDenied, + /// Anything else — daemon returned a typed `Failed(msg)` or similar. + Other, +} + +#[allow(dead_code)] // wired through keys_page.rs / enroll_dialog.rs in Tasks 4-6. +pub(crate) fn classify(err: &zbus::Error) -> ErrorKind { + match err { + zbus::Error::InputOutput(_) | zbus::Error::Address(_) | zbus::Error::Handshake(_) => { + ErrorKind::Disconnected + } + zbus::Error::FDO(boxed) => match boxed.as_ref() { + zbus::fdo::Error::AccessDenied(_) => ErrorKind::AccessDenied, + zbus::fdo::Error::ServiceUnknown(_) | zbus::fdo::Error::NameHasNoOwner(_) => { + ErrorKind::Disconnected + } + _ => ErrorKind::Other, + }, + _ => ErrorKind::Other, + } +} + +#[allow(dead_code)] // wired through keys_page.rs / enroll_dialog.rs in Tasks 4-6. +pub(crate) fn user_message(err: &zbus::Error) -> String { + match classify(err) { + ErrorKind::Disconnected => { + "Couldn't reach the authentication daemon. Make sure authforge-daemon is running." + .to_string() + } + ErrorKind::AccessDenied => match err { + zbus::Error::FDO(boxed) => match boxed.as_ref() { + zbus::fdo::Error::AccessDenied(msg) => msg.clone(), + _ => "Access denied.".to_string(), + }, + _ => "Access denied.".to_string(), + }, + ErrorKind::Other => err.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn service_unknown_classifies_as_disconnected() { + let e = zbus::Error::FDO(Box::new(zbus::fdo::Error::ServiceUnknown( + "no daemon".to_string(), + ))); + assert_eq!(classify(&e), ErrorKind::Disconnected); + } + + #[test] + fn access_denied_classifies_as_access_denied() { + let e = zbus::Error::FDO(Box::new(zbus::fdo::Error::AccessDenied( + "polkit said no".to_string(), + ))); + assert_eq!(classify(&e), ErrorKind::AccessDenied); + } + + #[test] + fn access_denied_user_message_is_daemon_message_verbatim() { + let e = zbus::Error::FDO(Box::new(zbus::fdo::Error::AccessDenied( + "polkit said no".to_string(), + ))); + assert_eq!(user_message(&e), "polkit said no"); + } + + #[test] + fn disconnected_user_message_mentions_authforge_daemon() { + let e = zbus::Error::FDO(Box::new(zbus::fdo::Error::ServiceUnknown( + "no daemon".to_string(), + ))); + assert!(user_message(&e).contains("authforge-daemon")); + } +} diff --git a/gui/src/main.rs b/gui/src/main.rs index 7aa2497..6e684d1 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -1,6 +1,8 @@ use adw::prelude::*; use gtk::glib; +mod error; + const APP_ID: &str = "io.dangerousthings.AuthForge"; fn main() -> glib::ExitCode {