Skip to main content

tauri_plugin_background_service/
error.rs

1//! Error types returned by background service operations.
2//!
3//! [`ServiceError`] is `#[non_exhaustive]` — new variants may be added in
4//! minor releases. Match with a wildcard `_` arm to avoid breakage.
5
6/// Errors that can occur during background service lifecycle.
7#[derive(Debug, thiserror::Error, Clone, serde::Serialize, serde::Deserialize)]
8#[non_exhaustive]
9pub enum ServiceError {
10    /// A service is already running; call `stopService()` first.
11    #[error("Service is already running")]
12    AlreadyRunning,
13
14    /// No service is currently running.
15    #[error("Service is not running")]
16    NotRunning,
17
18    /// The service's `init()` method failed.
19    #[error("Initialisation failed: {0}")]
20    Init(String),
21
22    /// A runtime error occurred inside the service's `run()` method.
23    #[error("Runtime error: {0}")]
24    Runtime(String),
25
26    /// A platform-specific error (e.g. Android foreground service denied).
27    #[error("Platform error: {0}")]
28    Platform(String),
29
30    /// A plugin invoke error from `run_mobile_plugin` (mobile only).
31    #[cfg(mobile)]
32    #[error("Plugin invoke error: {0}")]
33    PluginInvoke(String),
34
35    /// Failed to install the OS service (desktop only).
36    #[cfg(feature = "desktop-service")]
37    #[error("Service installation failed: {0}")]
38    ServiceInstall(String),
39
40    /// Failed to uninstall the OS service (desktop only).
41    #[cfg(feature = "desktop-service")]
42    #[error("Service uninstallation failed: {0}")]
43    ServiceUninstall(String),
44
45    /// An IPC communication error (desktop only).
46    #[cfg(feature = "desktop-service")]
47    #[error("IPC error: {0}")]
48    Ipc(String),
49
50    /// Failed to start the OS service (desktop only).
51    #[cfg(feature = "desktop-service")]
52    #[error("Service start failed: {0}")]
53    ServiceStart(String),
54
55    /// Failed to stop the OS service (desktop only).
56    #[cfg(feature = "desktop-service")]
57    #[error("Service stop failed: {0}")]
58    ServiceStop(String),
59}
60
61// ─── From impls for mobile error types ─────────────────────────────────────
62
63/// Convert `PluginInvokeError` into `ServiceError::PluginInvoke`.
64///
65/// This allows mobile call sites to use `.map_err(Into::into)` instead of
66/// `.map_err(|e| ServiceError::PluginInvoke(e.to_string()))`.
67#[cfg(mobile)]
68impl From<tauri::plugin::mobile::PluginInvokeError> for ServiceError {
69    fn from(e: tauri::plugin::mobile::PluginInvokeError) -> Self {
70        Self::PluginInvoke(e.to_string())
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn display_already_running() {
80        assert_eq!(
81            ServiceError::AlreadyRunning.to_string(),
82            "Service is already running"
83        );
84    }
85
86    #[test]
87    fn display_not_running() {
88        assert_eq!(
89            ServiceError::NotRunning.to_string(),
90            "Service is not running"
91        );
92    }
93
94    #[test]
95    fn display_init() {
96        let msg = "db connection failed".to_string();
97        assert_eq!(
98            ServiceError::Init(msg.clone()).to_string(),
99            format!("Initialisation failed: {msg}")
100        );
101    }
102
103    #[test]
104    fn display_runtime() {
105        let msg = "network timeout".to_string();
106        assert_eq!(
107            ServiceError::Runtime(msg.clone()).to_string(),
108            format!("Runtime error: {msg}")
109        );
110    }
111
112    #[test]
113    fn display_platform() {
114        let msg = "foreground service denied".to_string();
115        assert_eq!(
116            ServiceError::Platform(msg.clone()).to_string(),
117            format!("Platform error: {msg}")
118        );
119    }
120
121    #[test]
122    fn convert_to_invoke_error_via_serialize() {
123        // ServiceError derives Serialize, so Tauri's blanket From<T: Serialize> for InvokeError applies.
124        // Verify the conversion compiles (type-level proof).
125        let err = ServiceError::Init("test".into());
126        let invoke_err: tauri::ipc::InvokeError = err.into();
127        // InvokeError wraps serde_json::Value — verify it contains the serialized form
128        let _val = &invoke_err.0;
129        assert!(!invoke_err.0.is_null());
130    }
131
132    #[test]
133    fn clone_roundtrip() {
134        let err = ServiceError::Init("test".into());
135        let cloned = err.clone();
136        assert_eq!(err.to_string(), cloned.to_string());
137    }
138
139    #[test]
140    fn serde_roundtrip_already_running() {
141        let err = ServiceError::AlreadyRunning;
142        let json = serde_json::to_string(&err).unwrap();
143        let de: ServiceError = serde_json::from_str(&json).unwrap();
144        assert!(matches!(de, ServiceError::AlreadyRunning));
145    }
146
147    #[test]
148    fn serde_roundtrip_init() {
149        let err = ServiceError::Init("boom".into());
150        let json = serde_json::to_string(&err).unwrap();
151        let de: ServiceError = serde_json::from_str(&json).unwrap();
152        assert!(matches!(de, ServiceError::Init(ref s) if s == "boom"));
153    }
154
155    #[cfg(feature = "desktop-service")]
156    mod desktop_service {
157        use super::*;
158
159        #[test]
160        fn display_service_install() {
161            let msg = "permission denied".to_string();
162            assert_eq!(
163                ServiceError::ServiceInstall(msg.clone()).to_string(),
164                format!("Service installation failed: {msg}")
165            );
166        }
167
168        #[test]
169        fn display_service_uninstall() {
170            let msg = "not found".to_string();
171            assert_eq!(
172                ServiceError::ServiceUninstall(msg.clone()).to_string(),
173                format!("Service uninstallation failed: {msg}")
174            );
175        }
176
177        #[test]
178        fn display_ipc_error() {
179            let msg = "connection lost".to_string();
180            assert_eq!(
181                ServiceError::Ipc(msg.clone()).to_string(),
182                format!("IPC error: {msg}")
183            );
184        }
185
186        #[test]
187        fn serde_roundtrip_service_install() {
188            let err = ServiceError::ServiceInstall("fail".into());
189            let json = serde_json::to_string(&err).unwrap();
190            let de: ServiceError = serde_json::from_str(&json).unwrap();
191            assert!(matches!(de, ServiceError::ServiceInstall(ref s) if s == "fail"));
192        }
193
194        #[test]
195        fn serde_roundtrip_service_uninstall() {
196            let err = ServiceError::ServiceUninstall("fail".into());
197            let json = serde_json::to_string(&err).unwrap();
198            let de: ServiceError = serde_json::from_str(&json).unwrap();
199            assert!(matches!(de, ServiceError::ServiceUninstall(ref s) if s == "fail"));
200        }
201
202        #[test]
203        fn serde_roundtrip_ipc() {
204            let err = ServiceError::Ipc("socket closed".into());
205            let json = serde_json::to_string(&err).unwrap();
206            let de: ServiceError = serde_json::from_str(&json).unwrap();
207            assert!(matches!(de, ServiceError::Ipc(ref s) if s == "socket closed"));
208        }
209
210        #[test]
211        fn clone_roundtrip_service_install() {
212            let err = ServiceError::ServiceInstall("fail".into());
213            let cloned = err.clone();
214            assert_eq!(err.to_string(), cloned.to_string());
215        }
216
217        #[test]
218        fn clone_roundtrip_service_uninstall() {
219            let err = ServiceError::ServiceUninstall("fail".into());
220            let cloned = err.clone();
221            assert_eq!(err.to_string(), cloned.to_string());
222        }
223
224        #[test]
225        fn clone_roundtrip_ipc() {
226            let err = ServiceError::Ipc("timeout".into());
227            let cloned = err.clone();
228            assert_eq!(err.to_string(), cloned.to_string());
229        }
230
231        #[test]
232        fn display_service_start() {
233            let msg = "systemd failed".to_string();
234            assert_eq!(
235                ServiceError::ServiceStart(msg.clone()).to_string(),
236                format!("Service start failed: {msg}")
237            );
238        }
239
240        #[test]
241        fn display_service_stop() {
242            let msg = "not running".to_string();
243            assert_eq!(
244                ServiceError::ServiceStop(msg.clone()).to_string(),
245                format!("Service stop failed: {msg}")
246            );
247        }
248
249        #[test]
250        fn serde_roundtrip_service_start() {
251            let err = ServiceError::ServiceStart("fail".into());
252            let json = serde_json::to_string(&err).unwrap();
253            let de: ServiceError = serde_json::from_str(&json).unwrap();
254            assert!(matches!(de, ServiceError::ServiceStart(ref s) if s == "fail"));
255        }
256
257        #[test]
258        fn serde_roundtrip_service_stop() {
259            let err = ServiceError::ServiceStop("fail".into());
260            let json = serde_json::to_string(&err).unwrap();
261            let de: ServiceError = serde_json::from_str(&json).unwrap();
262            assert!(matches!(de, ServiceError::ServiceStop(ref s) if s == "fail"));
263        }
264    }
265}