Skip to main content

tauri_plugin_notifications/
models.rs

1use std::{collections::HashMap, fmt::Display};
2
3use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
4use tauri::plugin::PermissionState;
5
6use url::Url;
7
8#[derive(Debug, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct PermissionResponse {
11    pub permission_state: PermissionState,
12}
13
14#[cfg(feature = "push-notifications")]
15#[derive(Debug, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct PushNotificationResponse {
18    pub device_token: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct Attachment {
24    id: String,
25    url: Url,
26}
27
28impl Attachment {
29    pub fn new(id: impl Into<String>, url: Url) -> Self {
30        Self { id: id.into(), url }
31    }
32
33    #[must_use]
34    pub fn id(&self) -> &str {
35        &self.id
36    }
37
38    #[must_use]
39    pub const fn url(&self) -> &Url {
40        &self.url
41    }
42}
43
44#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct ScheduleInterval {
47    pub year: Option<u8>,
48    pub month: Option<u8>,
49    pub day: Option<u8>,
50    pub weekday: Option<u8>,
51    pub hour: Option<u8>,
52    pub minute: Option<u8>,
53    pub second: Option<u8>,
54}
55
56#[derive(Debug, Clone, Copy)]
57pub enum ScheduleEvery {
58    Year,
59    Month,
60    TwoWeeks,
61    Week,
62    Day,
63    Hour,
64    Minute,
65    Second,
66}
67
68impl Display for ScheduleEvery {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(
71            f,
72            "{}",
73            match self {
74                Self::Year => "year",
75                Self::Month => "month",
76                Self::TwoWeeks => "twoWeeks",
77                Self::Week => "week",
78                Self::Day => "day",
79                Self::Hour => "hour",
80                Self::Minute => "minute",
81                Self::Second => "second",
82            }
83        )
84    }
85}
86
87impl Serialize for ScheduleEvery {
88    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        serializer.serialize_str(self.to_string().as_ref())
93    }
94}
95
96impl<'de> Deserialize<'de> for ScheduleEvery {
97    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
98    where
99        D: Deserializer<'de>,
100    {
101        let s = String::deserialize(deserializer)?;
102        match s.to_lowercase().as_str() {
103            "year" => Ok(Self::Year),
104            "month" => Ok(Self::Month),
105            "twoweeks" => Ok(Self::TwoWeeks),
106            "week" => Ok(Self::Week),
107            "day" => Ok(Self::Day),
108            "hour" => Ok(Self::Hour),
109            "minute" => Ok(Self::Minute),
110            "second" => Ok(Self::Second),
111            _ => Err(DeError::custom(format!("unknown every kind '{s}'"))),
112        }
113    }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub enum Schedule {
119    #[serde(rename_all = "camelCase")]
120    At {
121        #[serde(
122            serialize_with = "iso8601::serialize",
123            deserialize_with = "time::serde::iso8601::deserialize"
124        )]
125        date: time::OffsetDateTime,
126        #[serde(default)]
127        repeating: bool,
128        #[serde(default)]
129        allow_while_idle: bool,
130    },
131    #[serde(rename_all = "camelCase")]
132    Interval {
133        interval: ScheduleInterval,
134        #[serde(default)]
135        allow_while_idle: bool,
136    },
137    #[serde(rename_all = "camelCase")]
138    Every {
139        interval: ScheduleEvery,
140        count: u8,
141        #[serde(default)]
142        allow_while_idle: bool,
143    },
144}
145
146// custom ISO-8601 serialization that does not use 6 digits for years.
147mod iso8601 {
148    use serde::{ser::Error as _, Serialize, Serializer};
149    use time::{
150        format_description::well_known::iso8601::{Config, EncodedConfig},
151        format_description::well_known::Iso8601,
152        OffsetDateTime,
153    };
154
155    const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode();
156
157    pub fn serialize<S: Serializer>(
158        datetime: &OffsetDateTime,
159        serializer: S,
160    ) -> Result<S::Ok, S::Error> {
161        datetime
162            .format(&Iso8601::<SERDE_CONFIG>)
163            .map_err(S::Error::custom)?
164            .serialize(serializer)
165    }
166}
167
168// Each bool is an independent flag in the JS wire format; grouping them would change the JSON shape.
169#[allow(clippy::struct_excessive_bools)]
170#[derive(Debug, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct NotificationData {
173    #[serde(default = "default_id")]
174    pub(crate) id: i32,
175    pub(crate) channel_id: Option<String>,
176    pub(crate) title: Option<String>,
177    pub(crate) body: Option<String>,
178    pub(crate) schedule: Option<Schedule>,
179    pub(crate) large_body: Option<String>,
180    pub(crate) summary: Option<String>,
181    pub(crate) action_type_id: Option<String>,
182    pub(crate) group: Option<String>,
183    #[serde(default)]
184    pub(crate) group_summary: bool,
185    pub(crate) sound: Option<String>,
186    #[serde(default)]
187    pub(crate) inbox_lines: Vec<String>,
188    pub(crate) icon: Option<String>,
189    pub(crate) large_icon: Option<String>,
190    pub(crate) icon_color: Option<String>,
191    #[serde(default)]
192    pub(crate) attachments: Vec<Attachment>,
193    #[serde(default)]
194    pub(crate) extra: HashMap<String, serde_json::Value>,
195    #[serde(default)]
196    pub(crate) ongoing: bool,
197    #[serde(default)]
198    pub(crate) auto_cancel: bool,
199    #[serde(default)]
200    pub(crate) silent: bool,
201}
202
203fn default_id() -> i32 {
204    rand::random()
205}
206
207impl Default for NotificationData {
208    fn default() -> Self {
209        Self {
210            id: default_id(),
211            channel_id: None,
212            title: None,
213            body: None,
214            schedule: None,
215            large_body: None,
216            summary: None,
217            action_type_id: None,
218            group: None,
219            group_summary: false,
220            sound: None,
221            inbox_lines: Vec::new(),
222            icon: None,
223            large_icon: None,
224            icon_color: None,
225            attachments: Vec::new(),
226            extra: HashMap::default(),
227            ongoing: false,
228            auto_cancel: false,
229            silent: false,
230        }
231    }
232}
233
234#[derive(Debug, Deserialize, Serialize)]
235#[serde(rename_all = "camelCase")]
236pub struct PendingNotification {
237    pub(crate) id: i32,
238    pub(crate) title: Option<String>,
239    pub(crate) body: Option<String>,
240    pub(crate) schedule: Schedule,
241}
242
243impl PendingNotification {
244    #[must_use]
245    pub const fn id(&self) -> i32 {
246        self.id
247    }
248
249    #[must_use]
250    pub fn title(&self) -> Option<&str> {
251        self.title.as_deref()
252    }
253
254    #[must_use]
255    pub fn body(&self) -> Option<&str> {
256        self.body.as_deref()
257    }
258
259    #[must_use]
260    pub const fn schedule(&self) -> &Schedule {
261        &self.schedule
262    }
263}
264
265#[derive(Debug, Clone, Deserialize, Serialize)]
266#[serde(rename_all = "camelCase")]
267pub struct ActiveNotification {
268    pub(crate) id: i32,
269    pub(crate) tag: Option<String>,
270    pub(crate) title: Option<String>,
271    pub(crate) body: Option<String>,
272    pub(crate) group: Option<String>,
273    #[serde(default)]
274    pub(crate) group_summary: bool,
275    #[serde(default)]
276    pub(crate) data: HashMap<String, String>,
277    #[serde(default)]
278    pub(crate) extra: HashMap<String, serde_json::Value>,
279    #[serde(default)]
280    pub(crate) attachments: Vec<Attachment>,
281    pub(crate) action_type_id: Option<String>,
282    pub(crate) schedule: Option<Schedule>,
283    pub(crate) sound: Option<String>,
284}
285
286impl ActiveNotification {
287    /// Constructs an `ActiveNotification` from the minimal set of fields the
288    /// desktop tracking layer can populate. All other fields fall back to
289    /// their defaults (the mobile/native paths fill them out fully).
290    #[must_use]
291    pub fn new(id: i32, title: Option<String>, body: Option<String>) -> Self {
292        Self {
293            id,
294            tag: None,
295            title,
296            body,
297            group: None,
298            group_summary: false,
299            data: HashMap::new(),
300            extra: HashMap::new(),
301            attachments: Vec::new(),
302            action_type_id: None,
303            schedule: None,
304            sound: None,
305        }
306    }
307
308    #[must_use]
309    pub const fn id(&self) -> i32 {
310        self.id
311    }
312
313    #[must_use]
314    pub fn tag(&self) -> Option<&str> {
315        self.tag.as_deref()
316    }
317
318    #[must_use]
319    pub fn title(&self) -> Option<&str> {
320        self.title.as_deref()
321    }
322
323    #[must_use]
324    pub fn body(&self) -> Option<&str> {
325        self.body.as_deref()
326    }
327
328    #[must_use]
329    pub fn group(&self) -> Option<&str> {
330        self.group.as_deref()
331    }
332
333    #[must_use]
334    pub const fn group_summary(&self) -> bool {
335        self.group_summary
336    }
337
338    #[must_use]
339    pub const fn data(&self) -> &HashMap<String, String> {
340        &self.data
341    }
342
343    #[must_use]
344    pub const fn extra(&self) -> &HashMap<String, serde_json::Value> {
345        &self.extra
346    }
347
348    #[must_use]
349    pub fn attachments(&self) -> &[Attachment] {
350        &self.attachments
351    }
352
353    #[must_use]
354    pub fn action_type_id(&self) -> Option<&str> {
355        self.action_type_id.as_deref()
356    }
357
358    #[must_use]
359    pub const fn schedule(&self) -> Option<&Schedule> {
360        self.schedule.as_ref()
361    }
362
363    #[must_use]
364    pub fn sound(&self) -> Option<&str> {
365        self.sound.as_deref()
366    }
367}
368
369// Each bool is an independent UNNotificationCategory option; grouping would change the JSON shape.
370#[allow(clippy::struct_excessive_bools)]
371#[derive(Debug, Clone, Serialize, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct ActionType {
374    id: String,
375    actions: Vec<Action>,
376    hidden_previews_body_placeholder: Option<String>,
377    #[serde(default)]
378    custom_dismiss_action: bool,
379    #[serde(default)]
380    allow_in_car_play: bool,
381    #[serde(default)]
382    hidden_previews_show_title: bool,
383    #[serde(default)]
384    hidden_previews_show_subtitle: bool,
385}
386
387impl ActionType {
388    pub fn new(id: impl Into<String>, actions: Vec<Action>) -> Self {
389        Self {
390            id: id.into(),
391            actions,
392            hidden_previews_body_placeholder: None,
393            custom_dismiss_action: false,
394            allow_in_car_play: false,
395            hidden_previews_show_title: false,
396            hidden_previews_show_subtitle: false,
397        }
398    }
399
400    #[must_use]
401    pub fn id(&self) -> &str {
402        &self.id
403    }
404
405    #[must_use]
406    pub fn actions(&self) -> &[Action] {
407        &self.actions
408    }
409}
410
411// Each bool is an independent UNNotificationAction option; grouping would change the JSON shape.
412#[allow(clippy::struct_excessive_bools)]
413#[derive(Debug, Clone, Serialize, Deserialize)]
414#[serde(rename_all = "camelCase")]
415pub struct Action {
416    id: String,
417    title: String,
418    #[serde(default)]
419    requires_authentication: bool,
420    #[serde(default)]
421    foreground: bool,
422    #[serde(default)]
423    destructive: bool,
424    #[serde(default)]
425    input: bool,
426    input_button_title: Option<String>,
427    input_placeholder: Option<String>,
428}
429
430impl Action {
431    pub fn new(id: impl Into<String>, title: impl Into<String>, foreground: bool) -> Self {
432        Self {
433            id: id.into(),
434            title: title.into(),
435            requires_authentication: false,
436            foreground,
437            destructive: false,
438            input: false,
439            input_button_title: None,
440            input_placeholder: None,
441        }
442    }
443
444    #[must_use]
445    pub fn id(&self) -> &str {
446        &self.id
447    }
448
449    #[must_use]
450    pub fn title(&self) -> &str {
451        &self.title
452    }
453
454    #[must_use]
455    pub const fn foreground(&self) -> bool {
456        self.foreground
457    }
458}
459
460pub use android::*;
461
462mod android {
463    use serde::{Deserialize, Serialize};
464    use serde_repr::{Deserialize_repr, Serialize_repr};
465
466    #[derive(Debug, Default, Clone, Copy, Serialize_repr, Deserialize_repr)]
467    #[repr(u8)]
468    pub enum Importance {
469        None = 0,
470        Min = 1,
471        Low = 2,
472        #[default]
473        Default = 3,
474        High = 4,
475    }
476
477    #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
478    #[repr(i8)]
479    pub enum Visibility {
480        Secret = -1,
481        Private = 0,
482        Public = 1,
483    }
484
485    #[derive(Debug, Serialize, Deserialize)]
486    #[serde(rename_all = "camelCase")]
487    pub struct Channel {
488        id: String,
489        name: String,
490        description: Option<String>,
491        sound: Option<String>,
492        lights: Option<bool>,
493        light_color: Option<String>,
494        vibration: Option<bool>,
495        importance: Option<Importance>,
496        visibility: Option<Visibility>,
497    }
498
499    #[derive(Debug)]
500    pub struct ChannelBuilder(Channel);
501
502    impl Channel {
503        pub fn builder(id: impl Into<String>, name: impl Into<String>) -> ChannelBuilder {
504            ChannelBuilder(Self {
505                id: id.into(),
506                name: name.into(),
507                description: None,
508                sound: None,
509                lights: Some(false),
510                light_color: None,
511                vibration: Some(false),
512                importance: None,
513                visibility: None,
514            })
515        }
516
517        #[must_use]
518        pub fn id(&self) -> &str {
519            &self.id
520        }
521
522        #[must_use]
523        pub fn name(&self) -> &str {
524            &self.name
525        }
526
527        #[must_use]
528        pub fn description(&self) -> Option<&str> {
529            self.description.as_deref()
530        }
531
532        #[must_use]
533        pub fn sound(&self) -> Option<&str> {
534            self.sound.as_deref()
535        }
536
537        #[must_use]
538        pub fn lights(&self) -> bool {
539            self.lights.unwrap_or(false)
540        }
541
542        #[must_use]
543        pub fn light_color(&self) -> Option<&str> {
544            self.light_color.as_deref()
545        }
546
547        #[must_use]
548        pub fn vibration(&self) -> bool {
549            self.vibration.unwrap_or(false)
550        }
551
552        #[must_use]
553        pub fn importance(&self) -> Importance {
554            self.importance.unwrap_or_default()
555        }
556
557        #[must_use]
558        pub const fn visibility(&self) -> Option<Visibility> {
559            self.visibility
560        }
561    }
562
563    impl ChannelBuilder {
564        #[must_use]
565        pub fn description(mut self, description: impl Into<String>) -> Self {
566            self.0.description.replace(description.into());
567            self
568        }
569
570        #[must_use]
571        pub fn sound(mut self, sound: impl Into<String>) -> Self {
572            self.0.sound.replace(sound.into());
573            self
574        }
575
576        #[must_use]
577        pub const fn lights(mut self, lights: bool) -> Self {
578            self.0.lights = Some(lights);
579            self
580        }
581
582        #[must_use]
583        pub fn light_color(mut self, color: impl Into<String>) -> Self {
584            self.0.light_color.replace(color.into());
585            self
586        }
587
588        #[must_use]
589        pub const fn vibration(mut self, vibration: bool) -> Self {
590            self.0.vibration = Some(vibration);
591            self
592        }
593
594        #[must_use]
595        pub const fn importance(mut self, importance: Importance) -> Self {
596            self.0.importance = Some(importance);
597            self
598        }
599
600        #[must_use]
601        pub fn visibility(mut self, visibility: Visibility) -> Self {
602            self.0.visibility.replace(visibility);
603            self
604        }
605
606        #[must_use]
607        pub fn build(self) -> Channel {
608            self.0
609        }
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn test_attachment_creation() {
619        let url = Url::parse("https://example.com/image.png").expect("Failed to parse URL");
620        let attachment = Attachment::new("test_id", url.clone());
621        assert_eq!(attachment.id, "test_id");
622        assert_eq!(attachment.url, url);
623    }
624
625    #[test]
626    fn test_attachment_serialization() {
627        let url = Url::parse("https://example.com/image.png").expect("Failed to parse URL");
628        let attachment = Attachment::new("test_id", url);
629        let json = serde_json::to_string(&attachment).expect("Failed to serialize attachment");
630        assert!(json.contains("test_id"));
631        assert!(json.contains("https://example.com/image.png"));
632    }
633
634    #[test]
635    fn test_attachment_deserialization() {
636        let json = r#"{"id":"test_id","url":"https://example.com/image.png"}"#;
637        let attachment: Attachment =
638            serde_json::from_str(json).expect("Failed to deserialize attachment");
639        assert_eq!(attachment.id, "test_id");
640        assert_eq!(attachment.url.as_str(), "https://example.com/image.png");
641    }
642
643    #[test]
644    fn test_schedule_every_display() {
645        assert_eq!(ScheduleEvery::Year.to_string(), "year");
646        assert_eq!(ScheduleEvery::Month.to_string(), "month");
647        assert_eq!(ScheduleEvery::TwoWeeks.to_string(), "twoWeeks");
648        assert_eq!(ScheduleEvery::Week.to_string(), "week");
649        assert_eq!(ScheduleEvery::Day.to_string(), "day");
650        assert_eq!(ScheduleEvery::Hour.to_string(), "hour");
651        assert_eq!(ScheduleEvery::Minute.to_string(), "minute");
652        assert_eq!(ScheduleEvery::Second.to_string(), "second");
653    }
654
655    #[test]
656    fn test_schedule_every_serialization() {
657        let json = serde_json::to_string(&ScheduleEvery::Day).expect("Failed to serialize Day");
658        assert_eq!(json, "\"day\"");
659
660        let json =
661            serde_json::to_string(&ScheduleEvery::TwoWeeks).expect("Failed to serialize TwoWeeks");
662        assert_eq!(json, "\"twoWeeks\"");
663    }
664
665    #[test]
666    fn test_schedule_every_deserialization() {
667        let every: ScheduleEvery =
668            serde_json::from_str("\"year\"").expect("Failed to deserialize year");
669        assert!(matches!(every, ScheduleEvery::Year));
670
671        let every: ScheduleEvery =
672            serde_json::from_str("\"month\"").expect("Failed to deserialize month");
673        assert!(matches!(every, ScheduleEvery::Month));
674
675        let every: ScheduleEvery =
676            serde_json::from_str("\"twoweeks\"").expect("Failed to deserialize twoweeks");
677        assert!(matches!(every, ScheduleEvery::TwoWeeks));
678
679        let every: ScheduleEvery =
680            serde_json::from_str("\"week\"").expect("Failed to deserialize week");
681        assert!(matches!(every, ScheduleEvery::Week));
682
683        let every: ScheduleEvery =
684            serde_json::from_str("\"day\"").expect("Failed to deserialize day");
685        assert!(matches!(every, ScheduleEvery::Day));
686
687        let every: ScheduleEvery =
688            serde_json::from_str("\"hour\"").expect("Failed to deserialize hour");
689        assert!(matches!(every, ScheduleEvery::Hour));
690
691        let every: ScheduleEvery =
692            serde_json::from_str("\"minute\"").expect("Failed to deserialize minute");
693        assert!(matches!(every, ScheduleEvery::Minute));
694
695        let every: ScheduleEvery =
696            serde_json::from_str("\"second\"").expect("Failed to deserialize second");
697        assert!(matches!(every, ScheduleEvery::Second));
698    }
699
700    #[test]
701    fn test_schedule_every_deserialization_invalid() {
702        let result: Result<ScheduleEvery, _> = serde_json::from_str("\"invalid\"");
703        assert!(result.is_err());
704    }
705
706    #[test]
707    fn test_schedule_interval_default() {
708        let interval = ScheduleInterval::default();
709        assert!(interval.year.is_none());
710        assert!(interval.month.is_none());
711        assert!(interval.day.is_none());
712        assert!(interval.weekday.is_none());
713        assert!(interval.hour.is_none());
714        assert!(interval.minute.is_none());
715        assert!(interval.second.is_none());
716    }
717
718    #[test]
719    fn test_schedule_interval_serialization() {
720        let interval = ScheduleInterval {
721            year: Some(24),
722            month: Some(12),
723            day: Some(25),
724            weekday: Some(1),
725            hour: Some(10),
726            minute: Some(30),
727            second: Some(0),
728        };
729        let json = serde_json::to_string(&interval).expect("Failed to serialize interval");
730        assert!(json.contains("\"year\":24"));
731        assert!(json.contains("\"month\":12"));
732        assert!(json.contains("\"day\":25"));
733    }
734
735    #[test]
736    fn test_notification_data_default() {
737        let data = NotificationData::default();
738        assert!(data.id != 0); // Should be a random ID
739        assert!(data.channel_id.is_none());
740        assert!(data.title.is_none());
741        assert!(data.body.is_none());
742        assert!(data.schedule.is_none());
743        assert!(!data.group_summary);
744        assert!(!data.ongoing);
745        assert!(!data.auto_cancel);
746        assert!(!data.silent);
747        assert!(data.inbox_lines.is_empty());
748        assert!(data.attachments.is_empty());
749        assert!(data.extra.is_empty());
750    }
751
752    #[test]
753    fn test_notification_data_serialization() {
754        let data = NotificationData {
755            id: 123,
756            title: Some("Test Title".to_string()),
757            body: Some("Test Body".to_string()),
758            ongoing: true,
759            ..Default::default()
760        };
761
762        let json = serde_json::to_string(&data).expect("Failed to serialize notification data");
763        assert!(json.contains("\"id\":123"));
764        assert!(json.contains("\"title\":\"Test Title\""));
765        assert!(json.contains("\"body\":\"Test Body\""));
766        assert!(json.contains("\"ongoing\":true"));
767    }
768
769    #[test]
770    fn test_pending_notification_getters() {
771        let json = r#"{
772            "id": 456,
773            "title": "Pending Title",
774            "body": "Pending Body",
775            "schedule": {"every": {"interval": "day", "count": 1}}
776        }"#;
777        let pending: PendingNotification =
778            serde_json::from_str(json).expect("Failed to deserialize pending notification");
779
780        assert_eq!(pending.id(), 456);
781        assert_eq!(pending.title(), Some("Pending Title"));
782        assert_eq!(pending.body(), Some("Pending Body"));
783        assert!(matches!(pending.schedule(), Schedule::Every { .. }));
784    }
785
786    #[test]
787    fn test_active_notification_getters() {
788        let json = r#"{
789            "id": 789,
790            "title": "Active Title",
791            "body": "Active Body",
792            "group": "test_group",
793            "groupSummary": true
794        }"#;
795        let active: ActiveNotification =
796            serde_json::from_str(json).expect("Failed to deserialize active notification");
797
798        assert_eq!(active.id(), 789);
799        assert_eq!(active.title(), Some("Active Title"));
800        assert_eq!(active.body(), Some("Active Body"));
801        assert_eq!(active.group(), Some("test_group"));
802        assert!(active.group_summary());
803        assert!(active.data().is_empty());
804        assert!(active.extra().is_empty());
805        assert!(active.attachments().is_empty());
806        assert!(active.action_type_id().is_none());
807        assert!(active.schedule().is_none());
808        assert!(active.sound().is_none());
809    }
810
811    #[cfg(target_os = "android")]
812    #[test]
813    fn test_importance_default() {
814        let importance = Importance::default();
815        assert!(matches!(importance, Importance::Default));
816    }
817
818    #[cfg(target_os = "android")]
819    #[test]
820    fn test_importance_serialization() {
821        assert_eq!(
822            serde_json::to_string(&Importance::None).expect("Failed to serialize Importance::None"),
823            "0"
824        );
825        assert_eq!(
826            serde_json::to_string(&Importance::Min).expect("Failed to serialize Importance::Min"),
827            "1"
828        );
829        assert_eq!(
830            serde_json::to_string(&Importance::Low).expect("Failed to serialize Importance::Low"),
831            "2"
832        );
833        assert_eq!(
834            serde_json::to_string(&Importance::Default)
835                .expect("Failed to serialize Importance::Default"),
836            "3"
837        );
838        assert_eq!(
839            serde_json::to_string(&Importance::High).expect("Failed to serialize Importance::High"),
840            "4"
841        );
842    }
843
844    #[cfg(target_os = "android")]
845    #[test]
846    fn test_visibility_serialization() {
847        assert_eq!(
848            serde_json::to_string(&Visibility::Secret)
849                .expect("Failed to serialize Visibility::Secret"),
850            "-1"
851        );
852        assert_eq!(
853            serde_json::to_string(&Visibility::Private)
854                .expect("Failed to serialize Visibility::Private"),
855            "0"
856        );
857        assert_eq!(
858            serde_json::to_string(&Visibility::Public)
859                .expect("Failed to serialize Visibility::Public"),
860            "1"
861        );
862    }
863
864    #[cfg(target_os = "android")]
865    #[test]
866    fn test_channel_builder() {
867        let channel = Channel::builder("test_id", "Test Channel")
868            .description("Test Description")
869            .sound("test_sound")
870            .lights(true)
871            .light_color("#FF0000")
872            .vibration(true)
873            .importance(Importance::High)
874            .visibility(Visibility::Public)
875            .build();
876
877        assert_eq!(channel.id(), "test_id");
878        assert_eq!(channel.name(), "Test Channel");
879        assert_eq!(channel.description(), Some("Test Description"));
880        assert_eq!(channel.sound(), Some("test_sound"));
881        assert!(channel.lights());
882        assert_eq!(channel.light_color(), Some("#FF0000"));
883        assert!(channel.vibration());
884        assert!(matches!(channel.importance(), Importance::High));
885        assert_eq!(channel.visibility(), Some(Visibility::Public));
886    }
887
888    #[cfg(target_os = "android")]
889    #[test]
890    fn test_channel_builder_minimal() {
891        let channel = Channel::builder("minimal_id", "Minimal Channel").build();
892
893        assert_eq!(channel.id(), "minimal_id");
894        assert_eq!(channel.name(), "Minimal Channel");
895        assert_eq!(channel.description(), None);
896        assert_eq!(channel.sound(), None);
897        assert!(!channel.lights());
898        assert_eq!(channel.light_color(), None);
899        assert!(!channel.vibration());
900        assert!(matches!(channel.importance(), Importance::Default));
901        assert_eq!(channel.visibility(), None);
902    }
903
904    #[test]
905    fn test_schedule_at_serialization() {
906        use time::OffsetDateTime;
907
908        let date = OffsetDateTime::now_utc();
909        let schedule = Schedule::At {
910            date,
911            repeating: true,
912            allow_while_idle: false,
913        };
914
915        let json = serde_json::to_string(&schedule).expect("Failed to serialize Schedule::At");
916        assert!(json.contains("\"at\""));
917        assert!(json.contains("\"date\""));
918        assert!(json.contains("\"repeating\":true"));
919        assert!(json.contains("\"allowWhileIdle\":false"));
920    }
921
922    #[test]
923    fn test_schedule_interval_variant() {
924        let schedule = Schedule::Interval {
925            interval: ScheduleInterval {
926                hour: Some(10),
927                minute: Some(30),
928                ..Default::default()
929            },
930            allow_while_idle: true,
931        };
932
933        let json =
934            serde_json::to_string(&schedule).expect("Failed to serialize Schedule::Interval");
935        assert!(json.contains("\"interval\""));
936        assert!(json.contains("\"hour\":10"));
937        assert!(json.contains("\"minute\":30"));
938        assert!(json.contains("\"allowWhileIdle\":true"));
939    }
940
941    #[test]
942    fn test_schedule_every_variant() {
943        let schedule = Schedule::Every {
944            interval: ScheduleEvery::Day,
945            count: 5,
946            allow_while_idle: false,
947        };
948
949        let json = serde_json::to_string(&schedule).expect("Failed to serialize Schedule::Every");
950        assert!(json.contains("\"every\""));
951        assert!(json.contains("\"interval\":\"day\""));
952        assert!(json.contains("\"count\":5"));
953    }
954}