Skip to main content

tauri_plugin_background_service/
models.rs

1//! Data types shared between the plugin's Rust core and the JS/Tauri layer.
2//!
3//! - [`ServiceContext`] is passed to every [`BackgroundService`](crate::BackgroundService) method.
4//! - [`StartConfig`] and [`PluginConfig`] control service and plugin behaviour.
5//! - [`PluginEvent`] represents events emitted to the JavaScript front-end.
6
7use serde::{Deserialize, Serialize};
8use tauri::Runtime;
9use tokio_util::sync::CancellationToken;
10
11use crate::notifier::Notifier;
12
13/// Passed into both `init` and `run`.
14/// Gives your service everything it needs to interact with the outside world.
15pub struct ServiceContext<R: Runtime> {
16    /// Fire a local notification. Works on all platforms.
17    pub notifier: Notifier<R>,
18
19    /// Emit an event to the JS UI layer.
20    pub app: tauri::AppHandle<R>,
21
22    /// Cancelled when `stopService()` is called.
23    pub shutdown: CancellationToken,
24
25    /// Text shown in the Android persistent notification.
26    /// `None` on desktop platforms.
27    pub service_label: Option<String>,
28
29    /// Android foreground service type (e.g. "dataSync", "specialUse").
30    /// `None` on desktop platforms.
31    pub foreground_service_type: Option<String>,
32}
33
34/// Optional startup configuration forwarded from JS through the plugin.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct StartConfig {
38    /// Text shown in the Android persistent foreground notification.
39    #[serde(default = "default_label")]
40    pub service_label: String,
41
42    /// Android foreground service type (e.g. "dataSync", "specialUse").
43    #[serde(default = "default_foreground_service_type")]
44    pub foreground_service_type: String,
45}
46
47fn default_label() -> String {
48    "Service running".into()
49}
50
51fn default_foreground_service_type() -> String {
52    "dataSync".into()
53}
54
55/// Plugin-level configuration, deserialized from the Tauri plugin config.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct PluginConfig {
59    /// iOS safety timeout in seconds for the expiration handler.
60    /// Default: 28.0 (Apple recommends keeping BG tasks under ~30s).
61    #[serde(default = "default_ios_safety_timeout")]
62    pub ios_safety_timeout_secs: f64,
63
64    /// iOS cancel listener timeout in seconds.
65    /// Default: 14400 (4 hours). Balances leak risk vs. service lifetime.
66    #[serde(default = "default_ios_cancel_listener_timeout_secs")]
67    pub ios_cancel_listener_timeout_secs: u64,
68
69    /// iOS BGProcessingTask safety timeout in seconds.
70    /// Default: 0.0 (no cap). Processing tasks can run for minutes/hours,
71    /// so unlike BGAppRefreshTask (28s default), this defaults to uncapped.
72    /// Set to a positive value to impose a hard cap on processing task duration.
73    #[serde(default = "default_ios_processing_safety_timeout_secs")]
74    pub ios_processing_safety_timeout_secs: f64,
75
76    /// Desktop service mode: "inProcess" (default) or "osService".
77    /// Controls whether the background service runs in-process or as a
78    /// registered OS service/daemon.
79    #[cfg(feature = "desktop-service")]
80    #[serde(default = "default_desktop_service_mode")]
81    pub desktop_service_mode: String,
82
83    /// Optional custom label for the desktop OS service registration.
84    /// When `None`, the label is auto-derived from the app identifier.
85    #[cfg(feature = "desktop-service")]
86    #[serde(default)]
87    pub desktop_service_label: Option<String>,
88}
89
90fn default_ios_safety_timeout() -> f64 {
91    28.0
92}
93
94fn default_ios_cancel_listener_timeout_secs() -> u64 {
95    14400
96}
97
98fn default_ios_processing_safety_timeout_secs() -> f64 {
99    0.0
100}
101
102#[cfg(feature = "desktop-service")]
103fn default_desktop_service_mode() -> String {
104    "inProcess".into()
105}
106
107impl Default for StartConfig {
108    fn default() -> Self {
109        Self {
110            service_label: default_label(),
111            foreground_service_type: default_foreground_service_type(),
112        }
113    }
114}
115
116/// Built-in event types emitted by the runner itself.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase", tag = "type")]
119#[non_exhaustive]
120pub enum PluginEvent {
121    /// init() completed successfully
122    Started,
123    /// run() returned or was cancelled
124    Stopped { reason: String },
125    /// init() or run() returned an error
126    Error { message: String },
127}
128
129impl Default for PluginConfig {
130    fn default() -> Self {
131        Self {
132            ios_safety_timeout_secs: default_ios_safety_timeout(),
133            ios_cancel_listener_timeout_secs: default_ios_cancel_listener_timeout_secs(),
134            ios_processing_safety_timeout_secs: default_ios_processing_safety_timeout_secs(),
135            #[cfg(feature = "desktop-service")]
136            desktop_service_mode: default_desktop_service_mode(),
137            #[cfg(feature = "desktop-service")]
138            desktop_service_label: None,
139        }
140    }
141}
142
143/// Arguments sent to the native `startKeepalive` handler.
144///
145/// Lives in `models.rs` (not `mobile.rs`) so serde tests run on all platforms.
146#[derive(Debug, Serialize)]
147#[serde(rename_all = "camelCase")]
148#[allow(dead_code)]
149pub(crate) struct StartKeepaliveArgs<'a> {
150    pub label: &'a str,
151    pub foreground_service_type: &'a str,
152    /// iOS safety timeout in seconds. Only sent to iOS; `None` omits the key.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub ios_safety_timeout_secs: Option<f64>,
155    /// iOS BGProcessingTask safety timeout in seconds. Only sent to iOS; `None` omits the key.
156    /// When `Some(positive)`, caps the processing task duration.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub ios_processing_safety_timeout_secs: Option<f64>,
159}
160
161/// Auto-start config returned by the Kotlin bridge.
162///
163/// Deserialized from SharedPreferences values read by `getAutoStartConfig`.
164/// Only used on Android (the iOS path doesn't have auto-start).
165#[doc(hidden)]
166#[derive(Debug, Clone, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct AutoStartConfig {
169    pub pending: bool,
170    pub label: Option<String>,
171    pub service_type: Option<String>,
172}
173
174impl AutoStartConfig {
175    /// Convert to `StartConfig` if auto-start is pending and label is available.
176    pub fn into_start_config(self) -> Option<StartConfig> {
177        if self.pending {
178            self.label.map(|label| StartConfig {
179                service_label: label,
180                foreground_service_type: self
181                    .service_type
182                    .unwrap_or_else(default_foreground_service_type),
183            })
184        } else {
185            None
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    // --- StartConfig tests ---
195
196    #[test]
197    fn start_config_default_label() {
198        let config = StartConfig::default();
199        assert_eq!(config.service_label, "Service running");
200    }
201
202    #[test]
203    fn start_config_custom_label() {
204        let config = StartConfig {
205            service_label: "Syncing data".into(),
206            ..Default::default()
207        };
208        assert_eq!(config.service_label, "Syncing data");
209    }
210
211    #[test]
212    fn start_config_serde_roundtrip_default() {
213        let config = StartConfig::default();
214        let json = serde_json::to_string(&config).unwrap();
215        let de: StartConfig = serde_json::from_str(&json).unwrap();
216        assert_eq!(de.service_label, config.service_label);
217    }
218
219    #[test]
220    fn start_config_serde_roundtrip_custom() {
221        let config = StartConfig {
222            service_label: "My service".into(),
223            ..Default::default()
224        };
225        let json = serde_json::to_string(&config).unwrap();
226        let de: StartConfig = serde_json::from_str(&json).unwrap();
227        assert_eq!(de.service_label, "My service");
228    }
229
230    #[test]
231    fn start_config_deserialize_missing_field_uses_default() {
232        // An empty JSON object should produce the default label
233        let json = "{}";
234        let de: StartConfig = serde_json::from_str(json).unwrap();
235        assert_eq!(de.service_label, "Service running");
236    }
237
238    #[test]
239    fn start_config_json_key_is_camel_case() {
240        let config = StartConfig {
241            service_label: "test".into(),
242            ..Default::default()
243        };
244        let json = serde_json::to_string(&config).unwrap();
245        assert!(json.contains("serviceLabel"), "JSON should use camelCase: {json}");
246    }
247
248    // --- PluginEvent tests ---
249
250    #[test]
251    fn plugin_event_started_serde_roundtrip() {
252        let event = PluginEvent::Started;
253        let json = serde_json::to_string(&event).unwrap();
254        let de: PluginEvent = serde_json::from_str(&json).unwrap();
255        assert!(matches!(de, PluginEvent::Started));
256    }
257
258    #[test]
259    fn plugin_event_stopped_serde_roundtrip() {
260        let event = PluginEvent::Stopped {
261            reason: "cancelled".into(),
262        };
263        let json = serde_json::to_string(&event).unwrap();
264        let de: PluginEvent = serde_json::from_str(&json).unwrap();
265        match de {
266            PluginEvent::Stopped { reason } => assert_eq!(reason, "cancelled"),
267            other => panic!("Expected Stopped, got {other:?}"),
268        }
269    }
270
271    #[test]
272    fn plugin_event_error_serde_roundtrip() {
273        let event = PluginEvent::Error {
274            message: "init failed".into(),
275        };
276        let json = serde_json::to_string(&event).unwrap();
277        let de: PluginEvent = serde_json::from_str(&json).unwrap();
278        match de {
279            PluginEvent::Error { message } => assert_eq!(message, "init failed"),
280            other => panic!("Expected Error, got {other:?}"),
281        }
282    }
283
284    #[test]
285    fn plugin_event_tagged_json_format() {
286        let event = PluginEvent::Started;
287        let json = serde_json::to_string(&event).unwrap();
288        assert!(json.contains("\"type\":\"started\""), "Tagged JSON: {json}");
289    }
290
291    #[test]
292    fn plugin_event_stopped_json_keys_camel_case() {
293        let event = PluginEvent::Stopped {
294            reason: "done".into(),
295        };
296        let json = serde_json::to_string(&event).unwrap();
297        assert!(json.contains("\"type\":\"stopped\""), "Tag: {json}");
298        assert!(json.contains("\"reason\":\"done\""), "Reason: {json}");
299    }
300
301    #[test]
302    fn plugin_event_error_json_keys_camel_case() {
303        let event = PluginEvent::Error {
304            message: "oops".into(),
305        };
306        let json = serde_json::to_string(&event).unwrap();
307        assert!(json.contains("\"type\":\"error\""), "Tag: {json}");
308        assert!(json.contains("\"message\":\"oops\""), "Message: {json}");
309    }
310
311    // --- StartConfig foreground_service_type tests ---
312
313    #[test]
314    fn start_config_default_service_type() {
315        let config = StartConfig::default();
316        assert_eq!(config.foreground_service_type, "dataSync");
317    }
318
319    #[test]
320    fn start_config_custom_service_type() {
321        let config = StartConfig {
322            service_label: "test".into(),
323            foreground_service_type: "specialUse".into(),
324        };
325        assert_eq!(config.foreground_service_type, "specialUse");
326    }
327
328    #[test]
329    fn start_config_serde_roundtrip_service_type() {
330        let config = StartConfig {
331            service_label: "test".into(),
332            foreground_service_type: "specialUse".into(),
333        };
334        let json = serde_json::to_string(&config).unwrap();
335        let de: StartConfig = serde_json::from_str(&json).unwrap();
336        assert_eq!(de.foreground_service_type, "specialUse");
337    }
338
339    #[test]
340    fn start_config_deserialize_missing_service_type() {
341        let json = r#"{"serviceLabel":"test"}"#;
342        let de: StartConfig = serde_json::from_str(json).unwrap();
343        assert_eq!(de.foreground_service_type, "dataSync");
344    }
345
346    #[test]
347    fn start_config_deserialize_special_use() {
348        let json = r#"{"serviceLabel":"test","foregroundServiceType":"specialUse"}"#;
349        let de: StartConfig = serde_json::from_str(json).unwrap();
350        assert_eq!(de.foreground_service_type, "specialUse");
351    }
352
353    #[test]
354    fn start_config_unrecognized_type_passes_through() {
355        let json = r#"{"serviceLabel":"test","foregroundServiceType":"customType"}"#;
356        let de: StartConfig = serde_json::from_str(json).unwrap();
357        assert_eq!(de.foreground_service_type, "customType");
358    }
359
360    #[test]
361    fn start_config_json_key_is_camel_case_service_type() {
362        let config = StartConfig {
363            service_label: "test".into(),
364            foreground_service_type: "specialUse".into(),
365        };
366        let json = serde_json::to_string(&config).unwrap();
367        assert!(
368            json.contains("foregroundServiceType"),
369            "JSON should use camelCase: {json}"
370        );
371    }
372
373    // --- AutoStartConfig tests ---
374
375    #[test]
376    fn auto_start_config_pending_with_label_returns_start_config() {
377        let json = r#"{"pending": true, "label": "Syncing"}"#;
378        let config: AutoStartConfig = serde_json::from_str(json).unwrap();
379        let result = config.into_start_config();
380        assert!(result.is_some());
381        let start_config = result.unwrap();
382        assert_eq!(start_config.service_label, "Syncing");
383        assert_eq!(start_config.foreground_service_type, "dataSync");
384    }
385
386    #[test]
387    fn auto_start_config_not_pending_returns_none() {
388        let json = r#"{"pending": false, "label": null}"#;
389        let config: AutoStartConfig = serde_json::from_str(json).unwrap();
390        let result = config.into_start_config();
391        assert!(result.is_none());
392    }
393
394    #[test]
395    fn auto_start_config_pending_no_label_returns_none() {
396        let json = r#"{"pending": true, "label": null}"#;
397        let config: AutoStartConfig = serde_json::from_str(json).unwrap();
398        let result = config.into_start_config();
399        assert!(result.is_none());
400    }
401
402    #[test]
403    fn auto_start_config_with_service_type_preserves_it() {
404        let json = r#"{"pending":true,"label":"test","serviceType":"specialUse"}"#;
405        let config: AutoStartConfig = serde_json::from_str(json).unwrap();
406        assert_eq!(config.service_type, Some("specialUse".to_string()));
407        let result = config.into_start_config();
408        assert!(result.is_some());
409        let start_config = result.unwrap();
410        assert_eq!(start_config.foreground_service_type, "specialUse");
411    }
412
413    #[test]
414    fn auto_start_config_without_service_type_uses_default() {
415        let json = r#"{"pending":true,"label":"test"}"#;
416        let config: AutoStartConfig = serde_json::from_str(json).unwrap();
417        assert_eq!(config.service_type, None);
418        let result = config.into_start_config();
419        assert!(result.is_some());
420        assert_eq!(result.unwrap().foreground_service_type, "dataSync");
421    }
422
423    #[test]
424    fn auto_start_config_null_service_type_uses_default() {
425        let json = r#"{"pending":true,"label":"test","serviceType":null}"#;
426        let config: AutoStartConfig = serde_json::from_str(json).unwrap();
427        assert_eq!(config.service_type, None);
428        let result = config.into_start_config();
429        assert!(result.is_some());
430        assert_eq!(result.unwrap().foreground_service_type, "dataSync");
431    }
432
433    // --- PluginConfig tests ---
434
435    #[test]
436    fn plugin_config_default_ios_safety_timeout() {
437        let json = "{}";
438        let config: PluginConfig = serde_json::from_str(json).unwrap();
439        assert_eq!(config.ios_safety_timeout_secs, 28.0);
440    }
441
442    #[test]
443    fn plugin_config_custom_ios_safety_timeout() {
444        let json = r#"{"iosSafetyTimeoutSecs":15.0}"#;
445        let config: PluginConfig = serde_json::from_str(json).unwrap();
446        assert_eq!(config.ios_safety_timeout_secs, 15.0);
447    }
448
449    #[test]
450    fn plugin_config_serde_roundtrip_preserves_value() {
451        let config = PluginConfig {
452            ios_safety_timeout_secs: 30.0,
453            ios_cancel_listener_timeout_secs: 14400,
454            ios_processing_safety_timeout_secs: 0.0,
455            ..Default::default()
456        };
457        let json = serde_json::to_string(&config).unwrap();
458        let de: PluginConfig = serde_json::from_str(&json).unwrap();
459        assert_eq!(de.ios_safety_timeout_secs, 30.0);
460    }
461
462    #[test]
463    fn plugin_config_default_impl() {
464        let config = PluginConfig::default();
465        assert_eq!(config.ios_safety_timeout_secs, 28.0);
466    }
467
468    #[test]
469    fn plugin_config_default_cancel_timeout() {
470        let json = "{}";
471        let config: PluginConfig = serde_json::from_str(json).unwrap();
472        assert_eq!(config.ios_cancel_listener_timeout_secs, 14400);
473    }
474
475    #[test]
476    fn plugin_config_custom_cancel_timeout() {
477        let json = r#"{"iosCancelListenerTimeoutSecs":7200}"#;
478        let config: PluginConfig = serde_json::from_str(json).unwrap();
479        assert_eq!(config.ios_cancel_listener_timeout_secs, 7200);
480    }
481
482    #[test]
483    fn plugin_config_cancel_timeout_serde_roundtrip() {
484        let config = PluginConfig {
485            ios_safety_timeout_secs: 28.0,
486            ios_cancel_listener_timeout_secs: 3600,
487            ios_processing_safety_timeout_secs: 0.0,
488            ..Default::default()
489        };
490        let json = serde_json::to_string(&config).unwrap();
491        let de: PluginConfig = serde_json::from_str(&json).unwrap();
492        assert_eq!(de.ios_cancel_listener_timeout_secs, 3600);
493    }
494
495    // --- PluginConfig ios_processing_safety_timeout_secs tests ---
496
497    #[test]
498    fn plugin_config_processing_timeout_default() {
499        let json = "{}";
500        let config: PluginConfig = serde_json::from_str(json).unwrap();
501        assert_eq!(config.ios_processing_safety_timeout_secs, 0.0);
502    }
503
504    #[test]
505    fn plugin_config_processing_timeout_custom() {
506        let json = r#"{"iosProcessingSafetyTimeoutSecs":60.0}"#;
507        let config: PluginConfig = serde_json::from_str(json).unwrap();
508        assert_eq!(config.ios_processing_safety_timeout_secs, 60.0);
509    }
510
511    #[test]
512    fn plugin_config_processing_timeout_serde_roundtrip() {
513        let config = PluginConfig {
514            ios_safety_timeout_secs: 28.0,
515            ios_cancel_listener_timeout_secs: 14400,
516            ios_processing_safety_timeout_secs: 120.0,
517            ..Default::default()
518        };
519        let json = serde_json::to_string(&config).unwrap();
520        let de: PluginConfig = serde_json::from_str(&json).unwrap();
521        assert_eq!(de.ios_processing_safety_timeout_secs, 120.0);
522    }
523
524    // --- StartKeepaliveArgs tests ---
525
526    #[test]
527    fn start_keepalive_args_with_timeout() {
528        let args = StartKeepaliveArgs {
529            label: "Test",
530            foreground_service_type: "dataSync",
531            ios_safety_timeout_secs: Some(15.0),
532            ios_processing_safety_timeout_secs: None,
533        };
534        let json = serde_json::to_string(&args).unwrap();
535        assert!(
536            json.contains("\"iosSafetyTimeoutSecs\":15.0"),
537            "JSON should contain iosSafetyTimeoutSecs: {json}"
538        );
539    }
540
541    #[test]
542    fn start_keepalive_args_without_timeout() {
543        let args = StartKeepaliveArgs {
544            label: "Test",
545            foreground_service_type: "dataSync",
546            ios_safety_timeout_secs: None,
547            ios_processing_safety_timeout_secs: None,
548        };
549        let json = serde_json::to_string(&args).unwrap();
550        assert!(
551            !json.contains("iosSafetyTimeoutSecs"),
552            "JSON should NOT contain iosSafetyTimeoutSecs when None: {json}"
553        );
554    }
555
556    #[test]
557    fn start_keepalive_args_processing_timeout() {
558        let args = StartKeepaliveArgs {
559            label: "Test",
560            foreground_service_type: "dataSync",
561            ios_safety_timeout_secs: None,
562            ios_processing_safety_timeout_secs: Some(60.0),
563        };
564        let json = serde_json::to_string(&args).unwrap();
565        assert!(
566            json.contains("\"iosProcessingSafetyTimeoutSecs\":60.0"),
567            "JSON should contain iosProcessingSafetyTimeoutSecs: {json}"
568        );
569    }
570
571    #[test]
572    fn start_keepalive_args_no_processing_timeout() {
573        let args = StartKeepaliveArgs {
574            label: "Test",
575            foreground_service_type: "dataSync",
576            ios_safety_timeout_secs: None,
577            ios_processing_safety_timeout_secs: None,
578        };
579        let json = serde_json::to_string(&args).unwrap();
580        assert!(
581            !json.contains("iosProcessingSafetyTimeoutSecs"),
582            "JSON should NOT contain iosProcessingSafetyTimeoutSecs when None: {json}"
583        );
584    }
585
586    #[test]
587    fn start_keepalive_args_camel_case_keys() {
588        let args = StartKeepaliveArgs {
589            label: "Test",
590            foreground_service_type: "specialUse",
591            ios_safety_timeout_secs: None,
592            ios_processing_safety_timeout_secs: None,
593        };
594        let json = serde_json::to_string(&args).unwrap();
595        assert!(json.contains("\"label\""), "label: {json}");
596        assert!(
597            json.contains("\"foregroundServiceType\""),
598            "foregroundServiceType: {json}"
599        );
600    }
601
602    // --- PluginConfig desktop fields tests (feature-gated) ---
603
604    #[cfg(feature = "desktop-service")]
605    #[test]
606    fn plugin_config_desktop_mode_default() {
607        let json = "{}";
608        let config: PluginConfig = serde_json::from_str(json).unwrap();
609        assert_eq!(config.desktop_service_mode, "inProcess");
610    }
611
612    #[cfg(feature = "desktop-service")]
613    #[test]
614    fn plugin_config_desktop_mode_custom() {
615        let json = r#"{"desktopServiceMode":"osService"}"#;
616        let config: PluginConfig = serde_json::from_str(json).unwrap();
617        assert_eq!(config.desktop_service_mode, "osService");
618    }
619
620    #[cfg(feature = "desktop-service")]
621    #[test]
622    fn plugin_config_desktop_mode_serde_roundtrip() {
623        let config = PluginConfig {
624            desktop_service_mode: "osService".into(),
625            ..Default::default()
626        };
627        let json = serde_json::to_string(&config).unwrap();
628        let de: PluginConfig = serde_json::from_str(&json).unwrap();
629        assert_eq!(de.desktop_service_mode, "osService");
630    }
631
632    #[cfg(feature = "desktop-service")]
633    #[test]
634    fn plugin_config_desktop_label_default() {
635        let json = "{}";
636        let config: PluginConfig = serde_json::from_str(json).unwrap();
637        assert_eq!(config.desktop_service_label, None);
638    }
639
640    #[cfg(feature = "desktop-service")]
641    #[test]
642    fn plugin_config_desktop_label_custom() {
643        let json = r#"{"desktopServiceLabel":"my.svc"}"#;
644        let config: PluginConfig = serde_json::from_str(json).unwrap();
645        assert_eq!(config.desktop_service_label, Some("my.svc".to_string()));
646    }
647
648    use tauri::AppHandle;
649
650    // --- ServiceContext new fields tests ---
651
652    /// Compile-time + runtime test: ServiceContext accepts the new optional fields.
653    #[allow(dead_code)]
654    fn service_context_new_fields_default_to_none<R: Runtime>(app: AppHandle<R>) {
655        let ctx = ServiceContext {
656            notifier: Notifier { app: app.clone() },
657            app,
658            shutdown: CancellationToken::new(),
659            service_label: None,
660            foreground_service_type: None,
661        };
662        assert_eq!(ctx.service_label, None);
663        assert_eq!(ctx.foreground_service_type, None);
664    }
665
666    /// Compile-time + runtime test: ServiceContext carries label and type values.
667    #[allow(dead_code)]
668    fn service_context_new_fields_with_values<R: Runtime>(app: AppHandle<R>) {
669        let ctx = ServiceContext {
670            notifier: Notifier { app: app.clone() },
671            app,
672            shutdown: CancellationToken::new(),
673            service_label: Some("Syncing".into()),
674            foreground_service_type: Some("dataSync".into()),
675        };
676        assert_eq!(ctx.service_label.as_deref(), Some("Syncing"));
677        assert_eq!(ctx.foreground_service_type.as_deref(), Some("dataSync"));
678    }
679}