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
use crate::error::AetoliaResult;
20
use crate::prelude::AetoliaError;
21
pub use error::*;
22

            
23
188
pub fn validate_model(ical_object: &ICalObject) -> AetoliaResult<Vec<ICalendarError>> {
24
188
    let mut errors = Vec::new();
25
188

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

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

            
42
188
    let mut calendar_info = CalendarInfo::new(time_zone_ids);
43
188

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

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

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

            
82
144
        Ok(())
83
144
    };
84

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

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

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

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

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

            
251
188
    Ok(errors)
252
188
}
253

            
254
16
fn validate_time(time: &crate::parser::types::Time) -> AetoliaResult<()> {
255
16
    if time.hour > 23 {
256
4
        return Err(AetoliaError::other("Hour must be between 0 and 23"));
257
12
    }
258
12

            
259
12
    if time.minute > 59 {
260
4
        return Err(AetoliaError::other("Minute must be between 0 and 59"));
261
8
    }
262
8

            
263
8
    if time.second > 60 {
264
4
        return Err(AetoliaError::other("Second must be between 0 and 60"));
265
4
    }
266
4

            
267
4
    Ok(())
268
16
}
269

            
270
12
fn validate_utc_offset(offset: &crate::parser::types::UtcOffset) -> AetoliaResult<()> {
271
12
    if offset.sign < 0
272
8
        && (offset.hours == 0
273
8
            && offset.minutes == 0
274
8
            && (offset.seconds.is_none() || offset.seconds == Some(0)))
275
    {
276
4
        return Err(AetoliaError::other(
277
4
            "UTC offset must have a non-zero value if it is negative",
278
4
        ));
279
8
    }
280
8

            
281
8
    if offset.minutes > 59 {
282
4
        return Err(AetoliaError::other("Minutes must be between 0 and 59"));
283
4
    }
284
4

            
285
4
    Ok(())
286
12
}
287

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

            
296
impl CalendarInfo {
297
188
    fn new(time_zone_ids: HashSet<String>) -> Self {
298
188
        CalendarInfo {
299
188
            time_zone_ids,
300
188
            method: None,
301
188
        }
302
188
    }
303
}
304

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

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

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

            
352
124
    fn utc(mut self, is_utc: bool) -> Self {
353
124
        self.value_is_utc = Some(is_utc);
354
124
        self
355
124
    }
356
}
357

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

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

            
382
#[derive(Debug, Clone, PartialEq)]
383
enum OccurrenceExpectation {
384
    Once,
385
    OnceOrMany,
386
    OptionalOnce,
387
    OptionalMany,
388
    Never,
389
}
390

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

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

            
420
2638
            None
421
3992
        })
422
2262
}
423

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

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

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

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

            
526
#[cfg(test)]
527
mod tests {
528
    use super::*;
529
    use crate::convert::ToModel;
530

            
531
    use crate::parser::Error;
532
    use crate::test_utils::check_rem;
533

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

            
548
    macro_rules! assert_errors {
549
        ($errors:expr, $msg:literal $(,$others:literal)* $(,)?) => {
550
            assert_errors!($errors, &[$msg, $($others,)*]);
551
        };
552

            
553
        ($errors:expr, $messages:expr) => {
554
480
            similar_asserts::assert_eq!($errors.iter().map(|e| e.to_string()).collect::<Vec<_>>().as_slice(), $messages);
555
        };
556
    }
557

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

            
590
2
        let errors = validate_content(content);
591
2

            
592
2
        assert_no_errors!(&errors);
593
2
    }
594

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

            
604
2
        let errors = validate_model(&object).unwrap();
605
2

            
606
2
        assert_errors!(
607
2
            errors,
608
2
            "No components found in calendar object, required at least one"
609
2
        );
610
2
    }
611

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

            
623
2
        let errors = validate_model(&object).unwrap();
624
2

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

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

            
638
2
        let errors = validate_content(content);
639
2

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

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

            
656
2
        let errors = validate_content(content);
657
2

            
658
2
        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");
659
2
    }
660

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

            
671
2
        let errors = validate_content(content);
672
2

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

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

            
689
2
        let errors = validate_content(content);
690
2

            
691
2
        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");
692
2
    }
693

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

            
704
2
        let errors = validate_content(content);
705
2

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

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

            
722
2
        let errors = validate_content(content);
723
2

            
724
2
        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");
725
2
    }
726

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

            
737
2
        let errors = validate_content(content);
738
2

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

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

            
755
2
        let errors = validate_content(content);
756
2

            
757
2
        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");
758
2
    }
759

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

            
770
2
        let errors = validate_content(content);
771
2

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

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

            
788
2
        let errors = validate_content(content);
789
2

            
790
2
        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");
791
2
    }
792

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

            
806
2
        let errors = validate_content(content);
807
2

            
808
2
        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");
809
2
    }
810

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

            
824
2
        let errors = validate_content(content);
825
2

            
826
2
        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");
827
2
    }
828

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

            
839
2
        let errors = validate_content(content);
840
2

            
841
2
        assert_errors!(
842
2
            errors,
843
2
            "In calendar property \"VERSION\" at index 1: FMTTYPE is not allowed"
844
2
        );
845
2
    }
846

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

            
860
2
        let errors = validate_content(content);
861
2

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

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

            
875
2
        let errors = validate_content(content);
876
2

            
877
2
        assert_errors!(
878
2
            errors,
879
2
            "In calendar property \"VERSION\" at index 1: FBTYPE is not allowed"
880
2
        );
881
2
    }
882

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

            
896
2
        let errors = validate_content(content);
897
2

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

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

            
911
2
        let errors = validate_content(content);
912
2

            
913
2
        assert_errors!(
914
2
            errors,
915
2
            "In calendar property \"VERSION\" at index 1: LANGUAGE is not allowed"
916
2
        );
917
2
    }
918

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

            
932
2
        let errors = validate_content(content);
933
2

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

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

            
947
2
        let errors = validate_content(content);
948
2

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

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

            
968
2
        let errors = validate_content(content);
969
2

            
970
2
        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");
971
2
    }
972

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

            
986
2
        let errors = validate_content(content);
987
2

            
988
2
        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");
989
2
    }
990

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

            
2
        let errors = validate_content(content);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In calendar property \"VERSION\" at index 1: RANGE is not allowed"
2
        );
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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In calendar property \"VERSION\" at index 1: TZID is not allowed"
2
        );
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);
2

            
2
        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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In calendar property \"VERSION\" at index 1: VALUE is not allowed"
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
            "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 2: VALUE is not allowed"
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0: DTSTAMP is required",
2
            "In component \"VEVENT\" at index 0: UID is required",
2
            "In component \"VEVENT\" at index 0: DTSTART is required",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0, in component property \"DTSTART\" at index 3: DTSTART must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"CLASS\" at index 5: CLASS must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"CREATED\" at index 7: CREATED must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"DESCRIPTION\" at index 9: DESCRIPTION must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"GEO\" at index 11: GEO must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"LAST-MODIFIED\" at index 13: LAST-MODIFIED must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"LOCATION\" at index 15: LOCATION must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"ORGANIZER\" at index 17: ORGANIZER must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"PRIORITY\" at index 19: PRIORITY must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"SEQUENCE\" at index 21: SEQUENCE must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"STATUS\" at index 23: STATUS must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"SUMMARY\" at index 25: SUMMARY must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"TRANSP\" at index 27: TRANSP must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"URL\" at index 29: URL must only appear once",
2
            "In component \"VEVENT\" at index 0, in component property \"RECURRENCE-ID\" at index 31: RECURRENCE-ID must only appear once",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0, in component property \"DTEND\" at index 3: DTEND must only appear once",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0, in component property \"DURATION\" at index 3: DURATION must only appear once",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0: Both DTEND and DURATION properties are present, only one is allowed",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTODO\" at index 0: DTSTAMP is required",
2
            "In component \"VTODO\" at index 0: UID is required",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTODO\" at index 0, in component property \"CLASS\" at index 3: CLASS must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"CREATED\" at index 7: CREATED must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"DESCRIPTION\" at index 9: DESCRIPTION must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"DTSTART\" at index 11: DTSTART must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"GEO\" at index 13: GEO must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"LAST-MODIFIED\" at index 15: LAST-MODIFIED must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"LOCATION\" at index 17: LOCATION must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"ORGANIZER\" at index 19: ORGANIZER must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"PERCENT-COMPLETE\" at index 21: PERCENT-COMPLETE must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"PRIORITY\" at index 23: PRIORITY must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"RECURRENCE-ID\" at index 25: RECURRENCE-ID must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"SEQUENCE\" at index 27: SEQUENCE must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"STATUS\" at index 29: STATUS must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"SUMMARY\" at index 31: SUMMARY must only appear once",
2
            "In component \"VTODO\" at index 0, in component property \"URL\" at index 33: URL must only appear once",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTODO\" at index 0, in component property \"DUE\" at index 3: DUE must only appear once",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTODO\" at index 0, in component property \"DURATION\" at index 4: DURATION must only appear once",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTODO\" at index 0: DURATION property is present but no DTSTART property is present",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTODO\" at index 0: Both DUE and DURATION properties are present, only one is allowed",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VJOURNAL\" at index 0: DTSTAMP is required",
2
            "In component \"VJOURNAL\" at index 0: UID is required",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VJOURNAL\" at index 0, in component property \"CLASS\" at index 3: CLASS must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"CREATED\" at index 5: CREATED must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"DTSTART\" at index 7: DTSTART must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"LAST-MODIFIED\" at index 9: LAST-MODIFIED must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"ORGANIZER\" at index 11: ORGANIZER must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"RECURRENCE-ID\" at index 13: RECURRENCE-ID must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"SEQUENCE\" at index 15: SEQUENCE must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"STATUS\" at index 17: STATUS must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"SUMMARY\" at index 19: SUMMARY must only appear once",
2
            "In component \"VJOURNAL\" at index 0, in component property \"URL\" at index 21: URL must only appear once"
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VFREEBUSY\" at index 0: DTSTAMP is required",
2
            "In component \"VFREEBUSY\" at index 0: UID is required",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VFREEBUSY\" at index 0, in component property \"CONTACT\" at index 3: CONTACT must only appear once",
2
            "In component \"VFREEBUSY\" at index 0, in component property \"DTSTART\" at index 5: DTSTART must only appear once",
2
            "In component \"VFREEBUSY\" at index 0, in component property \"DTEND\" at index 7: DTEND must only appear once",
2
            "In component \"VFREEBUSY\" at index 0, in component property \"ORGANIZER\" at index 9: ORGANIZER must only appear once",
2
            "In component \"VFREEBUSY\" at index 0, in component property \"URL\" at index 11: URL must only appear once",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTIMEZONE\" at index 0: TZID is required",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTIMEZONE\" at index 0: No standard or daylight components found in time zone, required at least one",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTIMEZONE\" at index 0, in component property \"LAST-MODIFIED\" at index 2: LAST-MODIFIED must only appear once",
2
            "In component \"VTIMEZONE\" at index 0, in component property \"TZURL\" at index 4: TZURL must only appear once",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VTIMEZONE\" at index 0, in nested component \"STANDARD\" at index 0: DTSTART is required",
2
            "In component \"VTIMEZONE\" at index 0, in nested component \"STANDARD\" at index 0: TZOFFSETTO is required",
2
            "In component \"VTIMEZONE\" at index 0, in nested component \"STANDARD\" at index 0: TZOFFSETFROM is required",
2
            "In component \"VTIMEZONE\" at index 0, in nested component \"DAYLIGHT\" at index 1: DTSTART is required",
2
            "In component \"VTIMEZONE\" at index 0, in nested component \"DAYLIGHT\" at index 1: TZOFFSETTO is required",
2
            "In component \"VTIMEZONE\" at index 0, in nested component \"DAYLIGHT\" at index 1: TZOFFSETFROM is required",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0: Required exactly one ACTION property but found 0",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0: TRIGGER is required",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 1: TRIGGER is required",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 1: DESCRIPTION is required",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2: TRIGGER is required",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2: DESCRIPTION is required",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2: SUMMARY is required",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 0: DURATION and REPEAT properties must be present together",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 1: DURATION and REPEAT properties must be present together",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 2: DURATION and REPEAT properties must be present together",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 3: DURATION and REPEAT properties must be present together",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 4: DURATION and REPEAT properties must be present together",
2
            "In component \"VEVENT\" at index 0, in nested component \"VALARM\" at index 5: DURATION and REPEAT properties must be present together",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0, in component property \"ATTACH\" at index 2: Redundant value specification which matches the default value",
2
            "In component \"VEVENT\" at index 0, in component property \"DTEND\" at index 3: Redundant value specification which matches the default value",
2
            "In component \"VEVENT\" at index 0, in component property \"DTSTART\" at index 4: Redundant value specification which matches the default value",
2
            "In component \"VEVENT\" at index 0, in component property \"EXDATE\" at index 5: Redundant value specification which matches the default value",
2
            "In component \"VEVENT\" at index 0, in component property \"RDATE\" at index 6: Redundant value specification which matches the default value",
2
            "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",
2
            "In component \"VTODO\" at index 1, in component property \"DUE\" at index 2: Redundant value specification which matches the default value",
2
        );
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);
2

            
2
        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);
2

            
2
        // Gets picked up as IANA
2
        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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
             "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 5: Recurrence rule must start with a frequency",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 6: Repeated FREQ part at index 1",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 7: Repeated UNTIL part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 8: Repeated COUNT part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 9: Repeated INTERVAL part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 10: Repeated BYSECOND part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 11: Repeated BYMINUTE part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 12: Repeated BYHOUR part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 13: Repeated BYDAY part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 14: Repeated BYMONTHDAY part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 15: Repeated BYYEARDAY part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 16: Repeated BYWEEKNO part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 17: Repeated BYMONTH part at index 2",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 18: Repeated WKST part at index 4",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 19: Repeated BYSETPOS part at index 3",
2
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
            "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",
2
            "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
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 2: Recurrence rule must have a DTSTART property associated with it",
2
            "In component \"VEVENT\" at index 0: DTSTART is required",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
            "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",
2
            "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
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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
        );
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);
2

            
2
        assert_errors!(
2
            errors,
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "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",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 16: WKST part at index 2 is redundant",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 17: WKST part at index 2 is redundant",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 19: WKST part at index 1 is redundant",
2
            "In component \"VEVENT\" at index 0, in component property \"RRULE\" at index 21: WKST part at index 1 is redundant",
2
            "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
        );
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);
2

            
2
        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
    }
}