1use serde::{Deserialize, Serialize};
8use tauri::Runtime;
9use tokio_util::sync::CancellationToken;
10
11use crate::error::ServiceError;
12use crate::notifier::Notifier;
13
14pub const VALID_FOREGROUND_SERVICE_TYPES: &[&str] = &[
20 "dataSync",
21 "mediaPlayback",
22 "phoneCall",
23 "location",
24 "connectedDevice",
25 "mediaProjection",
26 "camera",
27 "microphone",
28 "health",
29 "remoteMessaging",
30 "systemExempted",
31 "shortService",
32 "specialUse",
33 "mediaProcessing",
34];
35
36pub fn validate_foreground_service_type(t: &str) -> Result<(), ServiceError> {
41 if VALID_FOREGROUND_SERVICE_TYPES.contains(&t) {
42 Ok(())
43 } else {
44 Err(ServiceError::Platform(format!(
45 "invalid foreground_service_type '{}'. Valid types: {:?}",
46 t, VALID_FOREGROUND_SERVICE_TYPES
47 )))
48 }
49}
50
51pub struct ServiceContext<R: Runtime> {
54 pub notifier: Notifier<R>,
56
57 pub app: tauri::AppHandle<R>,
59
60 pub shutdown: CancellationToken,
62
63 pub service_label: Option<String>,
66
67 pub foreground_service_type: Option<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct StartConfig {
76 #[serde(default = "default_label")]
78 pub service_label: String,
79
80 #[serde(default = "default_foreground_service_type")]
82 pub foreground_service_type: String,
83}
84
85fn default_label() -> String {
86 "Service running".into()
87}
88
89fn default_foreground_service_type() -> String {
90 "dataSync".into()
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct PluginConfig {
97 #[serde(default = "default_ios_safety_timeout")]
100 pub ios_safety_timeout_secs: f64,
101
102 #[serde(default = "default_ios_cancel_listener_timeout_secs")]
105 pub ios_cancel_listener_timeout_secs: u64,
106
107 #[serde(default = "default_ios_processing_safety_timeout_secs")]
112 pub ios_processing_safety_timeout_secs: f64,
113
114 #[serde(default = "default_ios_earliest_refresh_begin_minutes")]
117 pub ios_earliest_refresh_begin_minutes: f64,
118
119 #[serde(default = "default_ios_earliest_processing_begin_minutes")]
122 pub ios_earliest_processing_begin_minutes: f64,
123
124 #[serde(default)]
128 pub ios_requires_external_power: bool,
129
130 #[serde(default)]
134 pub ios_requires_network_connectivity: bool,
135
136 #[serde(default = "default_channel_capacity")]
140 pub channel_capacity: usize,
141
142 #[cfg(feature = "desktop-service")]
146 #[serde(default = "default_desktop_service_mode")]
147 pub desktop_service_mode: String,
148
149 #[cfg(feature = "desktop-service")]
152 #[serde(default)]
153 pub desktop_service_label: Option<String>,
154}
155
156fn default_ios_safety_timeout() -> f64 {
157 28.0
158}
159
160fn default_ios_cancel_listener_timeout_secs() -> u64 {
161 14400
162}
163
164fn default_ios_processing_safety_timeout_secs() -> f64 {
165 0.0
166}
167
168fn default_ios_earliest_refresh_begin_minutes() -> f64 {
169 15.0
170}
171
172fn default_ios_earliest_processing_begin_minutes() -> f64 {
173 15.0
174}
175
176fn default_channel_capacity() -> usize {
177 16
178}
179
180#[cfg(feature = "desktop-service")]
181fn default_desktop_service_mode() -> String {
182 "inProcess".into()
183}
184
185impl Default for StartConfig {
186 fn default() -> Self {
187 Self {
188 service_label: default_label(),
189 foreground_service_type: default_foreground_service_type(),
190 }
191 }
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase", tag = "type")]
197#[non_exhaustive]
198pub enum PluginEvent {
199 Started,
201 Stopped { reason: String },
203 Error { message: String },
205}
206
207impl Default for PluginConfig {
208 fn default() -> Self {
209 Self {
210 ios_safety_timeout_secs: default_ios_safety_timeout(),
211 ios_cancel_listener_timeout_secs: default_ios_cancel_listener_timeout_secs(),
212 ios_processing_safety_timeout_secs: default_ios_processing_safety_timeout_secs(),
213 ios_earliest_refresh_begin_minutes: default_ios_earliest_refresh_begin_minutes(),
214 ios_earliest_processing_begin_minutes: default_ios_earliest_processing_begin_minutes(),
215 ios_requires_external_power: false,
216 ios_requires_network_connectivity: false,
217 channel_capacity: default_channel_capacity(),
218 #[cfg(feature = "desktop-service")]
219 desktop_service_mode: default_desktop_service_mode(),
220 #[cfg(feature = "desktop-service")]
221 desktop_service_label: None,
222 }
223 }
224}
225
226#[derive(Debug, Serialize)]
230#[serde(rename_all = "camelCase")]
231#[allow(dead_code)]
232pub(crate) struct StartKeepaliveArgs<'a> {
233 pub label: &'a str,
234 pub foreground_service_type: &'a str,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub ios_safety_timeout_secs: Option<f64>,
238 #[serde(skip_serializing_if = "Option::is_none")]
241 pub ios_processing_safety_timeout_secs: Option<f64>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub ios_earliest_refresh_begin_minutes: Option<f64>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub ios_earliest_processing_begin_minutes: Option<f64>,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub ios_requires_external_power: Option<bool>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub ios_requires_network_connectivity: Option<bool>,
254}
255
256#[doc(hidden)]
261#[derive(Debug, Clone, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct AutoStartConfig {
264 pub pending: bool,
265 pub label: Option<String>,
266 pub service_type: Option<String>,
267}
268
269impl AutoStartConfig {
270 pub fn into_start_config(self) -> Option<StartConfig> {
272 if self.pending {
273 self.label.map(|label| StartConfig {
274 service_label: label,
275 foreground_service_type: self
276 .service_type
277 .unwrap_or_else(default_foreground_service_type),
278 })
279 } else {
280 None
281 }
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
292 fn start_config_default_label() {
293 let config = StartConfig::default();
294 assert_eq!(config.service_label, "Service running");
295 }
296
297 #[test]
298 fn start_config_custom_label() {
299 let config = StartConfig {
300 service_label: "Syncing data".into(),
301 ..Default::default()
302 };
303 assert_eq!(config.service_label, "Syncing data");
304 }
305
306 #[test]
307 fn start_config_serde_roundtrip_default() {
308 let config = StartConfig::default();
309 let json = serde_json::to_string(&config).unwrap();
310 let de: StartConfig = serde_json::from_str(&json).unwrap();
311 assert_eq!(de.service_label, config.service_label);
312 }
313
314 #[test]
315 fn start_config_serde_roundtrip_custom() {
316 let config = StartConfig {
317 service_label: "My service".into(),
318 ..Default::default()
319 };
320 let json = serde_json::to_string(&config).unwrap();
321 let de: StartConfig = serde_json::from_str(&json).unwrap();
322 assert_eq!(de.service_label, "My service");
323 }
324
325 #[test]
326 fn start_config_deserialize_missing_field_uses_default() {
327 let json = "{}";
329 let de: StartConfig = serde_json::from_str(json).unwrap();
330 assert_eq!(de.service_label, "Service running");
331 }
332
333 #[test]
334 fn start_config_json_key_is_camel_case() {
335 let config = StartConfig {
336 service_label: "test".into(),
337 ..Default::default()
338 };
339 let json = serde_json::to_string(&config).unwrap();
340 assert!(
341 json.contains("serviceLabel"),
342 "JSON should use camelCase: {json}"
343 );
344 }
345
346 #[test]
349 fn plugin_event_started_serde_roundtrip() {
350 let event = PluginEvent::Started;
351 let json = serde_json::to_string(&event).unwrap();
352 let de: PluginEvent = serde_json::from_str(&json).unwrap();
353 assert!(matches!(de, PluginEvent::Started));
354 }
355
356 #[test]
357 fn plugin_event_stopped_serde_roundtrip() {
358 let event = PluginEvent::Stopped {
359 reason: "cancelled".into(),
360 };
361 let json = serde_json::to_string(&event).unwrap();
362 let de: PluginEvent = serde_json::from_str(&json).unwrap();
363 match de {
364 PluginEvent::Stopped { reason } => assert_eq!(reason, "cancelled"),
365 other => panic!("Expected Stopped, got {other:?}"),
366 }
367 }
368
369 #[test]
370 fn plugin_event_error_serde_roundtrip() {
371 let event = PluginEvent::Error {
372 message: "init failed".into(),
373 };
374 let json = serde_json::to_string(&event).unwrap();
375 let de: PluginEvent = serde_json::from_str(&json).unwrap();
376 match de {
377 PluginEvent::Error { message } => assert_eq!(message, "init failed"),
378 other => panic!("Expected Error, got {other:?}"),
379 }
380 }
381
382 #[test]
383 fn plugin_event_tagged_json_format() {
384 let event = PluginEvent::Started;
385 let json = serde_json::to_string(&event).unwrap();
386 assert!(json.contains("\"type\":\"started\""), "Tagged JSON: {json}");
387 }
388
389 #[test]
390 fn plugin_event_stopped_json_keys_camel_case() {
391 let event = PluginEvent::Stopped {
392 reason: "done".into(),
393 };
394 let json = serde_json::to_string(&event).unwrap();
395 assert!(json.contains("\"type\":\"stopped\""), "Tag: {json}");
396 assert!(json.contains("\"reason\":\"done\""), "Reason: {json}");
397 }
398
399 #[test]
400 fn plugin_event_error_json_keys_camel_case() {
401 let event = PluginEvent::Error {
402 message: "oops".into(),
403 };
404 let json = serde_json::to_string(&event).unwrap();
405 assert!(json.contains("\"type\":\"error\""), "Tag: {json}");
406 assert!(json.contains("\"message\":\"oops\""), "Message: {json}");
407 }
408
409 #[test]
412 fn start_config_default_service_type() {
413 let config = StartConfig::default();
414 assert_eq!(config.foreground_service_type, "dataSync");
415 }
416
417 #[test]
418 fn start_config_custom_service_type() {
419 let config = StartConfig {
420 service_label: "test".into(),
421 foreground_service_type: "specialUse".into(),
422 };
423 assert_eq!(config.foreground_service_type, "specialUse");
424 }
425
426 #[test]
427 fn start_config_serde_roundtrip_service_type() {
428 let config = StartConfig {
429 service_label: "test".into(),
430 foreground_service_type: "specialUse".into(),
431 };
432 let json = serde_json::to_string(&config).unwrap();
433 let de: StartConfig = serde_json::from_str(&json).unwrap();
434 assert_eq!(de.foreground_service_type, "specialUse");
435 }
436
437 #[test]
438 fn start_config_deserialize_missing_service_type() {
439 let json = r#"{"serviceLabel":"test"}"#;
440 let de: StartConfig = serde_json::from_str(json).unwrap();
441 assert_eq!(de.foreground_service_type, "dataSync");
442 }
443
444 #[test]
445 fn start_config_deserialize_special_use() {
446 let json = r#"{"serviceLabel":"test","foregroundServiceType":"specialUse"}"#;
447 let de: StartConfig = serde_json::from_str(json).unwrap();
448 assert_eq!(de.foreground_service_type, "specialUse");
449 }
450
451 #[test]
452 fn start_config_unrecognized_type_rejected_by_validation() {
453 let json = r#"{"serviceLabel":"test","foregroundServiceType":"customType"}"#;
455 let de: StartConfig = serde_json::from_str(json).unwrap();
456 assert_eq!(de.foreground_service_type, "customType");
457 let result = validate_foreground_service_type(&de.foreground_service_type);
459 assert!(
460 result.is_err(),
461 "validation should reject unrecognized type"
462 );
463 let err_msg = result.unwrap_err().to_string();
464 assert!(
465 err_msg.contains("customType"),
466 "error should mention the invalid type: {err_msg}"
467 );
468 }
469
470 #[test]
471 fn start_config_json_key_is_camel_case_service_type() {
472 let config = StartConfig {
473 service_label: "test".into(),
474 foreground_service_type: "specialUse".into(),
475 };
476 let json = serde_json::to_string(&config).unwrap();
477 assert!(
478 json.contains("foregroundServiceType"),
479 "JSON should use camelCase: {json}"
480 );
481 }
482
483 #[test]
486 fn auto_start_config_pending_with_label_returns_start_config() {
487 let json = r#"{"pending": true, "label": "Syncing"}"#;
488 let config: AutoStartConfig = serde_json::from_str(json).unwrap();
489 let result = config.into_start_config();
490 assert!(result.is_some());
491 let start_config = result.unwrap();
492 assert_eq!(start_config.service_label, "Syncing");
493 assert_eq!(start_config.foreground_service_type, "dataSync");
494 }
495
496 #[test]
497 fn auto_start_config_not_pending_returns_none() {
498 let json = r#"{"pending": false, "label": null}"#;
499 let config: AutoStartConfig = serde_json::from_str(json).unwrap();
500 let result = config.into_start_config();
501 assert!(result.is_none());
502 }
503
504 #[test]
505 fn auto_start_config_pending_no_label_returns_none() {
506 let json = r#"{"pending": true, "label": null}"#;
507 let config: AutoStartConfig = serde_json::from_str(json).unwrap();
508 let result = config.into_start_config();
509 assert!(result.is_none());
510 }
511
512 #[test]
513 fn auto_start_config_with_service_type_preserves_it() {
514 let json = r#"{"pending":true,"label":"test","serviceType":"specialUse"}"#;
515 let config: AutoStartConfig = serde_json::from_str(json).unwrap();
516 assert_eq!(config.service_type, Some("specialUse".to_string()));
517 let result = config.into_start_config();
518 assert!(result.is_some());
519 let start_config = result.unwrap();
520 assert_eq!(start_config.foreground_service_type, "specialUse");
521 }
522
523 #[test]
524 fn auto_start_config_without_service_type_uses_default() {
525 let json = r#"{"pending":true,"label":"test"}"#;
526 let config: AutoStartConfig = serde_json::from_str(json).unwrap();
527 assert_eq!(config.service_type, None);
528 let result = config.into_start_config();
529 assert!(result.is_some());
530 assert_eq!(result.unwrap().foreground_service_type, "dataSync");
531 }
532
533 #[test]
534 fn auto_start_config_null_service_type_uses_default() {
535 let json = r#"{"pending":true,"label":"test","serviceType":null}"#;
536 let config: AutoStartConfig = serde_json::from_str(json).unwrap();
537 assert_eq!(config.service_type, None);
538 let result = config.into_start_config();
539 assert!(result.is_some());
540 assert_eq!(result.unwrap().foreground_service_type, "dataSync");
541 }
542
543 #[test]
546 fn plugin_config_default_ios_safety_timeout() {
547 let json = "{}";
548 let config: PluginConfig = serde_json::from_str(json).unwrap();
549 assert_eq!(config.ios_safety_timeout_secs, 28.0);
550 }
551
552 #[test]
553 fn plugin_config_custom_ios_safety_timeout() {
554 let json = r#"{"iosSafetyTimeoutSecs":15.0}"#;
555 let config: PluginConfig = serde_json::from_str(json).unwrap();
556 assert_eq!(config.ios_safety_timeout_secs, 15.0);
557 }
558
559 #[test]
560 fn plugin_config_serde_roundtrip_preserves_value() {
561 let config = PluginConfig {
562 ios_safety_timeout_secs: 30.0,
563 ios_cancel_listener_timeout_secs: 14400,
564 ios_processing_safety_timeout_secs: 0.0,
565 ios_earliest_refresh_begin_minutes: 20.0,
566 ios_earliest_processing_begin_minutes: 30.0,
567 ios_requires_external_power: true,
568 ios_requires_network_connectivity: true,
569 ..Default::default()
570 };
571 let json = serde_json::to_string(&config).unwrap();
572 let de: PluginConfig = serde_json::from_str(&json).unwrap();
573 assert_eq!(de.ios_safety_timeout_secs, 30.0);
574 assert_eq!(de.ios_earliest_refresh_begin_minutes, 20.0);
575 assert_eq!(de.ios_earliest_processing_begin_minutes, 30.0);
576 assert!(de.ios_requires_external_power);
577 assert!(de.ios_requires_network_connectivity);
578 }
579
580 #[test]
581 fn plugin_config_default_impl() {
582 let config = PluginConfig::default();
583 assert_eq!(config.ios_safety_timeout_secs, 28.0);
584 assert_eq!(config.channel_capacity, 16);
585 }
586
587 #[test]
588 fn plugin_config_default_cancel_timeout() {
589 let json = "{}";
590 let config: PluginConfig = serde_json::from_str(json).unwrap();
591 assert_eq!(config.ios_cancel_listener_timeout_secs, 14400);
592 }
593
594 #[test]
595 fn plugin_config_custom_cancel_timeout() {
596 let json = r#"{"iosCancelListenerTimeoutSecs":7200}"#;
597 let config: PluginConfig = serde_json::from_str(json).unwrap();
598 assert_eq!(config.ios_cancel_listener_timeout_secs, 7200);
599 }
600
601 #[test]
602 fn plugin_config_cancel_timeout_serde_roundtrip() {
603 let config = PluginConfig {
604 ios_cancel_listener_timeout_secs: 3600,
605 ..Default::default()
606 };
607 let json = serde_json::to_string(&config).unwrap();
608 let de: PluginConfig = serde_json::from_str(&json).unwrap();
609 assert_eq!(de.ios_cancel_listener_timeout_secs, 3600);
610 }
611
612 #[test]
615 fn plugin_config_processing_timeout_default() {
616 let json = "{}";
617 let config: PluginConfig = serde_json::from_str(json).unwrap();
618 assert_eq!(config.ios_processing_safety_timeout_secs, 0.0);
619 }
620
621 #[test]
622 fn plugin_config_processing_timeout_custom() {
623 let json = r#"{"iosProcessingSafetyTimeoutSecs":60.0}"#;
624 let config: PluginConfig = serde_json::from_str(json).unwrap();
625 assert_eq!(config.ios_processing_safety_timeout_secs, 60.0);
626 }
627
628 #[test]
629 fn plugin_config_processing_timeout_serde_roundtrip() {
630 let config = PluginConfig {
631 ios_processing_safety_timeout_secs: 120.0,
632 ..Default::default()
633 };
634 let json = serde_json::to_string(&config).unwrap();
635 let de: PluginConfig = serde_json::from_str(&json).unwrap();
636 assert_eq!(de.ios_processing_safety_timeout_secs, 120.0);
637 }
638
639 #[test]
642 fn start_keepalive_args_with_timeout() {
643 let args = StartKeepaliveArgs {
644 label: "Test",
645 foreground_service_type: "dataSync",
646 ios_safety_timeout_secs: Some(15.0),
647 ios_processing_safety_timeout_secs: None,
648 ios_earliest_refresh_begin_minutes: None,
649 ios_earliest_processing_begin_minutes: None,
650 ios_requires_external_power: None,
651 ios_requires_network_connectivity: None,
652 };
653 let json = serde_json::to_string(&args).unwrap();
654 assert!(
655 json.contains("\"iosSafetyTimeoutSecs\":15.0"),
656 "JSON should contain iosSafetyTimeoutSecs: {json}"
657 );
658 }
659
660 #[test]
661 fn start_keepalive_args_without_timeout() {
662 let args = StartKeepaliveArgs {
663 label: "Test",
664 foreground_service_type: "dataSync",
665 ios_safety_timeout_secs: None,
666 ios_processing_safety_timeout_secs: None,
667 ios_earliest_refresh_begin_minutes: None,
668 ios_earliest_processing_begin_minutes: None,
669 ios_requires_external_power: None,
670 ios_requires_network_connectivity: None,
671 };
672 let json = serde_json::to_string(&args).unwrap();
673 assert!(
674 !json.contains("iosSafetyTimeoutSecs"),
675 "JSON should NOT contain iosSafetyTimeoutSecs when None: {json}"
676 );
677 }
678
679 #[test]
680 fn start_keepalive_args_processing_timeout() {
681 let args = StartKeepaliveArgs {
682 label: "Test",
683 foreground_service_type: "dataSync",
684 ios_safety_timeout_secs: None,
685 ios_processing_safety_timeout_secs: Some(60.0),
686 ios_earliest_refresh_begin_minutes: None,
687 ios_earliest_processing_begin_minutes: None,
688 ios_requires_external_power: None,
689 ios_requires_network_connectivity: None,
690 };
691 let json = serde_json::to_string(&args).unwrap();
692 assert!(
693 json.contains("\"iosProcessingSafetyTimeoutSecs\":60.0"),
694 "JSON should contain iosProcessingSafetyTimeoutSecs: {json}"
695 );
696 }
697
698 #[test]
699 fn start_keepalive_args_no_processing_timeout() {
700 let args = StartKeepaliveArgs {
701 label: "Test",
702 foreground_service_type: "dataSync",
703 ios_safety_timeout_secs: None,
704 ios_processing_safety_timeout_secs: None,
705 ios_earliest_refresh_begin_minutes: None,
706 ios_earliest_processing_begin_minutes: None,
707 ios_requires_external_power: None,
708 ios_requires_network_connectivity: None,
709 };
710 let json = serde_json::to_string(&args).unwrap();
711 assert!(
712 !json.contains("iosProcessingSafetyTimeoutSecs"),
713 "JSON should NOT contain iosProcessingSafetyTimeoutSecs when None: {json}"
714 );
715 }
716
717 #[test]
718 fn start_keepalive_args_camel_case_keys() {
719 let args = StartKeepaliveArgs {
720 label: "Test",
721 foreground_service_type: "specialUse",
722 ios_safety_timeout_secs: None,
723 ios_processing_safety_timeout_secs: None,
724 ios_earliest_refresh_begin_minutes: None,
725 ios_earliest_processing_begin_minutes: None,
726 ios_requires_external_power: None,
727 ios_requires_network_connectivity: None,
728 };
729 let json = serde_json::to_string(&args).unwrap();
730 assert!(json.contains("\"label\""), "label: {json}");
731 assert!(
732 json.contains("\"foregroundServiceType\""),
733 "foregroundServiceType: {json}"
734 );
735 }
736
737 #[test]
738 fn start_keepalive_args_scheduling_intervals() {
739 let args = StartKeepaliveArgs {
740 label: "Test",
741 foreground_service_type: "dataSync",
742 ios_safety_timeout_secs: None,
743 ios_processing_safety_timeout_secs: None,
744 ios_earliest_refresh_begin_minutes: Some(30.0),
745 ios_earliest_processing_begin_minutes: Some(60.0),
746 ios_requires_external_power: None,
747 ios_requires_network_connectivity: None,
748 };
749 let json = serde_json::to_string(&args).unwrap();
750 assert!(
751 json.contains("\"iosEarliestRefreshBeginMinutes\":30.0"),
752 "JSON should contain iosEarliestRefreshBeginMinutes: {json}"
753 );
754 assert!(
755 json.contains("\"iosEarliestProcessingBeginMinutes\":60.0"),
756 "JSON should contain iosEarliestProcessingBeginMinutes: {json}"
757 );
758 }
759
760 #[test]
761 fn start_keepalive_args_processing_options() {
762 let args = StartKeepaliveArgs {
763 label: "Test",
764 foreground_service_type: "dataSync",
765 ios_safety_timeout_secs: None,
766 ios_processing_safety_timeout_secs: None,
767 ios_earliest_refresh_begin_minutes: None,
768 ios_earliest_processing_begin_minutes: None,
769 ios_requires_external_power: Some(true),
770 ios_requires_network_connectivity: Some(true),
771 };
772 let json = serde_json::to_string(&args).unwrap();
773 assert!(
774 json.contains("\"iosRequiresExternalPower\":true"),
775 "JSON should contain iosRequiresExternalPower: {json}"
776 );
777 assert!(
778 json.contains("\"iosRequiresNetworkConnectivity\":true"),
779 "JSON should contain iosRequiresNetworkConnectivity: {json}"
780 );
781 }
782
783 #[test]
786 fn plugin_config_earliest_refresh_default() {
787 let json = "{}";
788 let config: PluginConfig = serde_json::from_str(json).unwrap();
789 assert_eq!(config.ios_earliest_refresh_begin_minutes, 15.0);
790 }
791
792 #[test]
793 fn plugin_config_earliest_processing_default() {
794 let json = "{}";
795 let config: PluginConfig = serde_json::from_str(json).unwrap();
796 assert_eq!(config.ios_earliest_processing_begin_minutes, 15.0);
797 }
798
799 #[test]
800 fn plugin_config_requires_external_power_default() {
801 let json = "{}";
802 let config: PluginConfig = serde_json::from_str(json).unwrap();
803 assert!(!config.ios_requires_external_power);
804 }
805
806 #[test]
807 fn plugin_config_requires_network_connectivity_default() {
808 let json = "{}";
809 let config: PluginConfig = serde_json::from_str(json).unwrap();
810 assert!(!config.ios_requires_network_connectivity);
811 }
812
813 #[test]
814 fn plugin_config_custom_scheduling_intervals() {
815 let json =
816 r#"{"iosEarliestRefreshBeginMinutes":30.0,"iosEarliestProcessingBeginMinutes":60.0}"#;
817 let config: PluginConfig = serde_json::from_str(json).unwrap();
818 assert_eq!(config.ios_earliest_refresh_begin_minutes, 30.0);
819 assert_eq!(config.ios_earliest_processing_begin_minutes, 60.0);
820 }
821
822 #[test]
823 fn plugin_config_custom_processing_options() {
824 let json = r#"{"iosRequiresExternalPower":true,"iosRequiresNetworkConnectivity":true}"#;
825 let config: PluginConfig = serde_json::from_str(json).unwrap();
826 assert!(config.ios_requires_external_power);
827 assert!(config.ios_requires_network_connectivity);
828 }
829
830 #[test]
833 fn plugin_config_channel_capacity_default() {
834 let json = "{}";
835 let config: PluginConfig = serde_json::from_str(json).unwrap();
836 assert_eq!(config.channel_capacity, 16);
837 }
838
839 #[test]
840 fn plugin_config_channel_capacity_custom() {
841 let json = r#"{"channelCapacity":32}"#;
842 let config: PluginConfig = serde_json::from_str(json).unwrap();
843 assert_eq!(config.channel_capacity, 32);
844 }
845
846 #[test]
847 fn plugin_config_channel_capacity_serde_roundtrip() {
848 let config = PluginConfig {
849 channel_capacity: 64,
850 ..Default::default()
851 };
852 let json = serde_json::to_string(&config).unwrap();
853 let de: PluginConfig = serde_json::from_str(&json).unwrap();
854 assert_eq!(de.channel_capacity, 64);
855 }
856
857 #[test]
858 fn plugin_config_channel_capacity_json_key_camel_case() {
859 let config = PluginConfig {
860 channel_capacity: 32,
861 ..Default::default()
862 };
863 let json = serde_json::to_string(&config).unwrap();
864 assert!(
865 json.contains("channelCapacity"),
866 "JSON should use camelCase: {json}"
867 );
868 }
869
870 #[cfg(feature = "desktop-service")]
873 #[test]
874 fn plugin_config_desktop_mode_default() {
875 let json = "{}";
876 let config: PluginConfig = serde_json::from_str(json).unwrap();
877 assert_eq!(config.desktop_service_mode, "inProcess");
878 }
879
880 #[cfg(feature = "desktop-service")]
881 #[test]
882 fn plugin_config_desktop_mode_custom() {
883 let json = r#"{"desktopServiceMode":"osService"}"#;
884 let config: PluginConfig = serde_json::from_str(json).unwrap();
885 assert_eq!(config.desktop_service_mode, "osService");
886 }
887
888 #[cfg(feature = "desktop-service")]
889 #[test]
890 fn plugin_config_desktop_mode_serde_roundtrip() {
891 let config = PluginConfig {
892 desktop_service_mode: "osService".into(),
893 ..Default::default()
894 };
895 let json = serde_json::to_string(&config).unwrap();
896 let de: PluginConfig = serde_json::from_str(&json).unwrap();
897 assert_eq!(de.desktop_service_mode, "osService");
898 }
899
900 #[cfg(feature = "desktop-service")]
901 #[test]
902 fn plugin_config_desktop_label_default() {
903 let json = "{}";
904 let config: PluginConfig = serde_json::from_str(json).unwrap();
905 assert_eq!(config.desktop_service_label, None);
906 }
907
908 #[cfg(feature = "desktop-service")]
909 #[test]
910 fn plugin_config_desktop_label_custom() {
911 let json = r#"{"desktopServiceLabel":"my.svc"}"#;
912 let config: PluginConfig = serde_json::from_str(json).unwrap();
913 assert_eq!(config.desktop_service_label, Some("my.svc".to_string()));
914 }
915
916 use tauri::AppHandle;
917
918 #[allow(dead_code)]
922 fn service_context_new_fields_default_to_none<R: Runtime>(app: AppHandle<R>) {
923 let ctx = ServiceContext {
924 notifier: Notifier { app: app.clone() },
925 app,
926 shutdown: CancellationToken::new(),
927 service_label: None,
928 foreground_service_type: None,
929 };
930 assert_eq!(ctx.service_label, None);
931 assert_eq!(ctx.foreground_service_type, None);
932 }
933
934 #[allow(dead_code)]
936 fn service_context_new_fields_with_values<R: Runtime>(app: AppHandle<R>) {
937 let ctx = ServiceContext {
938 notifier: Notifier { app: app.clone() },
939 app,
940 shutdown: CancellationToken::new(),
941 service_label: Some("Syncing".into()),
942 foreground_service_type: Some("dataSync".into()),
943 };
944 assert_eq!(ctx.service_label.as_deref(), Some("Syncing"));
945 assert_eq!(ctx.foreground_service_type.as_deref(), Some("dataSync"));
946 }
947
948 #[test]
951 fn validate_data_sync_passes() {
952 assert!(
953 validate_foreground_service_type("dataSync").is_ok(),
954 "dataSync should be valid"
955 );
956 }
957
958 #[test]
959 fn validate_special_use_passes() {
960 assert!(
961 validate_foreground_service_type("specialUse").is_ok(),
962 "specialUse should be valid"
963 );
964 }
965
966 #[test]
967 fn validate_invalid_type_returns_platform_error() {
968 let result = validate_foreground_service_type("invalidType");
969 assert!(result.is_err(), "invalidType should be rejected");
970 match result {
971 Err(crate::error::ServiceError::Platform(msg)) => {
972 assert!(
973 msg.contains("invalidType"),
974 "error should mention the type: {msg}"
975 );
976 }
977 other => panic!("Expected Platform error, got: {other:?}"),
978 }
979 }
980
981 #[test]
982 fn validate_all_14_types_pass() {
983 for &t in VALID_FOREGROUND_SERVICE_TYPES {
984 assert!(
985 validate_foreground_service_type(t).is_ok(),
986 "{t} should be valid"
987 );
988 }
989 }
990
991 #[test]
992 fn valid_types_count_is_14() {
993 assert_eq!(
994 VALID_FOREGROUND_SERVICE_TYPES.len(),
995 14,
996 "should have exactly 14 valid types"
997 );
998 }
999
1000 #[test]
1001 fn validate_empty_string_returns_error() {
1002 let result = validate_foreground_service_type("");
1003 assert!(result.is_err(), "empty string should be rejected");
1004 }
1005
1006 #[test]
1007 fn validate_case_sensitive() {
1008 let result = validate_foreground_service_type("DataSync");
1010 assert!(
1011 result.is_err(),
1012 "validation should be case-sensitive: DataSync should fail"
1013 );
1014 }
1015}