1
mod calendar_properties;
2
mod component_properties;
3
mod error;
4
mod params;
5
mod recur;
6
mod value;
7

            
8
use crate::common::{PropertyKind, Value};
9
use crate::model::component::CalendarComponent;
10
use crate::model::object::ICalObject;
11
use crate::model::param::{Param, ValueTypeParam};
12
use crate::model::property::{CalendarProperty, ComponentProperty};
13
use crate::model::ComponentAccess;
14
use crate::validate::calendar_properties::validate_calendar_properties;
15
use crate::validate::component_properties::validate_component_properties;
16
use crate::validate::params::validate_params;
17
use std::collections::{HashMap, HashSet};
18

            
19
pub use error::*;
20

            
21
188
pub fn validate_model(ical_object: &ICalObject) -> anyhow::Result<Vec<ICalendarError>> {
22
188
    let mut errors = Vec::new();
23
188

            
24
188
    let time_zone_ids = ical_object
25
188
        .components
26
188
        .iter()
27
338
        .filter_map(|component| {
28
248
            if let CalendarComponent::TimeZone(time_zone) = component {
29
30
                for property in &time_zone.properties {
30
28
                    if let ComponentProperty::TimeZoneId(tz_id) = property {
31
26
                        return Some(tz_id.value.id.clone());
32
2
                    }
33
                }
34
220
            }
35

            
36
222
            None
37
338
        })
38
188
        .collect::<HashSet<_>>();
39
188

            
40
188
    let mut calendar_info = CalendarInfo::new(time_zone_ids);
41
188

            
42
188
    errors.extend_from_slice(
43
188
        ICalendarError::many_from_calendar_property_errors(validate_calendar_properties(
44
188
            ical_object,
45
188
            &mut calendar_info,
46
188
        ))
47
188
        .as_slice(),
48
188
    );
49
188

            
50
188
    if ical_object.components.is_empty() {
51
2
        errors.push(ICalendarError {
52
2
            message: "No components found in calendar object, required at least one".to_string(),
53
2
            severity: ICalendarErrorSeverity::Error,
54
2
            location: None,
55
2
        });
56
186
    }
57

            
58
188
    let validate_alarms = |errors: &mut Vec<ICalendarError>,
59
                           alarms: &[CalendarComponent],
60
                           index: usize,
61
                           name: &str|
62
144
     -> anyhow::Result<()> {
63
150
        for (alarm_index, alarm) in alarms.iter().enumerate() {
64
56
            errors.extend_from_slice(
65
56
                ICalendarError::many_from_nested_component_property_errors(
66
56
                    validate_component_properties(
67
56
                        &calendar_info,
68
56
                        PropertyLocation::Alarm,
69
56
                        alarm.properties(),
70
56
                    )?,
71
56
                    index,
72
56
                    name.to_string(),
73
56
                    alarm_index,
74
56
                    component_name(alarm).to_string(),
75
56
                )
76
56
                .as_slice(),
77
            );
78
        }
79

            
80
144
        Ok(())
81
144
    };
82

            
83
248
    for (index, component) in ical_object.components.iter().enumerate() {
84
248
        match component {
85
124
            CalendarComponent::Event(event) => {
86
124
                errors.extend_from_slice(
87
124
                    ICalendarError::many_from_component_property_errors(
88
124
                        validate_component_properties(
89
124
                            &calendar_info,
90
124
                            PropertyLocation::Event,
91
124
                            &event.properties,
92
124
                        )?,
93
124
                        index,
94
124
                        component_name(component).to_string(),
95
124
                    )
96
124
                    .as_slice(),
97
124
                );
98
124

            
99
124
                validate_alarms(&mut errors, &event.alarms, index, component_name(component))?;
100
            }
101
20
            CalendarComponent::ToDo(to_do) => {
102
20
                errors.extend_from_slice(
103
20
                    ICalendarError::many_from_component_property_errors(
104
20
                        validate_component_properties(
105
20
                            &calendar_info,
106
20
                            PropertyLocation::ToDo,
107
20
                            &to_do.properties,
108
20
                        )?,
109
20
                        index,
110
20
                        component_name(component).to_string(),
111
20
                    )
112
20
                    .as_slice(),
113
20
                );
114
20

            
115
20
                validate_alarms(&mut errors, &to_do.alarms, index, component_name(component))?;
116
            }
117
14
            CalendarComponent::Journal(journal) => {
118
14
                errors.extend_from_slice(
119
14
                    ICalendarError::many_from_component_property_errors(
120
14
                        validate_component_properties(
121
14
                            &calendar_info,
122
14
                            PropertyLocation::Journal,
123
14
                            &journal.properties,
124
14
                        )?,
125
14
                        index,
126
14
                        component_name(component).to_string(),
127
14
                    )
128
14
                    .as_slice(),
129
                );
130
            }
131
10
            CalendarComponent::FreeBusy(free_busy) => {
132
10
                errors.extend_from_slice(
133
10
                    ICalendarError::many_from_component_property_errors(
134
10
                        validate_component_properties(
135
10
                            &calendar_info,
136
10
                            PropertyLocation::FreeBusy,
137
10
                            &free_busy.properties,
138
10
                        )?,
139
10
                        index,
140
10
                        component_name(component).to_string(),
141
10
                    )
142
10
                    .as_slice(),
143
                );
144
            }
145
28
            CalendarComponent::TimeZone(time_zone) => {
146
28
                errors.extend_from_slice(
147
28
                    ICalendarError::many_from_component_property_errors(
148
28
                        validate_component_properties(
149
28
                            &calendar_info,
150
28
                            PropertyLocation::TimeZone,
151
28
                            &time_zone.properties,
152
28
                        )?,
153
28
                        index,
154
28
                        component_name(component).to_string(),
155
28
                    )
156
28
                    .as_slice(),
157
28
                );
158
28

            
159
28
                if time_zone.components.is_empty() {
160
2
                    errors.push(ICalendarError {
161
2
                        message: "No standard or daylight components found in time zone, required at least one"
162
2
                            .to_string(),
163
2
                        severity: ICalendarErrorSeverity::Error,
164
2
                        location: Some(ICalendarLocation::Component(ComponentLocation {
165
2
                            index,
166
2
                            name: component_name(component).to_string(),
167
2
                            location: None,
168
2
                        })),
169
2
                    });
170
26
                }
171

            
172
44
                for (tz_component_index, tz_component) in time_zone.components.iter().enumerate() {
173
44
                    match tz_component {
174
26
                        CalendarComponent::Standard(standard) => {
175
26
                            errors.extend_from_slice(
176
26
                                ICalendarError::many_from_nested_component_property_errors(
177
26
                                    validate_component_properties(
178
26
                                        &calendar_info,
179
26
                                        PropertyLocation::TimeZoneComponent,
180
26
                                        &standard.properties,
181
26
                                    )?,
182
26
                                    index,
183
26
                                    component_name(component).to_string(),
184
26
                                    tz_component_index,
185
26
                                    component_name(tz_component).to_string(),
186
26
                                )
187
26
                                .as_slice(),
188
                            );
189
                        }
190
18
                        CalendarComponent::Daylight(daylight) => {
191
18
                            errors.extend_from_slice(
192
18
                                ICalendarError::many_from_nested_component_property_errors(
193
18
                                    validate_component_properties(
194
18
                                        &calendar_info,
195
18
                                        PropertyLocation::TimeZoneComponent,
196
18
                                        &daylight.properties,
197
18
                                    )?,
198
18
                                    index,
199
18
                                    component_name(component).to_string(),
200
18
                                    tz_component_index,
201
18
                                    component_name(tz_component).to_string(),
202
18
                                )
203
18
                                .as_slice(),
204
                            );
205
                        }
206
                        _ => {
207
                            // Neither the parser nor the builder will let other subcomponents to
208
                            // be added here.
209
                            unreachable!()
210
                        }
211
                    }
212
                }
213
            }
214
10
            CalendarComponent::IanaComponent(iana_component) => {
215
10
                errors.extend_from_slice(
216
10
                    ICalendarError::many_from_component_property_errors(
217
10
                        validate_component_properties(
218
10
                            &calendar_info,
219
10
                            PropertyLocation::Other,
220
10
                            &iana_component.properties,
221
10
                        )?,
222
10
                        index,
223
10
                        component_name(component).to_string(),
224
10
                    )
225
10
                    .as_slice(),
226
                );
227
            }
228
42
            CalendarComponent::XComponent(x_component) => {
229
42
                errors.extend_from_slice(
230
42
                    ICalendarError::many_from_component_property_errors(
231
42
                        validate_component_properties(
232
42
                            &calendar_info,
233
42
                            PropertyLocation::Other,
234
42
                            &x_component.properties,
235
42
                        )?,
236
42
                        index,
237
42
                        component_name(component).to_string(),
238
42
                    )
239
42
                    .as_slice(),
240
                );
241
            }
242
            _ => {
243
                // Component at the wrong level will get picked up as IANA components
244
                unreachable!()
245
            }
246
        }
247
    }
248

            
249
188
    Ok(errors)
250
188
}
251

            
252
16
fn validate_time(time: &crate::parser::types::Time) -> anyhow::Result<()> {
253
16
    if time.hour > 23 {
254
4
        anyhow::bail!("Hour must be between 0 and 23");
255
12
    }
256
12

            
257
12
    if time.minute > 59 {
258
4
        anyhow::bail!("Minute must be between 0 and 59");
259
8
    }
260
8

            
261
8
    if time.second > 60 {
262
4
        anyhow::bail!("Second must be between 0 and 60");
263
4
    }
264
4

            
265
4
    Ok(())
266
16
}
267

            
268
12
fn validate_utc_offset(offset: &crate::parser::types::UtcOffset) -> anyhow::Result<()> {
269
12
    if offset.sign < 0
270
8
        && (offset.hours == 0
271
8
            && offset.minutes == 0
272
8
            && (offset.seconds.is_none() || offset.seconds == Some(0)))
273
    {
274
4
        anyhow::bail!("UTC offset must have a non-zero value if it is negative");
275
8
    }
276
8

            
277
8
    if offset.minutes > 59 {
278
4
        anyhow::bail!("Minutes must be between 0 and 59");
279
4
    }
280
4

            
281
4
    Ok(())
282
12
}
283

            
284
#[derive(Debug)]
285
struct CalendarInfo {
286
    /// The ids of the time zones that this calendar defines.
287
    time_zone_ids: HashSet<String>,
288
    /// The method for this calendar object, if specified.
289
    method: Option<String>,
290
}
291

            
292
impl CalendarInfo {
293
188
    fn new(time_zone_ids: HashSet<String>) -> Self {
294
188
        CalendarInfo {
295
188
            time_zone_ids,
296
188
            method: None,
297
188
        }
298
188
    }
299
}
300

            
301
#[derive(Debug)]
302
struct PropertyInfo<'a> {
303
    /// The location that this property has been used in
304
    property_location: PropertyLocation,
305
    /// The property kind that is the context for validating a property value or param
306
    property_kind: PropertyKind,
307
    /// The required value type for this property
308
    value_type: ValueType,
309
    /// If the property value contains a time, then this field will be set. If that time is UTC,
310
    /// then this field will be set to true.
311
    value_is_utc: Option<bool>,
312
    /// This is an xProperty or ianaProperty
313
    is_other: bool,
314
    /// Information about the calendar that contains this property
315
    calendar_info: &'a CalendarInfo,
316
}
317

            
318
#[derive(Debug, Clone, PartialEq)]
319
enum PropertyLocation {
320
    Calendar,
321
    Event,
322
    ToDo,
323
    Journal,
324
    FreeBusy,
325
    TimeZone,
326
    TimeZoneComponent,
327
    Other,
328
    Alarm,
329
}
330

            
331
impl<'a> PropertyInfo<'a> {
332
1768
    fn new(
333
1768
        calendar_info: &'a CalendarInfo,
334
1768
        property_location: PropertyLocation,
335
1768
        property_kind: PropertyKind,
336
1768
        value_type: ValueType,
337
1768
    ) -> Self {
338
1768
        PropertyInfo {
339
1768
            property_location,
340
1768
            property_kind,
341
1768
            value_type,
342
1768
            value_is_utc: None,
343
1768
            is_other: false,
344
1768
            calendar_info,
345
1768
        }
346
1768
    }
347

            
348
124
    fn utc(mut self, is_utc: bool) -> Self {
349
124
        self.value_is_utc = Some(is_utc);
350
124
        self
351
124
    }
352
}
353

            
354
#[derive(Eq, PartialEq, Debug)]
355
enum ValueType {
356
    VersionValue,
357
    CalendarAddress,
358
    Text,
359
    Duration,
360
    Date,
361
    DateTime,
362
    Binary,
363
    Float,
364
    Integer,
365
    Period,
366
    UtcOffset,
367
    Uri,
368
    Recurrence,
369
}
370

            
371
2662
fn add_to_seen(seen: &mut HashMap<String, u32>, key: &str) -> u32 {
372
2662
    *seen
373
2662
        .entry(key.to_string())
374
2778
        .and_modify(|count| *count += 1)
375
2662
        .or_insert(1)
376
2662
}
377

            
378
#[derive(Debug, Clone, PartialEq)]
379
enum OccurrenceExpectation {
380
    Once,
381
    OnceOrMany,
382
    OptionalOnce,
383
    OptionalMany,
384
    Never,
385
}
386

            
387
4212
fn check_occurrence(
388
4212
    seen: &HashMap<String, u32>,
389
4212
    key: &str,
390
4212
    expectation: OccurrenceExpectation,
391
4212
) -> Option<String> {
392
4212
    match (seen.get(key), expectation) {
393
46
        (None | Some(0), OccurrenceExpectation::Once) => Some(format!("{} is required", key)),
394
2060
        (Some(1), OccurrenceExpectation::Once) => None,
395
        (_, OccurrenceExpectation::Once) => Some(format!("{} must only appear once", key)),
396
        (None | Some(0), OccurrenceExpectation::OnceOrMany) => Some(format!("{} is required", key)),
397
6
        (_, OccurrenceExpectation::OnceOrMany) => None,
398
        (None | Some(0) | Some(1), OccurrenceExpectation::OptionalOnce) => None,
399
118
        (_, OccurrenceExpectation::OptionalOnce) => Some(format!("{} must only appear once", key)),
400
466
        (_, OccurrenceExpectation::OptionalMany) => None,
401
        (None | Some(0), OccurrenceExpectation::Never) => None,
402
24
        (_, OccurrenceExpectation::Never) => Some(format!("{} is not allowed", key)),
403
    }
404
2720
}
405

            
406
2262
fn get_declared_value_type(property: &ComponentProperty) -> Option<(Value, usize)> {
407
2262
    property
408
2262
        .params()
409
2262
        .iter()
410
2262
        .enumerate()
411
3992
        .find_map(|(index, param)| {
412
2798
            if let Param::ValueType(ValueTypeParam { value }) = param {
413
160
                return Some((value.clone(), index));
414
2638
            }
415
2638

            
416
2638
            None
417
3992
        })
418
2262
}
419

            
420
662
fn calendar_property_name(property: &CalendarProperty) -> &str {
421
662
    match property {
422
376
        CalendarProperty::Version { .. } => "VERSION",
423
188
        CalendarProperty::ProductId(_) => "PRODID",
424
6
        CalendarProperty::CalendarScale(_) => "CALSCALE",
425
92
        CalendarProperty::Method(_) => "METHOD",
426
        CalendarProperty::XProperty(x_prop) => &x_prop.name,
427
        CalendarProperty::IanaProperty(iana_prop) => &iana_prop.name,
428
    }
429
662
}
430

            
431
3606
fn component_property_name(property: &ComponentProperty) -> &str {
432
3606
    match property {
433
94
        ComponentProperty::Attach(_) => "ATTACH",
434
48
        ComponentProperty::Categories(_) => "CATEGORIES",
435
36
        ComponentProperty::Classification(_) => "CLASS",
436
72
        ComponentProperty::Comment(_) => "COMMENT",
437
194
        ComponentProperty::Description(_) => "DESCRIPTION",
438
40
        ComponentProperty::GeographicPosition(_) => "GEO",
439
52
        ComponentProperty::Location(_) => "LOCATION",
440
20
        ComponentProperty::PercentComplete(_) => "PERCENT-COMPLETE",
441
40
        ComponentProperty::Priority(_) => "PRIORITY",
442
24
        ComponentProperty::Resources(_) => "RESOURCES",
443
60
        ComponentProperty::Status(_) => "STATUS",
444
96
        ComponentProperty::Summary(_) => "SUMMARY",
445
12
        ComponentProperty::DateTimeCompleted(_) => "COMPLETED",
446
62
        ComponentProperty::DateTimeEnd(_) => "DTEND",
447
18
        ComponentProperty::DateTimeDue(_) => "DUE",
448
250
        ComponentProperty::DateTimeStart(_) => "DTSTART",
449
124
        ComponentProperty::Duration(_) => "DURATION",
450
12
        ComponentProperty::FreeBusyTime(_) => "FREEBUSY",
451
20
        ComponentProperty::TimeTransparency(_) => "TRANSP",
452
52
        ComponentProperty::TimeZoneId(_) => "TZID",
453
60
        ComponentProperty::TimeZoneName(_) => "TZNAME",
454
80
        ComponentProperty::TimeZoneOffsetFrom(_) => "TZOFFSETFROM",
455
80
        ComponentProperty::TimeZoneOffsetTo(_) => "TZOFFSETTO",
456
20
        ComponentProperty::TimeZoneUrl(_) => "TZURL",
457
80
        ComponentProperty::Attendee(_) => "ATTENDEE",
458
56
        ComponentProperty::Contact(_) => "CONTACT",
459
96
        ComponentProperty::Organizer(_) => "ORGANIZER",
460
60
        ComponentProperty::RecurrenceId(_) => "RECURRENCE-ID",
461
36
        ComponentProperty::RelatedTo(_) => "RELATED-TO",
462
40
        ComponentProperty::Url(_) => "URL",
463
158
        ComponentProperty::UniqueIdentifier(_) => "UID",
464
42
        ComponentProperty::ExceptionDateTimes(_) => "EXDATE",
465
66
        ComponentProperty::RecurrenceDateTimes(_) => "RDATE",
466
466
        ComponentProperty::RecurrenceRule(_) => "RRULE",
467
104
        ComponentProperty::Action(_) => "ACTION",
468
84
        ComponentProperty::Repeat(_) => "REPEAT",
469
94
        ComponentProperty::Trigger(_) => "TRIGGER",
470
72
        ComponentProperty::DateTimeCreated(_) => "CREATED",
471
316
        ComponentProperty::DateTimeStamp(_) => "DTSTAMP",
472
84
        ComponentProperty::LastModified(_) => "LAST-MODIFIED",
473
60
        ComponentProperty::Sequence(_) => "SEQUENCE",
474
38
        ComponentProperty::IanaProperty(iana_prop) => &iana_prop.name,
475
40
        ComponentProperty::XProperty(x_prop) => &x_prop.name,
476
48
        ComponentProperty::RequestStatus(_) => "REQUEST-STATUS",
477
    }
478
3606
}
479

            
480
538
fn component_name(component: &CalendarComponent) -> &str {
481
538
    match component {
482
248
        CalendarComponent::Event(_) => "VEVENT",
483
40
        CalendarComponent::ToDo(_) => "VTODO",
484
14
        CalendarComponent::Journal(_) => "VJOURNAL",
485
74
        CalendarComponent::TimeZone(_) => "VTIMEZONE",
486
10
        CalendarComponent::FreeBusy(_) => "VFREEBUSY",
487
56
        CalendarComponent::Alarm(_) => "VALARM",
488
26
        CalendarComponent::Standard(_) => "STANDARD",
489
18
        CalendarComponent::Daylight(_) => "DAYLIGHT",
490
10
        CalendarComponent::IanaComponent(component) => &component.name,
491
42
        CalendarComponent::XComponent(component) => &component.name,
492
    }
493
538
}
494

            
495
908
fn param_name(param: &Param) -> &str {
496
908
    match param {
497
120
        Param::AltRep { .. } => "ALTREP",
498
44
        Param::CommonName { .. } => "CN",
499
26
        Param::CalendarUserType { .. } => "CUTYPE",
500
20
        Param::DelegatedFrom { .. } => "DELEGATED-FROM",
501
20
        Param::DelegatedTo { .. } => "DELEGATED-TO",
502
44
        Param::DirectoryEntryReference { .. } => "DIR",
503
        Param::Encoding { .. } => "ENCODING",
504
20
        Param::FormatType { .. } => "FMTTYPE",
505
8
        Param::FreeBusyTimeType { .. } => "FBTYPE",
506
212
        Param::Language { .. } => "LANGUAGE",
507
20
        Param::Members { .. } => "MEMBER",
508
28
        Param::ParticipationStatus { .. } => "PARTSTAT",
509
20
        Param::Range { .. } => "RANGE",
510
8
        Param::TriggerRelationship { .. } => "RELATED",
511
20
        Param::RelationshipType { .. } => "RELTYPE",
512
26
        Param::Role { .. } => "ROLE",
513
26
        Param::Rsvp { .. } => "RSVP",
514
48
        Param::SentBy { .. } => "SENT-BY",
515
116
        Param::TimeZoneId { .. } => "TZID",
516
46
        Param::ValueType { .. } => "VALUE",
517
36
        Param::Other { name, .. } => name,
518
        Param::Others { name, .. } => name,
519
    }
520
908
}
521

            
522
#[cfg(test)]
523
mod tests {
524
    use super::*;
525
    use crate::convert::ToModel;
526

            
527
    use crate::parser::Error;
528
    use crate::test_utils::check_rem;
529

            
530
    macro_rules! assert_no_errors {
531
        ($errors:expr) => {
532
            if !$errors.is_empty() {
533
                panic!(
534
                    "Expected no errors, but got: {:?}",
535
                    $errors
536
                        .iter()
537
                        .map(|e| e.to_string())
538
                        .collect::<std::vec::Vec<_>>()
539
                );
540
            }
541
        };
542
    }
543

            
544
    macro_rules! assert_errors {
545
        ($errors:expr, $msg:literal $(,$others:literal)* $(,)?) => {
546
            assert_errors!($errors, &[$msg, $($others,)*]);
547
        };
548

            
549
        ($errors:expr, $messages:expr) => {
550
480
            similar_asserts::assert_eq!($errors.iter().map(|e| e.to_string()).collect::<Vec<_>>().as_slice(), $messages);
551
        };
552
    }
553

            
554
    #[test]
555
2
    fn sample_passes_validation() {
556
2
        let content = "BEGIN:VCALENDAR\r\n\
557
2
VERSION:2.0\r\n\
558
2
PRODID:-//hacksw/handcal//NONSGML v1.0//EN\r\n\
559
2
BEGIN:VTIMEZONE\r\n\
560
2
TZID:Fictitious\r\n\
561
2
LAST-MODIFIED:19870101T000000Z\r\n\
562
2
BEGIN:STANDARD\r\n\
563
2
DTSTART:19671029T020000\r\n\
564
2
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n\
565
2
TZOFFSETFROM:-0400\r\n\
566
2
TZOFFSETTO:-0500\r\n\
567
2
TZNAME:EST\r\n\
568
2
END:STANDARD\r\n\
569
2
BEGIN:DAYLIGHT\r\n\
570
2
DTSTART:19870405T020000\r\n\
571
2
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=19980404T070000Z\r\n\
572
2
TZOFFSETFROM:-0500\r\n\
573
2
TZOFFSETTO:-0400\r\n\
574
2
TZNAME:EDT\r\n\
575
2
END:DAYLIGHT\r\n\
576
2
BEGIN:DAYLIGHT\r\n\
577
2
DTSTART:19990424T020000\r\n\
578
2
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=4\r\n\
579
2
TZOFFSETFROM:-0500\r\n\
580
2
TZOFFSETTO:-0400\r\n\
581
2
TZNAME:EDT\r\n\
582
2
END:DAYLIGHT\r\n\
583
2
END:VTIMEZONE\r\n\
584
2
END:VCALENDAR\r\n";
585
2

            
586
2
        let errors = validate_content(content);
587

            
588
        assert_no_errors!(&errors);
589
2
    }
590

            
591
    #[test]
592
2
    fn calendar_with_no_components() {
593
2
        let object = ICalObject::builder()
594
2
            .add_product_id("-//hacksw/handcal//NONSGML v1.0//EN")
595
2
            .finish_property()
596
2
            .add_max_version("2.0")
597
2
            .finish_property()
598
2
            .build();
599
2

            
600
2
        let errors = validate_model(&object).unwrap();
601

            
602
        assert_errors!(
603
            errors,
604
            "No components found in calendar object, required at least one"
605
        );
606
2
    }
607

            
608
    #[test]
609
2
    fn component_with_no_properties() {
610
2
        let object = ICalObject::builder()
611
2
            .add_product_id("-//hacksw/handcal//NONSGML v1.0//EN")
612
2
            .finish_property()
613
2
            .add_max_version("2.0")
614
2
            .finish_property()
615
2
            .add_journal_component()
616
2
            .finish_component()
617
2
            .build();
618
2

            
619
2
        let errors = validate_model(&object).unwrap();
620

            
621
        assert_errors!(errors, "In component \"VJOURNAL\" at index 0: No properties found in component, required at least one");
622
2
    }
623

            
624
    #[test]
625
2
    fn common_name_on_version_property() {
626
2
        let content = "BEGIN:VCALENDAR\r\n\
627
2
PRODID:test\r\n\
628
2
VERSION;CN=hello:2.0\r\n\
629
2
BEGIN:X-NONE\r\n\
630
2
empty:value\r\n\
631
2
END:X-NONE\r\n\
632
2
END:VCALENDAR\r\n";
633
2

            
634
2
        let errors = validate_content(content);
635

            
636
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Common name (CN) is not allowed for this property type");
637
2
    }
638

            
639
    #[test]
640
2
    fn common_name_on_description_property() {
641
2
        let content = "BEGIN:VCALENDAR\r\n\
642
2
PRODID:test\r\n\
643
2
VERSION:2.0\r\n\
644
2
METHOD:send\r\n\
645
2
BEGIN:VEVENT\r\n\
646
2
DTSTAMP:19900101T000000Z\r\n\
647
2
UID:123\r\n\
648
2
DESCRIPTION;CN=hello:some text\r\n\
649
2
END:VEVENT\r\n\
650
2
END:VCALENDAR\r\n";
651
2

            
652
2
        let errors = validate_content(content);
653

            
654
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Common name (CN) is not allowed for this property type");
655
2
    }
656

            
657
    #[test]
658
2
    fn calendar_user_type_on_version_property() {
659
2
        let content = "BEGIN:VCALENDAR\r\n\
660
2
PRODID:test\r\n\
661
2
VERSION;CUTYPE=INDIVIDUAL:2.0\r\n\
662
2
BEGIN:X-NONE\r\n\
663
2
empty:value\r\n\
664
2
END:X-NONE\r\n\
665
2
END:VCALENDAR\r\n";
666
2

            
667
2
        let errors = validate_content(content);
668

            
669
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Calendar user type (CUTYPE) is not allowed for this property type");
670
2
    }
671

            
672
    #[test]
673
2
    fn calendar_user_type_on_description_property() {
674
2
        let content = "BEGIN:VCALENDAR\r\n\
675
2
PRODID:test\r\n\
676
2
VERSION:2.0\r\n\
677
2
METHOD:send\r\n\
678
2
BEGIN:VEVENT\r\n\
679
2
DTSTAMP:19900101T000000Z\r\n\
680
2
UID:123\r\n\
681
2
DESCRIPTION;CUTYPE=INDIVIDUAL:some text\r\n\
682
2
END:VEVENT\r\n\
683
2
END:VCALENDAR\r\n";
684
2

            
685
2
        let errors = validate_content(content);
686

            
687
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Calendar user type (CUTYPE) is not allowed for this property type");
688
2
    }
689

            
690
    #[test]
691
2
    fn delegated_from_on_version_property() {
692
2
        let content = "BEGIN:VCALENDAR\r\n\
693
2
PRODID:test\r\n\
694
2
VERSION;DELEGATED-FROM=\"mailto:hello@test.net\":2.0\r\n\
695
2
BEGIN:X-NONE\r\n\
696
2
empty:value\r\n\
697
2
END:X-NONE\r\n\
698
2
END:VCALENDAR\r\n";
699
2

            
700
2
        let errors = validate_content(content);
701

            
702
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Delegated from (DELEGATED-FROM) is not allowed for this property type");
703
2
    }
704

            
705
    #[test]
706
2
    fn delegated_from_on_description_property() {
707
2
        let content = "BEGIN:VCALENDAR\r\n\
708
2
PRODID:test\r\n\
709
2
VERSION:2.0\r\n\
710
2
METHOD:send\r\n\
711
2
BEGIN:VEVENT\r\n\
712
2
DTSTAMP:19900101T000000Z\r\n\
713
2
UID:123\r\n\
714
2
DESCRIPTION;DELEGATED-FROM=\"mailto:hello@test.net\":some text\r\n\
715
2
END:VEVENT\r\n\
716
2
END:VCALENDAR\r\n";
717
2

            
718
2
        let errors = validate_content(content);
719

            
720
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Delegated from (DELEGATED-FROM) is not allowed for this property type");
721
2
    }
722

            
723
    #[test]
724
2
    fn delegated_to_on_version_property() {
725
2
        let content = "BEGIN:VCALENDAR\r\n\
726
2
PRODID:test\r\n\
727
2
VERSION;DELEGATED-TO=\"mailto:hello@test.net\":2.0\r\n\
728
2
BEGIN:X-NONE\r\n\
729
2
empty:value\r\n\
730
2
END:X-NONE\r\n\
731
2
END:VCALENDAR\r\n";
732
2

            
733
2
        let errors = validate_content(content);
734

            
735
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Delegated to (DELEGATED-TO) is not allowed for this property type");
736
2
    }
737

            
738
    #[test]
739
2
    fn delegated_to_on_description_property() {
740
2
        let content = "BEGIN:VCALENDAR\r\n\
741
2
PRODID:test\r\n\
742
2
VERSION:2.0\r\n\
743
2
METHOD:send\r\n\
744
2
BEGIN:VEVENT\r\n\
745
2
DTSTAMP:19900101T000000Z\r\n\
746
2
UID:123\r\n\
747
2
DESCRIPTION;DELEGATED-TO=\"mailto:hello@test.net\":some text\r\n\
748
2
END:VEVENT\r\n\
749
2
END:VCALENDAR\r\n";
750
2

            
751
2
        let errors = validate_content(content);
752

            
753
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Delegated to (DELEGATED-TO) is not allowed for this property type");
754
2
    }
755

            
756
    #[test]
757
2
    fn dir_on_version_property() {
758
2
        let content = "BEGIN:VCALENDAR\r\n\
759
2
PRODID:test\r\n\
760
2
VERSION;DIR=\"ldap://example.com:6666/o=ABC\":2.0\r\n\
761
2
BEGIN:X-NONE\r\n\
762
2
empty:value\r\n\
763
2
END:X-NONE\r\n\
764
2
END:VCALENDAR\r\n";
765
2

            
766
2
        let errors = validate_content(content);
767

            
768
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Directory entry reference (DIR) is not allowed for this property type");
769
2
    }
770

            
771
    #[test]
772
2
    fn dir_on_description_property() {
773
2
        let content = "BEGIN:VCALENDAR\r\n\
774
2
PRODID:test\r\n\
775
2
VERSION:2.0\r\n\
776
2
METHOD:send\r\n\
777
2
BEGIN:VEVENT\r\n\
778
2
DTSTAMP:19900101T000000Z\r\n\
779
2
UID:123\r\n\
780
2
DESCRIPTION;DIR=\"ldap://example.com:6666/o=ABC\":some text\r\n\
781
2
END:VEVENT\r\n\
782
2
END:VCALENDAR\r\n";
783
2

            
784
2
        let errors = validate_content(content);
785

            
786
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Directory entry reference (DIR) is not allowed for this property type");
787
2
    }
788

            
789
    #[test]
790
2
    fn encoding_not_set_on_binary_value() {
791
2
        let content = "BEGIN:VCALENDAR\r\n\
792
2
PRODID:test\r\n\
793
2
VERSION:2.0\r\n\
794
2
METHOD:send\r\n\
795
2
BEGIN:VEVENT\r\n\
796
2
DTSTAMP:19900101T000000Z\r\n\
797
2
UID:123\r\n\
798
2
ATTACH;VALUE=BINARY:eA==\r\n\
799
2
END:VEVENT\r\n\
800
2
END:VCALENDAR\r\n";
801
2

            
802
2
        let errors = validate_content(content);
803

            
804
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"ATTACH\" at index 2: Property is declared to have a binary value but no encoding is set, must be set to BASE64");
805
2
    }
806

            
807
    #[test]
808
2
    fn encoding_set_to_8bit_on_binary_value() {
809
2
        let content = "BEGIN:VCALENDAR\r\n\
810
2
PRODID:test\r\n\
811
2
VERSION:2.0\r\n\
812
2
METHOD:send\r\n\
813
2
BEGIN:VEVENT\r\n\
814
2
DTSTAMP:19900101T000000Z\r\n\
815
2
UID:123\r\n\
816
2
ATTACH;VALUE=BINARY;ENCODING=8BIT:eA==\r\n\
817
2
END:VEVENT\r\n\
818
2
END:VCALENDAR\r\n";
819
2

            
820
2
        let errors = validate_content(content);
821

            
822
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"ATTACH\" at index 2: Property is declared to have a binary value but the encoding is set to 8BIT, instead of BASE64");
823
2
    }
824

            
825
    #[test]
826
2
    fn fmt_type_on_version_property() {
827
2
        let content = "BEGIN:VCALENDAR\r\n\
828
2
PRODID:test\r\n\
829
2
VERSION;FMTTYPE=text/plain:2.0\r\n\
830
2
BEGIN:X-NONE\r\n\
831
2
empty:value\r\n\
832
2
END:X-NONE\r\n\
833
2
END:VCALENDAR\r\n";
834
2

            
835
2
        let errors = validate_content(content);
836

            
837
        assert_errors!(
838
            errors,
839
            "In calendar property \"VERSION\" at index 1: FMTTYPE is not allowed"
840
        );
841
2
    }
842

            
843
    #[test]
844
2
    fn fmt_type_on_description_property() {
845
2
        let content = "BEGIN:VCALENDAR\r\n\
846
2
PRODID:test\r\n\
847
2
VERSION:2.0\r\n\
848
2
METHOD:send\r\n\
849
2
BEGIN:VEVENT\r\n\
850
2
DTSTAMP:19900101T000000Z\r\n\
851
2
UID:123\r\n\
852
2
DESCRIPTION;FMTTYPE=text/plain:some text\r\n\
853
2
END:VEVENT\r\n\
854
2
END:VCALENDAR\r\n";
855
2

            
856
2
        let errors = validate_content(content);
857

            
858
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: FMTTYPE is not allowed");
859
2
    }
860

            
861
    #[test]
862
2
    fn free_busy_type_on_version_property() {
863
2
        let content = "BEGIN:VCALENDAR\r\n\
864
2
PRODID:test\r\n\
865
2
VERSION;FBTYPE=BUSY:2.0\r\n\
866
2
BEGIN:X-NONE\r\n\
867
2
empty:value\r\n\
868
2
END:X-NONE\r\n\
869
2
END:VCALENDAR\r\n";
870
2

            
871
2
        let errors = validate_content(content);
872

            
873
        assert_errors!(
874
            errors,
875
            "In calendar property \"VERSION\" at index 1: FBTYPE is not allowed"
876
        );
877
2
    }
878

            
879
    #[test]
880
2
    fn free_busy_type_on_description_property() {
881
2
        let content = "BEGIN:VCALENDAR\r\n\
882
2
PRODID:test\r\n\
883
2
VERSION:2.0\r\n\
884
2
METHOD:send\r\n\
885
2
BEGIN:VEVENT\r\n\
886
2
DTSTAMP:19900101T000000Z\r\n\
887
2
UID:123\r\n\
888
2
DESCRIPTION;FBTYPE=BUSY:some text\r\n\
889
2
END:VEVENT\r\n\
890
2
END:VCALENDAR\r\n";
891
2

            
892
2
        let errors = validate_content(content);
893

            
894
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: FBTYPE is not allowed");
895
2
    }
896

            
897
    #[test]
898
2
    fn language_on_version_property() {
899
2
        let content = "BEGIN:VCALENDAR\r\n\
900
2
PRODID:test\r\n\
901
2
VERSION;LANGUAGE=en-US:2.0\r\n\
902
2
BEGIN:X-NONE\r\n\
903
2
empty:value\r\n\
904
2
END:X-NONE\r\n\
905
2
END:VCALENDAR\r\n";
906
2

            
907
2
        let errors = validate_content(content);
908

            
909
        assert_errors!(
910
            errors,
911
            "In calendar property \"VERSION\" at index 1: LANGUAGE is not allowed"
912
        );
913
2
    }
914

            
915
    #[test]
916
2
    fn language_on_date_time_start_property() {
917
2
        let content = "BEGIN:VCALENDAR\r\n\
918
2
PRODID:test\r\n\
919
2
VERSION:2.0\r\n\
920
2
METHOD:send\r\n\
921
2
BEGIN:VEVENT\r\n\
922
2
DTSTAMP:19900101T000000Z\r\n\
923
2
UID:123\r\n\
924
2
DTSTART;LANGUAGE=en-US:19970101T230000\r\n\
925
2
END:VEVENT\r\n\
926
2
END:VCALENDAR\r\n";
927
2

            
928
2
        let errors = validate_content(content);
929

            
930
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DTSTART\" at index 2: LANGUAGE is not allowed");
931
2
    }
932

            
933
    #[test]
934
2
    fn member_on_version_property() {
935
2
        let content = "BEGIN:VCALENDAR\r\n\
936
2
PRODID:test\r\n\
937
2
VERSION;MEMBER=\"mailto:hello@test.net\":2.0\r\n\
938
2
BEGIN:X-NONE\r\n\
939
2
empty:value\r\n\
940
2
END:X-NONE\r\n\
941
2
END:VCALENDAR\r\n";
942
2

            
943
2
        let errors = validate_content(content);
944

            
945
        assert_errors!(
946
            errors,
947
            "In calendar property \"VERSION\" at index 1: Group or list membership (MEMBER) is not allowed for this property type"
948
        );
949
2
    }
950

            
951
    #[test]
952
2
    fn member_on_description_property() {
953
2
        let content = "BEGIN:VCALENDAR\r\n\
954
2
PRODID:test\r\n\
955
2
VERSION:2.0\r\n\
956
2
METHOD:send\r\n\
957
2
BEGIN:VEVENT\r\n\
958
2
DTSTAMP:19900101T000000Z\r\n\
959
2
UID:123\r\n\
960
2
DESCRIPTION;MEMBER=\"mailto:hello@test.net\":some text\r\n\
961
2
END:VEVENT\r\n\
962
2
END:VCALENDAR\r\n";
963
2

            
964
2
        let errors = validate_content(content);
965

            
966
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Group or list membership (MEMBER) is not allowed for this property type");
967
2
    }
968

            
969
    #[test]
970
2
    fn part_stat_wrong_value_in_event() {
971
2
        let content = "BEGIN:VCALENDAR\r\n\
972
2
PRODID:test\r\n\
973
2
VERSION:2.0\r\n\
974
2
METHOD:send\r\n\
975
2
BEGIN:VEVENT\r\n\
976
2
DTSTAMP:19900101T000000Z\r\n\
977
2
UID:123\r\n\
978
2
ATTENDEE;PARTSTAT=COMPLETED:mailto:hello@test.net\r\n\
979
2
END:VEVENT\r\n\
980
2
END:VCALENDAR\r\n";
981
2

            
982
2
        let errors = validate_content(content);
983

            
984
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"ATTENDEE\" at index 2: Invalid participation status (PARTSTAT) value [Completed] in a VEVENT component context");
985
2
    }
986

            
987
    #[test]
988
2
    fn part_stat_wrong_value_in_journal() {
989
2
        let content = "BEGIN:VCALENDAR\r\n\
990
2
PRODID:test\r\n\
991
2
VERSION:2.0\r\n\
992
2
BEGIN:VJOURNAL\r\n\
993
2
DTSTAMP:19900101T000000Z\r\n\
994
2
UID:123\r\n\
995
2
ATTENDEE;PARTSTAT=IN-PROCESS:mailto:hello@test.net\r\n\
996
2
END:VJOURNAL\r\n\
997
2
END:VCALENDAR\r\n";
998
2

            
999
2
        let errors = validate_content(content);
        assert_errors!(&errors,"In component \"VJOURNAL\" at index 0, in component property \"ATTENDEE\" at index 2: Invalid participation status (PARTSTAT) value [InProcess] in a VJOURNAL component context");
2
    }
    #[test]
2
    fn part_stat_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;PARTSTAT=ACCEPTED:2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Participation status (PARTSTAT) is not allowed for this property type");
2
    }
    #[test]
2
    fn part_stat_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DESCRIPTION;PARTSTAT=NEEDS-ACTION:some text\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Participation status (PARTSTAT) is not allowed for this property type");
2
    }
    #[test]
2
    fn range_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;RANGE=THISANDFUTURE:2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In calendar property \"VERSION\" at index 1: RANGE is not allowed"
        );
2
    }
    #[test]
2
    fn range_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DESCRIPTION;RANGE=THISANDFUTURE:some text\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: RANGE is not allowed");
2
    }
    #[test]
2
    fn related_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;RELATED=END:2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Related (RELATED) is not allowed for this property type");
2
    }
    #[test]
2
    fn related_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DESCRIPTION;RELATED=START:some text\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Related (RELATED) is not allowed for this property type");
2
    }
    #[test]
2
    fn relationship_type_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;RELTYPE=SIBLING:2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Relationship type (RELTYPE) is not allowed for this property type");
2
    }
    #[test]
2
    fn relationship_type_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART;RELTYPE=SIBLING:19920101T000010Z\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DTSTART\" at index 2: Relationship type (RELTYPE) is not allowed for this property type");
2
    }
    #[test]
2
    fn role_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;ROLE=CHAIR:2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In calendar property \"VERSION\" at index 1: Participation role (ROLE) is not allowed for this property type");
2
    }
    #[test]
2
    fn role_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DESCRIPTION;ROLE=CHAIN:some text\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Participation role (ROLE) is not allowed for this property type");
2
    }
    #[test]
2
    fn rsvp_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;RSVP=TRUE:2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In calendar property \"VERSION\" at index 1: RSVP expectation (RSVP) is not allowed for this property type");
2
    }
    #[test]
2
    fn rsvp_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DESCRIPTION;RSVP=FALSE:some text\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: RSVP expectation (RSVP) is not allowed for this property type");
2
    }
    #[test]
2
    fn sent_by_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;SENT-BY=\"mailto:hello@test.net\":2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(errors, "In calendar property \"VERSION\" at index 1: Sent by (SENT-BY) is not allowed for this property type");
2
    }
    #[test]
2
    fn sent_by_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DESCRIPTION;SENT-BY=\"mailto:hello@test.net\":some text\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Sent by (SENT-BY) is not allowed for this property type");
2
    }
    #[test]
2
    fn sent_by_with_invalid_protocol() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
ORGANIZER;SENT-BY=\"http:hello@test.net\":mailto:world@test.net\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In component \"VEVENT\" at index 0, in component property \"ORGANIZER\" at index 2: Sent by (SENT-BY) must be a 'mailto:' URI");
2
    }
    #[test]
2
    fn missing_tz_id() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART;TZID=missing:20240606T220000\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In component \"VEVENT\" at index 0, in component property \"DTSTART\" at index 2: Required time zone ID [missing] is not defined in the calendar");
2
    }
    #[test]
2
    fn tz_id_specified_on_utc_start() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VTIMEZONE\r\n\
2
TZID:any\r\n\
2
BEGIN:STANDARD\r\n\
2
DTSTART:19671029T020000\r\n\
2
TZOFFSETFROM:-0400\r\n\
2
TZOFFSETTO:-0500\r\n\
2
END:STANDARD\r\n\
2
END:VTIMEZONE\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART;TZID=any:20240606T220000Z\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In component \"VEVENT\" at index 1, in component property \"DTSTART\" at index 2: Time zone ID (TZID) cannot be specified on a property with a UTC time");
2
    }
    #[test]
2
    fn tz_id_specified_on_date_start() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTIMEZONE\r\n\
2
TZID:any\r\n\
2
BEGIN:STANDARD\r\n\
2
DTSTART:19671029T020000\r\n\
2
TZOFFSETFROM:-0400\r\n\
2
TZOFFSETTO:-0500\r\n\
2
END:STANDARD\r\n\
2
END:VTIMEZONE\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART;VALUE=DATE;TZID=any:20240606\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In component \"VEVENT\" at index 1, in component property \"DTSTART\" at index 2: Time zone ID (TZID) is not allowed for the property value type DATE");
2
    }
    #[test]
2
    fn tz_id_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;TZID=/test:2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In calendar property \"VERSION\" at index 1: TZID is not allowed"
        );
2
    }
    #[test]
2
    fn tz_id_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DESCRIPTION;TZID=/test:some text\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: TZID is not allowed");
2
    }
    #[test]
2
    fn value_on_version_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION;VALUE=TEXT:2.0\r\n\
2
BEGIN:X-NONE\r\n\
2
empty:value\r\n\
2
END:X-NONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In calendar property \"VERSION\" at index 1: VALUE is not allowed"
        );
2
    }
    #[test]
2
    fn value_on_description_property() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DESCRIPTION;VALUE=INTEGER:some text\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: Property is declared to have an integer value but that is not valid for this property",
            "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: VALUE is not allowed"
        );
2
    }
    #[test]
2
    fn event_missing_required_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VEVENT\r\n\
2
X-ANY:test\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0: DTSTAMP is required",
            "In component \"VEVENT\" at index 0: UID is required",
            "In component \"VEVENT\" at index 0: DTSTART is required",
        );
2
    }
    #[test]
2
    fn event_duplicate_optional_once_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
CLASS:PUBLIC\r\n\
2
CLASS:PUBLIC\r\n\
2
CREATED:19900101T000000Z\r\n\
2
CREATED:19900101T000000Z\r\n\
2
DESCRIPTION:some text\r\n\
2
DESCRIPTION:some text\r\n\
2
GEO:1.1;2.2\r\n\
2
GEO:1.1;2.2\r\n\
2
LAST-MODIFIED:19900101T000000Z\r\n\
2
LAST-MODIFIED:19900101T000000Z\r\n\
2
LOCATION:some location\r\n\
2
LOCATION:some location\r\n\
2
ORGANIZER:mailto:hello@test.net\r\n\
2
ORGANIZER:mailto:hello@test.net\r\n\
2
PRIORITY:5\r\n\
2
PRIORITY:5\r\n\
2
SEQUENCE:0\r\n\
2
SEQUENCE:0\r\n\
2
STATUS:CONFIRMED\r\n\
2
STATUS:CONFIRMED\r\n\
2
SUMMARY:some summary\r\n\
2
SUMMARY:some summary\r\n\
2
TRANSP:OPAQUE\r\n\
2
TRANSP:OPAQUE\r\n\
2
URL:http://example.com\r\n\
2
URL:http://example.com\r\n\
2
RECURRENCE-ID:19900101T000000Z\r\n\
2
RECURRENCE-ID:19900101T000000Z\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"DTSTART\" at index 3: DTSTART must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"CLASS\" at index 5: CLASS must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"CREATED\" at index 7: CREATED must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 9: DESCRIPTION must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"GEO\" at index 11: GEO must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"LAST-MODIFIED\" at index 13: LAST-MODIFIED must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"LOCATION\" at index 15: LOCATION must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"ORGANIZER\" at index 17: ORGANIZER must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"PRIORITY\" at index 19: PRIORITY must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"SEQUENCE\" at index 21: SEQUENCE must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"STATUS\" at index 23: STATUS must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"SUMMARY\" at index 25: SUMMARY must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"TRANSP\" at index 27: TRANSP must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"URL\" at index 29: URL must only appear once",
            "In component \"VEVENT\" at index 0, in component property \"RECURRENCE-ID\" at index 31: RECURRENCE-ID must only appear once",
        );
2
    }
    #[test]
2
    fn event_duplicate_date_time_end() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTEND:19900101T000000Z\r\n\
2
DTEND:19900101T000000Z\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"DTEND\" at index 3: DTEND must only appear once",
        );
2
    }
    #[test]
2
    fn event_duplicate_duration() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DURATION:PT1H\r\n\
2
DURATION:PT1H\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"DURATION\" at index 3: DURATION must only appear once",
        );
2
    }
    #[test]
2
    fn event_both_date_time_end_and_duration() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTEND:19900101T000000Z\r\n\
2
DURATION:PT1H\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0: Both DTEND and DURATION properties are present, only one is allowed",
        );
2
    }
    #[test]
2
    fn todo_missing_required_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTODO\r\n\
2
X-ANY:test\r\n\
2
END:VTODO\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTODO\" at index 0: DTSTAMP is required",
            "In component \"VTODO\" at index 0: UID is required",
        );
2
    }
    #[test]
2
    fn todo_duplicate_optional_once_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTODO\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
CLASS:PUBLIC\r\n\
2
CLASS:PUBLIC\r\n\
2
COMPLETE:19900101T000000Z\r\n\
2
COMPLETE:19900101T000000Z\r\n\
2
CREATED:19900101T000000Z\r\n\
2
CREATED:19900101T000000Z\r\n\
2
DESCRIPTION:some text\r\n\
2
DESCRIPTION:some text\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
GEO:1.1;2.2\r\n\
2
GEO:1.1;2.2\r\n\
2
LAST-MODIFIED:19900101T000000Z\r\n\
2
LAST-MODIFIED:19900101T000000Z\r\n\
2
LOCATION:some location\r\n\
2
LOCATION:some location\r\n\
2
ORGANIZER:mailto:hello@test.net\r\n\
2
ORGANIZER:mailto:hello@test.net\r\n\
2
PERCENT-COMPLETE:50\r\n\
2
PERCENT-COMPLETE:50\r\n\
2
PRIORITY:5\r\n\
2
PRIORITY:5\r\n\
2
RECURRENCE-ID:19900101T000000Z\r\n\
2
RECURRENCE-ID:19900101T000000Z\r\n\
2
SEQUENCE:0\r\n\
2
SEQUENCE:0\r\n\
2
STATUS:COMPLETED\r\n\
2
STATUS:COMPLETED\r\n\
2
SUMMARY:some summary\r\n\
2
SUMMARY:some summary\r\n\
2
URL:http://example.com\r\n\
2
URL:http://example.com\r\n\
2
END:VTODO\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTODO\" at index 0, in component property \"CLASS\" at index 3: CLASS must only appear once",
            "In component \"VTODO\" at index 0, in component property \"CREATED\" at index 7: CREATED must only appear once",
            "In component \"VTODO\" at index 0, in component property \"DESCRIPTION\" at index 9: DESCRIPTION must only appear once",
            "In component \"VTODO\" at index 0, in component property \"DTSTART\" at index 11: DTSTART must only appear once",
            "In component \"VTODO\" at index 0, in component property \"GEO\" at index 13: GEO must only appear once",
            "In component \"VTODO\" at index 0, in component property \"LAST-MODIFIED\" at index 15: LAST-MODIFIED must only appear once",
            "In component \"VTODO\" at index 0, in component property \"LOCATION\" at index 17: LOCATION must only appear once",
            "In component \"VTODO\" at index 0, in component property \"ORGANIZER\" at index 19: ORGANIZER must only appear once",
            "In component \"VTODO\" at index 0, in component property \"PERCENT-COMPLETE\" at index 21: PERCENT-COMPLETE must only appear once",
            "In component \"VTODO\" at index 0, in component property \"PRIORITY\" at index 23: PRIORITY must only appear once",
            "In component \"VTODO\" at index 0, in component property \"RECURRENCE-ID\" at index 25: RECURRENCE-ID must only appear once",
            "In component \"VTODO\" at index 0, in component property \"SEQUENCE\" at index 27: SEQUENCE must only appear once",
            "In component \"VTODO\" at index 0, in component property \"STATUS\" at index 29: STATUS must only appear once",
            "In component \"VTODO\" at index 0, in component property \"SUMMARY\" at index 31: SUMMARY must only appear once",
            "In component \"VTODO\" at index 0, in component property \"URL\" at index 33: URL must only appear once",
        );
2
    }
    #[test]
2
    fn todo_duplicate_due() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTODO\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DUE:19900101T000000Z\r\n\
2
DUE:19900101T000000Z\r\n\
2
END:VTODO\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTODO\" at index 0, in component property \"DUE\" at index 3: DUE must only appear once",
        );
2
    }
    #[test]
2
    fn todo_duplicate_duration() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTODO\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
DURATION:PT1H\r\n\
2
DURATION:PT1H\r\n\
2
END:VTODO\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTODO\" at index 0, in component property \"DURATION\" at index 4: DURATION must only appear once",
        );
2
    }
    #[test]
2
    fn todo_duration_without_date_time_start() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTODO\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DURATION:PT1H\r\n\
2
END:VTODO\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTODO\" at index 0: DURATION property is present but no DTSTART property is present",
        );
2
    }
    #[test]
2
    fn todo_both_due_and_duration() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTODO\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
DUE:19900101T000000Z\r\n\
2
DURATION:PT1H\r\n\
2
END:VTODO\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTODO\" at index 0: Both DUE and DURATION properties are present, only one is allowed",
        );
2
    }
    #[test]
2
    fn journal_missing_required_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VJOURNAL\r\n\
2
X-ANY:test\r\n\
2
END:VJOURNAL\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VJOURNAL\" at index 0: DTSTAMP is required",
            "In component \"VJOURNAL\" at index 0: UID is required",
        );
2
    }
    #[test]
2
    fn journal_duplicate_optional_once_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VJOURNAL\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
CLASS:PUBLIC\r\n\
2
CLASS:PUBLIC\r\n\
2
CREATED:19900101T000000Z\r\n\
2
CREATED:19900101T000000Z\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
LAST-MODIFIED:19900101T000000Z\r\n\
2
LAST-MODIFIED:19900101T000000Z\r\n\
2
ORGANIZER:mailto:hello@test.net\r\n\
2
ORGANIZER:mailto:hello@test.net\r\n\
2
RECURRENCE-ID:19900101T000000Z\r\n\
2
RECURRENCE-ID:19900101T000000Z\r\n\
2
SEQUENCE:0\r\n\
2
SEQUENCE:0\r\n\
2
STATUS:FINAL\r\n\
2
STATUS:FINAL\r\n\
2
SUMMARY:some summary\r\n\
2
SUMMARY:some summary\r\n\
2
URL:http://example.com\r\n\
2
URL:http://example.com\r\n\
2
END:VJOURNAL\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VJOURNAL\" at index 0, in component property \"CLASS\" at index 3: CLASS must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"CREATED\" at index 5: CREATED must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"DTSTART\" at index 7: DTSTART must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"LAST-MODIFIED\" at index 9: LAST-MODIFIED must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"ORGANIZER\" at index 11: ORGANIZER must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"RECURRENCE-ID\" at index 13: RECURRENCE-ID must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"SEQUENCE\" at index 15: SEQUENCE must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"STATUS\" at index 17: STATUS must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"SUMMARY\" at index 19: SUMMARY must only appear once",
            "In component \"VJOURNAL\" at index 0, in component property \"URL\" at index 21: URL must only appear once"
        );
2
    }
    #[test]
2
    fn free_busy_missing_required_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VFREEBUSY\r\n\
2
X-ANY:test\r\n\
2
END:VFREEBUSY\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VFREEBUSY\" at index 0: DTSTAMP is required",
            "In component \"VFREEBUSY\" at index 0: UID is required",
        );
2
    }
    #[test]
2
    fn free_busy_duplicate_optional_once_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VFREEBUSY\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
CONTACT:mailto:hello@test.net\r\n\
2
CONTACT:mailto:hello@test.net\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
DTEND:19900101T000000Z\r\n\
2
DTEND:19900101T000000Z\r\n\
2
ORGANIZER:mailto:admin@test.net\r\n\
2
ORGANIZER:mailto:admin@test.net\r\n\
2
URL:http://example.com\r\n\
2
URL:http://example.com\r\n\
2
END:VFREEBUSY\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VFREEBUSY\" at index 0, in component property \"CONTACT\" at index 3: CONTACT must only appear once",
            "In component \"VFREEBUSY\" at index 0, in component property \"DTSTART\" at index 5: DTSTART must only appear once",
            "In component \"VFREEBUSY\" at index 0, in component property \"DTEND\" at index 7: DTEND must only appear once",
            "In component \"VFREEBUSY\" at index 0, in component property \"ORGANIZER\" at index 9: ORGANIZER must only appear once",
            "In component \"VFREEBUSY\" at index 0, in component property \"URL\" at index 11: URL must only appear once",
        );
2
    }
    #[test]
2
    fn time_zone_missing_required_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTIMEZONE\r\n\
2
X-ANY:test\r\n\
2
BEGIN:STANDARD\r\n\
2
DTSTART:19900101T000000\r\n\
2
TZOFFSETTO:+0000\r\n\
2
TZOFFSETFROM:+0000\r\n\
2
END:STANDARD\r\n\
2
END:VTIMEZONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTIMEZONE\" at index 0: TZID is required",
        );
2
    }
    #[test]
2
    fn time_zone_missing_required_nested_components() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTIMEZONE\r\n\
2
TZID:America/New_York\r\n\
2
END:VTIMEZONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTIMEZONE\" at index 0: No standard or daylight components found in time zone, required at least one",
        );
2
    }
    #[test]
2
    fn time_zone_duplicate_optional_once_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTIMEZONE\r\n\
2
TZID:America/New_York\r\n\
2
LAST-MODIFIED:20050809T050000Z\r\n\
2
LAST-MODIFIED:20050809T050000Z\r\n\
2
TZURL:http://example.com\r\n\
2
TZURL:http://example.com\r\n\
2
BEGIN:STANDARD\r\n\
2
DTSTART:19900101T000000\r\n\
2
TZOFFSETTO:+0000\r\n\
2
TZOFFSETFROM:+0000\r\n\
2
END:STANDARD\r\n\
2
END:VTIMEZONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTIMEZONE\" at index 0, in component property \"LAST-MODIFIED\" at index 2: LAST-MODIFIED must only appear once",
            "In component \"VTIMEZONE\" at index 0, in component property \"TZURL\" at index 4: TZURL must only appear once",
        );
2
    }
    #[test]
2
    fn time_zone_nested_missing_required_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VTIMEZONE\r\n\
2
TZID:America/New_York\r\n\
2
BEGIN:STANDARD\r\n\
2
X-ANY:test\r\n\
2
END:STANDARD\r\n\
2
BEGIN:DAYLIGHT\r\n\
2
X-ANY:test\r\n\
2
END:DAYLIGHT\r\n\
2
END:VTIMEZONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VTIMEZONE\" at index 0, in nested component \"STANDARD\" at index 0: DTSTART is required",
            "In component \"VTIMEZONE\" at index 0, in nested component \"STANDARD\" at index 0: TZOFFSETTO is required",
            "In component \"VTIMEZONE\" at index 0, in nested component \"STANDARD\" at index 0: TZOFFSETFROM is required",
            "In component \"VTIMEZONE\" at index 0, in nested component \"DAYLIGHT\" at index 1: DTSTART is required",
            "In component \"VTIMEZONE\" at index 0, in nested component \"DAYLIGHT\" at index 1: TZOFFSETTO is required",
            "In component \"VTIMEZONE\" at index 0, in nested component \"DAYLIGHT\" at index 1: TZOFFSETFROM is required",
        );
2
    }
    #[test]
2
    fn alarm_missing_action() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
BEGIN:VALARM\r\n\
2
X-ANY:test\r\n\
2
END:VALARM\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0: Required exactly one ACTION property but found 0",
        );
2
    }
    #[test]
2
    fn alarm_missing_required_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:AUDIO\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:DISPLAY\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:EMAIL\r\n\
2
END:VALARM\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0: TRIGGER is required",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 1: TRIGGER is required",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 1: DESCRIPTION is required",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2: TRIGGER is required",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2: DESCRIPTION is required",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2: SUMMARY is required",
        );
2
    }
    #[test]
2
    fn alarm_missing_duplicate_optional_once_properties() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:AUDIO\r\n\
2
TRIGGER:P3W\r\n\
2
DURATION:PT15M\r\n\
2
DURATION:PT15M\r\n\
2
REPEAT:2\r\n\
2
REPEAT:2\r\n\
2
ATTACH:ftp://example.com/pub/sounds/bell-01.aud\r\n\
2
ATTACH:ftp://example.com/pub/sounds/bell-01.aud\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:DISPLAY\r\n\
2
TRIGGER:P3W\r\n\
2
DESCRIPTION:Breakfast meeting with executive\r\n\
2
DURATION:PT15M\r\n\
2
DURATION:PT15M\r\n\
2
REPEAT:2\r\n\
2
REPEAT:2\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:EMAIL\r\n\
2
TRIGGER:P3W\r\n\
2
SUMMARY:New event\r\n\
2
DESCRIPTION:Breakfast meeting with executive\r\n\
2
DURATION:PT15M\r\n\
2
DURATION:PT15M\r\n\
2
REPEAT:2\r\n\
2
REPEAT:2\r\n\
2
END:VALARM\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0, in nested component property \"DURATION\" at index 3: DURATION must only appear once",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0, in nested component property \"REPEAT\" at index 5: REPEAT must only appear once",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0, in nested component property \"ATTACH\" at index 7: ATTACH must only appear once",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 1, in nested component property \"DURATION\" at index 4: DURATION must only appear once",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 1, in nested component property \"REPEAT\" at index 6: REPEAT must only appear once",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2, in nested component property \"DURATION\" at index 5: DURATION must only appear once",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2, in nested component property \"REPEAT\" at index 7: REPEAT must only appear once",
        );
2
    }
    #[test]
2
    fn alarm_duration_and_trigger_not_present_together() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:AUDIO\r\n\
2
TRIGGER:P3W\r\n\
2
DURATION:PT15M\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:AUDIO\r\n\
2
TRIGGER:P3W\r\n\
2
REPEAT:2\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:DISPLAY\r\n\
2
TRIGGER:P3W\r\n\
2
DESCRIPTION:Breakfast meeting with executive\r\n\
2
DURATION:PT15M\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:DISPLAY\r\n\
2
TRIGGER:P3W\r\n\
2
DESCRIPTION:Breakfast meeting with executive\r\n\
2
REPEAT:2\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:EMAIL\r\n\
2
TRIGGER:P3W\r\n\
2
SUMMARY:New event\r\n\
2
DESCRIPTION:Breakfast meeting with executive\r\n\
2
DURATION:PT15M\r\n\
2
END:VALARM\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:EMAIL\r\n\
2
TRIGGER:P3W\r\n\
2
SUMMARY:New event\r\n\
2
DESCRIPTION:Breakfast meeting with executive\r\n\
2
REPEAT:2\r\n\
2
END:VALARM\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0: DURATION and REPEAT properties must be present together",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 1: DURATION and REPEAT properties must be present together",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2: DURATION and REPEAT properties must be present together",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 3: DURATION and REPEAT properties must be present together",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 4: DURATION and REPEAT properties must be present together",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 5: DURATION and REPEAT properties must be present together",
        );
2
    }
    #[test]
2
    fn default_value_specified() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
ATTACH;VALUE=URI:ftp://example.com/pub/sounds/bell-01.aud\r\n\
2
DTEND;VALUE=DATE-TIME:19900101T000000Z\r\n\
2
DTSTART;VALUE=DATE-TIME:19900101T000000Z\r\n\
2
EXDATE;VALUE=DATE-TIME:19900101T000000Z\r\n\
2
RDATE;VALUE=DATE-TIME:19900101T000000Z\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:DISPLAY\r\n\
2
DESCRIPTION:Breakfast meeting with executive\r\n\
2
TRIGGER;VALUE=DURATION:PT15M\r\n\
2
END:VALARM\r\n\
2
END:VEVENT\r\n\
2
BEGIN:VTODO\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DUE;VALUE=DATE-TIME:19900101T000000Z\r\n\
2
END:VTODO\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"ATTACH\" at index 2: Redundant value specification which matches the default value",
            "In component \"VEVENT\" at index 0, in component property \"DTEND\" at index 3: Redundant value specification which matches the default value",
            "In component \"VEVENT\" at index 0, in component property \"DTSTART\" at index 4: Redundant value specification which matches the default value",
            "In component \"VEVENT\" at index 0, in component property \"EXDATE\" at index 5: Redundant value specification which matches the default value",
            "In component \"VEVENT\" at index 0, in component property \"RDATE\" at index 6: Redundant value specification which matches the default value",
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0, in nested component property \"TRIGGER\" at index 2: Redundant value specification which matches the default value",
            "In component \"VTODO\" at index 1, in component property \"DUE\" at index 2: Redundant value specification which matches the default value",
        );
2
    }
    #[test]
2
    fn iana_component() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:ANY\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
END:ANY\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_no_errors!(errors);
2
    }
    #[test]
2
    fn standard_at_top_level() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:DAYLIGHT\r\n\
2
X-ANY:test\r\n\
2
END:DAYLIGHT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        // Gets picked up as IANA
        assert_no_errors!(errors);
2
    }
    #[test]
2
    fn x_property_value_type_checks() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
X-TIME-INVALID-HOUR;VALUE=TIME:250000\r\n\
2
X-TIME-INVALID-MINUTE;VALUE=TIME:007000\r\n\
2
X-TIME-INVALID-SECOND;VALUE=TIME:000070\r\n\
2
X-TIME-VALID;VALUE=TIME:235960\r\n\
2
X-UTC-OFFSET-NEGATIVE-ZERO;VALUE=UTC-OFFSET:-000000\r\n\
2
X-UTC-OFFSET-NEGATIVE-NON-ZERO;VALUE=UTC-OFFSET:-000001\r\n\
2
X-UTC-OFFSET-INVALID-MINUTE;VALUE=UTC-OFFSET:+006000\r\n\
2
X-BASE-64;VALUE=BINARY;ENCODING=8BIT:nope\r\n\
2
X-BASE-64;VALUE=BINARY;ENCODING=BASE64:##\r\n\
2
X-BOOLEAN;VALUE=BOOLEAN:wendy\r\n\
2
X-CAL-ADDRESS-NOT-URL;VALUE=CAL-ADDRESS:test\r\n\
2
X-CAL-ADDRESS-NOT-MAILTO;VALUE=CAL-ADDRESS:mailto:hello@test.net\r\n\
2
X-DATE;VALUE=DATE:19900101T000120\r\n\
2
X-DATE-TIME;VALUE=DATE-TIME:19900101T000000P\r\n\
2
X-DURATION;VALUE=DURATION:3W\r\n\
2
X-FLOAT;VALUE=FLOAT:3.14.15\r\n\
2
X-INTEGER;VALUE=INTEGER:3.14\r\n\
2
X-PERIOD;VALUE=PERIOD:19900101T000000Z/19900101T000000W\r\n\
2
X-RECUR;VALUE=RECUR:19900101T000000Z\r\n\
2
X-TEXT;VALUE=TEXT:\\p\r\n\
2
X-URI;VALUE=URI:hello\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"X-TIME-INVALID-HOUR\" at index 2: Found an invalid time at index 0 - Hour must be between 0 and 23",
            "In component \"VEVENT\" at index 0, in component property \"X-TIME-INVALID-MINUTE\" at index 3: Found an invalid time at index 0 - Minute must be between 0 and 59",
            "In component \"VEVENT\" at index 0, in component property \"X-TIME-INVALID-SECOND\" at index 4: Found an invalid time at index 0 - Second must be between 0 and 60",
            "In component \"VEVENT\" at index 0, in component property \"X-UTC-OFFSET-NEGATIVE-ZERO\" at index 6: Found an invalid UTC offset - UTC offset must have a non-zero value if it is negative",
            "In component \"VEVENT\" at index 0, in component property \"X-UTC-OFFSET-INVALID-MINUTE\" at index 8: Found an invalid UTC offset - Minutes must be between 0 and 59",
            "In component \"VEVENT\" at index 0, in component property \"X-BASE-64\" at index 9: Property is declared to have a binary value but the encoding is set to 8BIT, instead of BASE64",
            "In component \"VEVENT\" at index 0, in component property \"X-BASE-64\" at index 10: Property is declared to have a binary value but the value is not base64",
            "In component \"VEVENT\" at index 0, in component property \"X-BOOLEAN\" at index 11: Property is declared to have a boolean value but the value is not a boolean",
            "In component \"VEVENT\" at index 0, in component property \"X-CAL-ADDRESS-NOT-URL\" at index 12: Property is declared to have a calendar address value but the value is not a mailto: URI",
            "In component \"VEVENT\" at index 0, in component property \"X-DATE\" at index 14: Property is declared to have a date value but the value is not a date",
            "In component \"VEVENT\" at index 0, in component property \"X-DATE-TIME\" at index 15: Property is declared to have a date-time value but the value is not a date-time",
            "In component \"VEVENT\" at index 0, in component property \"X-DURATION\" at index 16: Property is declared to have a duration value but the value is not a duration",
            "In component \"VEVENT\" at index 0, in component property \"X-FLOAT\" at index 17: Property is declared to have a float value but the value is not a float",
            "In component \"VEVENT\" at index 0, in component property \"X-INTEGER\" at index 18: Property is declared to have an integer value but the value is not an integer",
            "In component \"VEVENT\" at index 0, in component property \"X-PERIOD\" at index 19: Property is declared to have a period value but the value is not a period",
            "In component \"VEVENT\" at index 0, in component property \"X-RECUR\" at index 20: Property is declared to have a recurrence value but the value is not a recurrence",
            "In component \"VEVENT\" at index 0, in component property \"X-TEXT\" at index 21: Property is declared to have a text value but the value is not a text",
            "In component \"VEVENT\" at index 0, in component property \"X-URI\" at index 22: Property is declared to have a URI value but the value is not a URI",
        );
2
    }
    #[test]
2
    fn iana_property_value_type_checks() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
TIME-INVALID-HOUR;VALUE=TIME:250000\r\n\
2
TIME-INVALID-MINUTE;VALUE=TIME:007000\r\n\
2
TIME-INVALID-SECOND;VALUE=TIME:000070\r\n\
2
TIME-VALID;VALUE=TIME:235960\r\n\
2
UTC-OFFSET-NEGATIVE-ZERO;VALUE=UTC-OFFSET:-000000\r\n\
2
UTC-OFFSET-NEGATIVE-NON-ZERO;VALUE=UTC-OFFSET:-000001\r\n\
2
UTC-OFFSET-INVALID-MINUTE;VALUE=UTC-OFFSET:+006000\r\n\
2
BASE-64;VALUE=BINARY;ENCODING=8BIT:nope\r\n\
2
BASE-64;VALUE=BINARY;ENCODING=BASE64:##\r\n\
2
BOOLEAN;VALUE=BOOLEAN:wendy\r\n\
2
CAL-ADDRESS-NOT-URL;VALUE=CAL-ADDRESS:test\r\n\
2
CAL-ADDRESS-NOT-MAILTO;VALUE=CAL-ADDRESS:mailto:hello@test.net\r\n\
2
DATE;VALUE=DATE:19900101T000120\r\n\
2
DATE-TIME;VALUE=DATE-TIME:19900101T000000P\r\n\
2
OTHER-DURATION;VALUE=DURATION:3W\r\n\
2
FLOAT;VALUE=FLOAT:3.14.15\r\n\
2
INTEGER;VALUE=INTEGER:3.14\r\n\
2
PERIOD;VALUE=PERIOD:19900101T000000Z/19900101T000000W\r\n\
2
RECUR;VALUE=RECUR:19900101T000000Z\r\n\
2
TEXT;VALUE=TEXT:\\p\r\n\
2
OTHER-URI;VALUE=URI:hello\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"TIME-INVALID-HOUR\" at index 2: Found an invalid time at index 0 - Hour must be between 0 and 23",
            "In component \"VEVENT\" at index 0, in component property \"TIME-INVALID-MINUTE\" at index 3: Found an invalid time at index 0 - Minute must be between 0 and 59",
            "In component \"VEVENT\" at index 0, in component property \"TIME-INVALID-SECOND\" at index 4: Found an invalid time at index 0 - Second must be between 0 and 60",
            "In component \"VEVENT\" at index 0, in component property \"UTC-OFFSET-NEGATIVE-ZERO\" at index 6: Found an invalid UTC offset - UTC offset must have a non-zero value if it is negative",
            "In component \"VEVENT\" at index 0, in component property \"UTC-OFFSET-INVALID-MINUTE\" at index 8: Found an invalid UTC offset - Minutes must be between 0 and 59",
            "In component \"VEVENT\" at index 0, in component property \"BASE-64\" at index 9: Property is declared to have a binary value but the encoding is set to 8BIT, instead of BASE64",
            "In component \"VEVENT\" at index 0, in component property \"BASE-64\" at index 10: Property is declared to have a binary value but the value is not base64",
            "In component \"VEVENT\" at index 0, in component property \"BOOLEAN\" at index 11: Property is declared to have a boolean value but the value is not a boolean",
            "In component \"VEVENT\" at index 0, in component property \"CAL-ADDRESS-NOT-URL\" at index 12: Property is declared to have a calendar address value but the value is not a mailto: URI",
            "In component \"VEVENT\" at index 0, in component property \"DATE\" at index 14: Property is declared to have a date value but the value is not a date",
            "In component \"VEVENT\" at index 0, in component property \"DATE-TIME\" at index 15: Property is declared to have a date-time value but the value is not a date-time",
            "In component \"VEVENT\" at index 0, in component property \"OTHER-DURATION\" at index 16: Property is declared to have a duration value but the value is not a duration",
            "In component \"VEVENT\" at index 0, in component property \"FLOAT\" at index 17: Property is declared to have a float value but the value is not a float",
            "In component \"VEVENT\" at index 0, in component property \"INTEGER\" at index 18: Property is declared to have an integer value but the value is not an integer",
            "In component \"VEVENT\" at index 0, in component property \"PERIOD\" at index 19: Property is declared to have a period value but the value is not a period",
            "In component \"VEVENT\" at index 0, in component property \"RECUR\" at index 20: Property is declared to have a recurrence value but the value is not a recurrence",
            "In component \"VEVENT\" at index 0, in component property \"TEXT\" at index 21: Property is declared to have a text value but the value is not a text",
            "In component \"VEVENT\" at index 0, in component property \"OTHER-URI\" at index 22: Property is declared to have a URI value but the value is not a URI",
        );
2
    }
    #[test]
2
    fn recur_invalid_occurrence() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
RRULE:FREQ=MONTHLY;COUNT=5;BYDAY=1SU\r\n\
2
RRULE:COUNT=5\r\n\
2
RRULE:COUNT=5;FREQ=MONTHLY;BYDAY=1SU\r\n\
2
RRULE:FREQ=MONTHLY;FREQ=WEEKLY;BYDAY=1SU\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101T000000Z;UNTIL=19900101T000001Z\r\n\
2
RRULE:FREQ=WEEKLY;COUNT=3;COUNT=5\r\n\
2
RRULE:FREQ=WEEKLY;INTERVAL=2;INTERVAL=2\r\n\
2
RRULE:FREQ=WEEKLY;BYSECOND=1;BYSECOND=1\r\n\
2
RRULE:FREQ=WEEKLY;BYMINUTE=1;BYMINUTE=1\r\n\
2
RRULE:FREQ=WEEKLY;BYHOUR=1;BYHOUR=1\r\n\
2
RRULE:FREQ=MONTHLY;BYDAY=1SU;BYDAY=1SU\r\n\
2
RRULE:FREQ=YEARLY;BYMONTHDAY=1;BYMONTHDAY=1\r\n\
2
RRULE:FREQ=YEARLY;BYYEARDAY=1;BYYEARDAY=1\r\n\
2
RRULE:FREQ=YEARLY;BYWEEKNO=1;BYWEEKNO=1\r\n\
2
RRULE:FREQ=WEEKLY;BYMONTH=1;BYMONTH=1\r\n\
2
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU;WKST=SU;WKST=SU\r\n\
2
RRULE:FREQ=YEARLY;BYDAY=1SU;BYSETPOS=1;BYSETPOS=1\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 4: No frequency part found in recurrence rule, but it is required. This prevents the rest of the rule being checked",
             "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 5: Recurrence rule must start with a frequency",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 6: Repeated FREQ part at index 1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 7: Repeated UNTIL part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 8: Repeated COUNT part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 9: Repeated INTERVAL part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 10: Repeated BYSECOND part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 11: Repeated BYMINUTE part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 12: Repeated BYHOUR part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 13: Repeated BYDAY part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 14: Repeated BYMONTHDAY part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 15: Repeated BYYEARDAY part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 16: Repeated BYWEEKNO part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 17: Repeated BYMONTH part at index 2",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 18: Repeated WKST part at index 4",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 19: Repeated BYSETPOS part at index 3",
        );
2
    }
    #[test]
2
    fn recur_invalid_time_range() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
RRULE:FREQ=WEEKLY;BYSECOND=74\r\n\
2
RRULE:FREQ=WEEKLY;BYMINUTE=98\r\n\
2
RRULE:FREQ=WEEKLY;BYHOUR=25\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 3: Invalid BYSECOND part at index 1, seconds must be between 0 and 60",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 4: Invalid BYMINUTE part at index 1, minutes must be between 0 and 59",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 5: Invalid BYHOUR part at index 1, hours must be between 0 and 23",
        );
2
    }
    #[test]
2
    fn recur_mismatched_date_time_start_type() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:1\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101T000000Z\r\n\
2
END:VEVENT\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:1\r\n\
2
DTSTART;VALUE=DATE:19900101\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101T000000Z\r\n\
2
END:VEVENT\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:2\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101\r\n\
2
END:VEVENT\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:3\r\n\
2
DTSTART:19900101T000000Z\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101T000000\r\n\
2
END:VEVENT\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:4\r\n\
2
DTSTART;TZID=/America/New_York:19900101T000000\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101T000000\r\n\
2
END:VEVENT\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:4\r\n\
2
DTSTART:19900101T000000\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101T000000Z\r\n\
2
END:VEVENT\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:4\r\n\
2
DTSTART;VALUE=DATE:19900101\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101\r\n\
2
END:VEVENT\r\n\
2
BEGIN:VTIMEZONE\r\n\
2
TZID:test\r\n\
2
BEGIN:STANDARD\r\n\
2
DTSTART:19900101T000000\r\n\
2
RRULE:FREQ=WEEKLY;UNTIL=19900101T001000\r\n\
2
TZOFFSETTO:+0000\r\n\
2
TZOFFSETFROM:+0000\r\n\
2
END:STANDARD\r\n\
2
END:VTIMEZONE\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 2: Recurrence rule must have a DTSTART property associated with it",
            "In component \"VEVENT\" at index 0: DTSTART is required",
            "In component \"VEVENT\" at index 1, in component property \"RRULE\" at index 3: UNTIL part at index 1 is a date-time, but the associated DTSTART property is a date",
            "In component \"VEVENT\" at index 2, in component property \"RRULE\" at index 3: UNTIL part at index 1 is a date, but the associated DTSTART property is a date-time",
            "In component \"VEVENT\" at index 3, in component property \"RRULE\" at index 3: UNTIL part at index 1 must be a UTC time if the associated DTSTART property is a UTC time or a local time with a timezone",
            "In component \"VEVENT\" at index 4, in component property \"RRULE\" at index 3: UNTIL part at index 1 must be a UTC time if the associated DTSTART property is a UTC time or a local time with a timezone",
            "In component \"VEVENT\" at index 5, in component property \"RRULE\" at index 3: UNTIL part at index 1 must be a local time if the associated DTSTART property is a local time",
            "In component \"VTIMEZONE\" at index 7, in nested component \"STANDARD\" at index 0, in nested component property \"RRULE\" at index 1: UNTIL part at index 1 must be a UTC time here",
        );
2
    }
    #[test]
2
    fn recur_invalid_freq_with_date_dt_start() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:1\r\n\
2
DTSTART;VALUE=DATE:19900101\r\n\
2
RRULE:FREQ=SECONDLY;BYSECOND=1,5\r\n\
2
RRULE:FREQ=MINUTELY;BYMINUTE=1,5\r\n\
2
RRULE:FREQ=HOURLY;BYHOUR=1,5\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 3: BYSECOND part at index 1 is not valid when the associated DTSTART property has a DATE value type",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 4: BYMINUTE part at index 1 is not valid when the associated DTSTART property has a DATE value type",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 5: BYHOUR part at index 1 is not valid when the associated DTSTART property has a DATE value type",
        );
2
    }
    #[test]
2
    fn recur_value_violations() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:1\r\n\
2
DTSTART:19900101T001000\r\n\
2
RRULE:FREQ=WEEKLY;BYSECOND=1,65,5\r\n\
2
RRULE:FREQ=WEEKLY;BYSECOND=1,0,60,5\r\n\
2
RRULE:FREQ=WEEKLY;BYMINUTE=1,60,5\r\n\
2
RRULE:FREQ=WEEKLY;BYMINUTE=1,0,59,5\r\n\
2
RRULE:FREQ=WEEKLY;BYHOUR=1,24,5\r\n\
2
RRULE:FREQ=WEEKLY;BYHOUR=1,0,23,5\r\n\
2
RRULE:FREQ=MONTHLY;BYMONTHDAY=1,32,5\r\n\
2
RRULE:FREQ=MONTHLY;BYMONTHDAY=1,-32,5\r\n\
2
RRULE:FREQ=MONTHLY;BYMONTHDAY=-31,-1,1,31\r\n\
2
RRULE:FREQ=YEARLY;BYYEARDAY=5,367,1\r\n\
2
RRULE:FREQ=YEARLY;BYYEARDAY=-5,-367,-1\r\n\
2
RRULE:FREQ=YEARLY;BYYEARDAY=-366,-1,1,366\r\n\
2
RRULE:FREQ=YEARLY;BYWEEKNO=1,54,5\r\n\
2
RRULE:FREQ=YEARLY;BYWEEKNO=-1,-54,-5\r\n\
2
RRULE:FREQ=YEARLY;BYWEEKNO=-53,-1,1,53\r\n\
2
RRULE:FREQ=WEEKLY;BYSETPOS=1,367,5;BYDAY=MO\r\n\
2
RRULE:FREQ=WEEKLY;BYSETPOS=1,-367,5;BYDAY=MO\r\n\
2
RRULE:FREQ=WEEKLY;BYSETPOS=1,0,5;BYDAY=MO\r\n\
2
RRULE:FREQ=WEEKLY;BYSETPOS=-366,-1,1,366;BYDAY=MO\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 3: Invalid BYSECOND part at index 1, seconds must be between 0 and 60",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 5: Invalid BYMINUTE part at index 1, minutes must be between 0 and 59",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 7: Invalid BYHOUR part at index 1, hours must be between 0 and 23",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 9: Invalid BYMONTHDAY part at index 1, days must be between 1 and 31, or -31 and -1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 10: Invalid BYMONTHDAY part at index 1, days must be between 1 and 31, or -31 and -1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 12: Invalid BYYEARDAY part at index 1, days must be between 1 and 366, or -366 and -1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 13: Invalid BYYEARDAY part at index 1, days must be between 1 and 366, or -366 and -1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 15: Invalid BYWEEKNO part at index 1, weeks must be between 1 and 53, or -53 and -1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 16: Invalid BYWEEKNO part at index 1, weeks must be between 1 and 53, or -53 and -1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 18: Invalid BYSETPOS part at index 1, set positions must be between 1 and 366, or -366 and -1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 19: Invalid BYSETPOS part at index 1, set positions must be between 1 and 366, or -366 and -1",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 20: Invalid BYSETPOS part at index 1, set positions must be between 1 and 366, or -366 and -1",
        );
2
    }
    #[test]
2
    fn recur_logical_violations() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:1\r\n\
2
DTSTART:19900101T001000\r\n\
2
RRULE:FREQ=WEEKLY;BYDAY=1MO\r\n\
2
RRULE:FREQ=DAILY;BYDAY=1MO\r\n\
2
RRULE:FREQ=HOURLY;BYDAY=1MO\r\n\
2
RRULE:FREQ=MINUTELY;BYDAY=1MO\r\n\
2
RRULE:FREQ=SECONDLY;BYDAY=1MO\r\n\
2
RRULE:FREQ=MONTHLY;BYDAY=1MO\r\n\
2
RRULE:FREQ=YEARLY;BYDAY=1MO\r\n\
2
RRULE:FREQ=YEARLY;BYWEEKNO=3;BYDAY=1MO\r\n\
2
RRULE:FREQ=WEEKLY;BYMONTHDAY=1\r\n\
2
RRULE:FREQ=MONTHLY;BYYEARDAY=1\r\n\
2
RRULE:FREQ=WEEKLY;BYYEARDAY=1\r\n\
2
RRULE:FREQ=DAILY;BYYEARDAY=1\r\n\
2
RRULE:FREQ=MONTHLY;BYWEEKNO=1\r\n\
2
RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=TU\r\n\
2
RRULE:FREQ=WEEKLY;BYDAY=SU;WKST=TU\r\n\
2
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU;WKST=TU\r\n\
2
RRULE:FREQ=YEARLY;WKST=TU\r\n\
2
RRULE:FREQ=YEARLY;BYWEEKNO=5;WKST=TU\r\n\
2
RRULE:FREQ=DAILY;WKST=TU\r\n\
2
RRULE:FREQ=WEEKLY;BYSETPOS=-366,-1,1,366\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(
            errors,
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 3: BYDAY part at index 1 has a day with an offset, but the frequency is not MONTHLY or YEARLY",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 4: BYDAY part at index 1 has a day with an offset, but the frequency is not MONTHLY or YEARLY",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 5: BYDAY part at index 1 has a day with an offset, but the frequency is not MONTHLY or YEARLY",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 6: BYDAY part at index 1 has a day with an offset, but the frequency is not MONTHLY or YEARLY",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 7: BYDAY part at index 1 has a day with an offset, but the frequency is not MONTHLY or YEARLY",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 10: BYDAY part at index 2 has a day with an offset, but the frequency is YEARLY and a BYWEEKNO part is specified",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 11: BYMONTHDAY part at index 1 is not valid for a WEEKLY frequency",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 12: BYYEARDAY part at index 1 is not valid for a DAILY, WEEKLY or MONTHLY frequency",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 13: BYYEARDAY part at index 1 is not valid for a DAILY, WEEKLY or MONTHLY frequency",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 14: BYYEARDAY part at index 1 is not valid for a DAILY, WEEKLY or MONTHLY frequency",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 15: BYWEEKNO part at index 1 is only valid for a YEARLY frequency",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 16: WKST part at index 2 is redundant",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 17: WKST part at index 2 is redundant",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 19: WKST part at index 1 is redundant",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 21: WKST part at index 1 is redundant",
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 22: BYSETPOS part at index 1 is not valid without another BYxxx rule part",
        );
2
    }
    #[test]
2
    fn x_prop_declares_boolean_but_is_not_boolean() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
X-HELLO;VALUE=BOOLEAN:123\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
2

            
2
        assert_eq!(errors.len(), 1);
2
        assert_eq!("In component \"VEVENT\" at index 0, in component property \"X-HELLO\" at index 2: Property is declared to have a boolean value but the value is not a boolean", errors.first().unwrap().to_string());
2
    }
    #[test]
2
    fn iana_prop_declares_boolean_but_is_not_boolean() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
HELLO;VALUE=BOOLEAN:123\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
2

            
2
        assert_eq!(errors.len(), 1);
2
        assert_eq!("In component \"VEVENT\" at index 0, in component property \"HELLO\" at index 2: Property is declared to have a boolean value but the value is not a boolean", errors.first().unwrap().to_string());
2
    }
    #[test]
2
    fn x_prop_declares_date() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
X-HELLO;VALUE=DATE:19900101\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
2
        assert_eq!(errors.len(), 0);
2
    }
    #[test]
2
    fn x_prop_declares_date_and_is_multi() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
X-HELLO;VALUE=DATE:19900101,19920101\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
2
        assert_eq!(errors.len(), 0);
2
    }
    #[test]
2
    fn x_prop_declares_date_and_is_not() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
X-HELLO;VALUE=DATE:TRUE\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
2

            
2
        assert_eq!(errors.len(), 1);
2
        assert_eq!("In component \"VEVENT\" at index 0, in component property \"X-HELLO\" at index 2: Property is declared to have a date value but the value is not a date", errors.first().unwrap().to_string());
2
    }
    #[test]
2
    fn alarm_with_no_action() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
BEGIN:VALARM\r\n\
2
TRIGGER;VALUE=DURATION:P3W\r\n\
2
END:VALARM\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
2

            
2
        assert_eq!(errors.len(), 1);
2
        assert_eq!("In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0: Required exactly one ACTION property but found 0", errors.first().unwrap().to_string());
2
    }
    #[test]
2
    fn audio_alarm_with_duplicate_attach() {
2
        let content = "BEGIN:VCALENDAR\r\n\
2
PRODID:test\r\n\
2
VERSION:2.0\r\n\
2
METHOD:send\r\n\
2
BEGIN:VEVENT\r\n\
2
DTSTAMP:19900101T000000Z\r\n\
2
UID:123\r\n\
2
BEGIN:VALARM\r\n\
2
ACTION:AUDIO\r\n\
2
TRIGGER:P3W\r\n\
2
ATTACH:ftp://example.com/pub/sounds/bell-01.aud\r\n\
2
ATTACH:ftp://example.com/pub/sounds/bell-01.aud\r\n\
2
END:VALARM\r\n\
2
END:VEVENT\r\n\
2
END:VCALENDAR\r\n";
2

            
2
        let errors = validate_content(content);
        assert_errors!(&errors, "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0, in nested component property \"ATTACH\" at index 3: ATTACH must only appear once");
2
    }
172
    fn validate_content(content: &str) -> Vec<ICalendarError> {
172
        let (rem, object) = crate::parser::ical_object::<Error>(content.as_bytes()).unwrap();
172
        check_rem(rem, 0);
172

            
172
        validate_model(&object.to_model().unwrap()).unwrap()
172
    }
}