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
51// ─── From impls for mobile error types ─────────────────────────────────────
52
53/// Convert `PluginInvokeError` into `ServiceError::PluginInvoke`.
54///
55/// This allows mobile call sites to use `.map_err(Into::into)` instead of
56/// `.map_err(|e| ServiceError::PluginInvoke(e.to_string()))`.
57#[cfg(mobile)]
58impl From<tauri::plugin::mobile::PluginInvokeError> for ServiceError {
59    fn from(e: tauri::plugin::mobile::PluginInvokeError) -> Self {
60        Self::PluginInvoke(e.to_string())
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn display_already_running() {
70        assert_eq!(
71            ServiceError::AlreadyRunning.to_string(),
72            "Service is already running"
73        );
74    }
75
76    #[test]
77    fn display_not_running() {
78        assert_eq!(
79            ServiceError::NotRunning.to_string(),
80            "Service is not running"
81        );
82    }
83
84    #[test]
85    fn display_init() {
86        let msg = "db connection failed".to_string();
87        assert_eq!(
88            ServiceError::Init(msg.clone()).to_string(),
89            format!("Initialisation failed: {msg}")
90        );
91    }
92
93    #[test]
94    fn display_runtime() {
95        let msg = "network timeout".to_string();
96        assert_eq!(
97            ServiceError::Runtime(msg.clone()).to_string(),
98            format!("Runtime error: {msg}")
99        );
100    }
101
102    #[test]
103    fn display_platform() {
104        let msg = "foreground service denied".to_string();
105        assert_eq!(
106            ServiceError::Platform(msg.clone()).to_string(),
107            format!("Platform error: {msg}")
108        );
109    }
110
111    #[test]
112    fn convert_to_invoke_error_via_serialize() {
113        // ServiceError derives Serialize, so Tauri's blanket From<T: Serialize> for InvokeError applies.
114        // Verify the conversion compiles (type-level proof).
115        let err = ServiceError::Init("test".into());
116        let invoke_err: tauri::ipc::InvokeError = err.into();
117        // InvokeError wraps serde_json::Value — verify it contains the serialized form
118        let _val = &invoke_err.0;
119        assert!(!invoke_err.0.is_null());
120    }
121
122    #[test]
123    fn clone_roundtrip() {
124        let err = ServiceError::Init("test".into());
125        let cloned = err.clone();
126        assert_eq!(err.to_string(), cloned.to_string());
127    }
128
129    #[test]
130    fn serde_roundtrip_already_running() {
131        let err = ServiceError::AlreadyRunning;
132        let json = serde_json::to_string(&err).unwrap();
133        let de: ServiceError = serde_json::from_str(&json).unwrap();
134        assert!(matches!(de, ServiceError::AlreadyRunning));
135    }
136
137    #[test]
138    fn serde_roundtrip_init() {
139        let err = ServiceError::Init("boom".into());
140        let json = serde_json::to_string(&err).unwrap();
141        let de: ServiceError = serde_json::from_str(&json).unwrap();
142        assert!(matches!(de, ServiceError::Init(ref s) if s == "boom"));
143    }
144
145    #[cfg(feature = "desktop-service")]
146    mod desktop_service {
147        use super::*;
148
149        #[test]
150        fn display_service_install() {
151            let msg = "permission denied".to_string();
152            assert_eq!(
153                ServiceError::ServiceInstall(msg.clone()).to_string(),
154                format!("Service installation failed: {msg}")
155            );
156        }
157
158        #[test]
159        fn display_service_uninstall() {
160            let msg = "not found".to_string();
161            assert_eq!(
162                ServiceError::ServiceUninstall(msg.clone()).to_string(),
163                format!("Service uninstallation failed: {msg}")
164            );
165        }
166
167        #[test]
168        fn display_ipc_error() {
169            let msg = "connection lost".to_string();
170            assert_eq!(
171                ServiceError::Ipc(msg.clone()).to_string(),
172                format!("IPC error: {msg}")
173            );
174        }
175
176        #[test]
177        fn serde_roundtrip_service_install() {
178            let err = ServiceError::ServiceInstall("fail".into());
179            let json = serde_json::to_string(&err).unwrap();
180            let de: ServiceError = serde_json::from_str(&json).unwrap();
181            assert!(matches!(de, ServiceError::ServiceInstall(ref s) if s == "fail"));
182        }
183
184        #[test]
185        fn serde_roundtrip_service_uninstall() {
186            let err = ServiceError::ServiceUninstall("fail".into());
187            let json = serde_json::to_string(&err).unwrap();
188            let de: ServiceError = serde_json::from_str(&json).unwrap();
189            assert!(matches!(de, ServiceError::ServiceUninstall(ref s) if s == "fail"));
190        }
191
192        #[test]
193        fn serde_roundtrip_ipc() {
194            let err = ServiceError::Ipc("socket closed".into());
195            let json = serde_json::to_string(&err).unwrap();
196            let de: ServiceError = serde_json::from_str(&json).unwrap();
197            assert!(matches!(de, ServiceError::Ipc(ref s) if s == "socket closed"));
198        }
199
200        #[test]
201        fn clone_roundtrip_service_install() {
202            let err = ServiceError::ServiceInstall("fail".into());
203            let cloned = err.clone();
204            assert_eq!(err.to_string(), cloned.to_string());
205        }
206
207        #[test]
208        fn clone_roundtrip_service_uninstall() {
209            let err = ServiceError::ServiceUninstall("fail".into());
210            let cloned = err.clone();
211            assert_eq!(err.to_string(), cloned.to_string());
212        }
213
214        #[test]
215        fn clone_roundtrip_ipc() {
216            let err = ServiceError::Ipc("timeout".into());
217            let cloned = err.clone();
218            assert_eq!(err.to_string(), cloned.to_string());
219        }
220    }
221}