Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / test / calendar / vcal.spec.ts
blob008b38fdc69a263ca4c8604c9b6d8f1a3e627f8a
1 import {
2     fromTriggerString,
3     getMillisecondsFromTriggerString,
4     getVeventWithoutErrors,
5     parse,
6     parseVcalendarWithRecoveryAndMaybeErrors,
7     serialize,
8 } from '../../lib/calendar/vcal';
9 import { DAY, HOUR, MINUTE, SECOND, WEEK } from '../../lib/constants';
10 import type {
11     VcalErrorComponent,
12     VcalValarmComponent,
13     VcalVcalendarWithMaybeErrors,
14     VcalVeventComponent,
15     VcalVeventComponentWithMaybeErrors,
16 } from '../../lib/interfaces/calendar';
18 const vevent = `BEGIN:VEVENT
19 DTSTAMP:20190719T130854Z
20 UID:7E018059-2165-4170-B32F-6936E88E61E5
21 DTSTART;TZID=America/New_York:20190719T120000
22 DTEND;TZID=Europe/Zurich:20190719T130000
23 SEQUENCE:0
24 CATEGORIES:ANNIVERSARY,PERSONAL,SPECIAL OCCASION
25 SUMMARY:Our Blissful Anniversary
26 END:VEVENT`;
28 const allDayVevent = `BEGIN:VEVENT
29 UID:9E018059-2165-4170-B32F-6936E88E61E5
30 DTSTART;VALUE=DATE:20190812
31 DTEND;VALUE=DATE:20190813
32 SUMMARY:text
33 END:VEVENT`;
35 const veventWithRecurrenceId = `BEGIN:VEVENT
36 UID:9E018059-2165-4170-B32F-6936E88E61E5
37 RECURRENCE-ID;TZID=Europe/Zurich:20200311T100000
38 DTSTART;TZID=Europe/Zurich:20200311T100000
39 DTEND;TZID=Europe/Zurich:20200312T100000
40 SUMMARY:text
41 END:VEVENT`;
43 const veventWithAttendees = `BEGIN:VEVENT
44 UID:7E018059-2165-4170-B32F-6936E88E61E5
45 DTSTART;VALUE=DATE:20190812
46 DTEND;VALUE=DATE:20190813
47 SUMMARY:text
48 ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP=TRUE;X-PM-TOKEN=123;CN
49  =james@bond.co.uk:mailto:james@bond.co.uk
50 ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP=TRUE;X-PM-TOKEN=123;CN
51  =Dr No.:mailto:dr.no@mi6.co.uk
52 ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=NON-PARTICIPANT;RSVP=FALSE;CN=Miss Moneypen
53  ny:mailto:moneypenny@mi6.co.uk
54 END:VEVENT`;
56 const valarm = `BEGIN:VALARM
57 TRIGGER:-PT15H
58 ACTION:DISPLAY
59 DESCRIPTION:asd
60 END:VALARM`;
62 const valarmInVevent = `BEGIN:VEVENT
63 UID:7E018059-2165-4170-B32F-6936E88E61E5
64 DTSTAMP:20190719T110000Z
65 DTSTART:20190719T120000Z
66 DTEND:20190719T130000Z
67 BEGIN:VALARM
68 ACTION:DISPLAY
69 TRIGGER:-PT15H
70 END:VALARM
71 END:VEVENT`;
73 const veventRruleDaily1 = `BEGIN:VEVENT
74 UID:7E018059-2165-4170-B32F-6936E88E61E5
75 DTSTART:20190719T120000Z
76 DTEND:20190719T130000Z
77 RRULE:FREQ=DAILY;COUNT=10;INTERVAL=3
78 END:VEVENT`;
80 const veventRruleDaily2 = `BEGIN:VEVENT
81 UID:7E018059-2165-4170-B32F-6936E88E61E5
82 DTSTART;VALUE=DATE:20190719
83 DTEND;VALUE=DATE:20190719
84 RRULE:FREQ=DAILY;UNTIL=20200130
85 END:VEVENT`;
87 const veventRruleDaily3 = `BEGIN:VEVENT
88 UID:7E018059-2165-4170-B32F-6936E88E61E5
89 DTSTART:20190719T120000Z
90 DTEND:20190719T130000Z
91 RRULE:FREQ=DAILY;UNTIL=20200130T225959Z
92 END:VEVENT`;
94 const veventRruleDaily4 = `BEGIN:VEVENT
95 UID:7E018059-2165-4170-B32F-6936E88E61E5
96 DTSTART;TZID=America/New_York:20190719T120000
97 DTEND:20190719T130000Z
98 RRULE:FREQ=DAILY;UNTIL=20200130T225959Z
99 END:VEVENT`;
101 const veventsRruleDaily = [veventRruleDaily1, veventRruleDaily2, veventRruleDaily3, veventRruleDaily4];
103 const veventRruleWeekly1 = `BEGIN:VEVENT
104 UID:7E018059-2165-4170-B32F-6936E88E61E5
105 DTSTART:20190719T120000Z
106 DTEND:20190719T130000Z
107 RRULE:FREQ=WEEKLY;COUNT=10;INTERVAL=3;BYDAY=WE,TH
108 END:VEVENT`;
110 const veventRruleWeekly2 = `BEGIN:VEVENT
111 UID:7E018059-2165-4170-B32F-6936E88E61E5
112 DTSTART;VALUE=DATE:20190719
113 DTEND;VALUE=DATE:20190719
114 RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20200130
115 END:VEVENT`;
117 const veventRruleWeekly3 = `BEGIN:VEVENT
118 UID:7E018059-2165-4170-B32F-6936E88E61E5
119 DTSTART:20190719T120000Z
120 DTEND:20190719T130000Z
121 RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20200130T225959Z
122 END:VEVENT`;
124 const veventRruleWeekly4 = `BEGIN:VEVENT
125 UID:7E018059-2165-4170-B32F-6936E88E61E5
126 DTSTART;TZID=America/New_York:20190719T120000
127 DTEND:20190719T130000Z
128 RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20200130T225959Z
129 END:VEVENT`;
131 const veventsRruleWeekly = [veventRruleWeekly1, veventRruleWeekly2, veventRruleWeekly3, veventRruleWeekly4];
133 const veventRruleMonthly1 = `BEGIN:VEVENT
134 UID:7E018059-2165-4170-B32F-6936E88E61E5
135 DTSTART:20190719T120000Z
136 DTEND:20190719T130000Z
137 RRULE:FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=13;UNTIL=20200130T230000Z
138 END:VEVENT`;
140 const veventRruleMonthly2 = `BEGIN:VEVENT
141 UID:7E018059-2165-4170-B32F-6936E88E61E5
142 DTSTART:20190719T120000Z
143 DTEND:20190719T130000Z
144 RRULE:FREQ=MONTHLY;COUNT=4;BYSETPOS=2;BYDAY=TU
145 END:VEVENT`;
147 const veventRruleMonthly3 = `BEGIN:VEVENT
148 UID:7E018059-2165-4170-B32F-6936E88E61E5
149 DTSTART;VALUE=DATE:20190719
150 DTEND;VALUE=DATE:20190719
151 RRULE:FREQ=MONTHLY;BYSETPOS=-1;BYDAY=MO;UNTIL=20200130
152 END:VEVENT`;
154 const veventRruleMonthly4 = `BEGIN:VEVENT
155 UID:7E018059-2165-4170-B32F-6936E88E61E5
156 DTSTART;TZID=America/New_York:20190719T120000
157 DTEND:20190719T130000Z
158 RRULE:FREQ=MONTHLY;BYMONTHDAY=2;UNTIL=20200130T225959Z
159 END:VEVENT`;
161 const veventsRruleMonthly = [veventRruleMonthly1, veventRruleMonthly2, veventRruleMonthly3, veventRruleMonthly4];
163 const veventRruleYearly1 = `BEGIN:VEVENT
164 UID:7E018059-2165-4170-B32F-6936E88E61E5
165 DTSTART:20190719T120000Z
166 DTEND:20190719T130000Z
167 RRULE:FREQ=YEARLY;COUNT=4;BYMONTH=7;BYMONTHDAY=25
168 END:VEVENT`;
170 const veventRruleYearly2 = `BEGIN:VEVENT
171 UID:7E018059-2165-4170-B32F-6936E88E61E5
172 DTSTART;VALUE=DATE:20190719
173 DTEND;VALUE=DATE:20190719
174 RRULE:FREQ=YEARLY;INTERVAL=2;UNTIL=20200130
175 END:VEVENT`;
177 const veventRruleYearly3 = `BEGIN:VEVENT
178 UID:7E018059-2165-4170-B32F-6936E88E61E5
179 DTSTART:20190719T120000Z
180 DTEND:20190719T130000Z
181 RRULE:FREQ=YEARLY;INTERVAL=2;BYMONTH=7;BYMONTHDAY=25;UNTIL=20200130T225959Z
182 END:VEVENT`;
184 const veventRruleYearly4 = `BEGIN:VEVENT
185 UID:7E018059-2165-4170-B32F-6936E88E61E5
186 DTSTART;TZID=America/New_York:20190719T120000
187 DTEND:20190719T130000Z
188 RRULE:FREQ=YEARLY;UNTIL=20200130T225959Z
189 END:VEVENT`;
191 const veventsRruleYearly = [veventRruleYearly1, veventRruleYearly2, veventRruleYearly3, veventRruleYearly4];
193 const vfreebusy = `BEGIN:VFREEBUSY
194 UID:19970901T095957Z-76A912@example.com
195 ORGANIZER:mailto:jane_doe@example.com
196 ATTENDEE:mailto:john_public@example.com
197 DTSTAMP:19970901T100000Z
198 FREEBUSY:19971015T050000Z/PT8H30M,19971015T160000Z/PT5H30M,19971015T223000Z/PT6H30M
199 URL:http://example.com/pub/busy/jpublic-01.ifb
200 COMMENT:This iCalendar file contains busy time information for the next three months.
201 END:VFREEBUSY`;
203 const vfreebusy2 = `BEGIN:VFREEBUSY
204 UID:19970901T115957Z-76A912@example.com
205 DTSTAMP:19970901T120000Z
206 ORGANIZER:jsmith@example.com
207 DTSTART:19980313T141711Z
208 DTEND:19980410T141711Z
209 FREEBUSY:19980314T233000Z/19980315T003000Z
210 FREEBUSY:19980316T153000Z/19980316T163000Z
211 FREEBUSY:19980318T030000Z/19980318T040000Z
212 URL:http://www.example.com/calendar/busytime/jsmith.ifb
213 END:VFREEBUSY`;
215 const veventWithTrueBoolean = `BEGIN:VEVENT
216 X-PM-PROTON-REPLY;VALUE=BOOLEAN:TRUE
217 END:VEVENT`;
219 const veventWithFalseBoolean = `BEGIN:VEVENT
220 X-PM-PROTON-REPLY;VALUE=BOOLEAN:FALSE
221 END:VEVENT`;
223 const veventWithRandomBoolean = `BEGIN:VEVENT
224 X-PM-PROTON-REPLY;VALUE=BOOLEAN:GNEEEE
225 END:VEVENT`;
227 const veventWithInvalidVAlarm = `
228 BEGIN:VEVENT
229 DESCRIPTION;LANGUAGE=en-US:\n\n\n
230 UID:040000008200E00074C5B7101A82E00800000000B058B6A2A081D901000000000000000
231     0100000004A031FE80ACD7C418A7A1762749176F121
232 SUMMARY:Calendar test
233 DTSTART;TZID=Eastern Standard Time:20230513T123000
234 DTEND;TZID=Eastern Standard Time:20230513T130000
235 CLASS:PUBLIC
236 PRIORITY:5
237 DTSTAMP:20230508T153204Z
238 TRANSP:OPAQUE
239 STATUS:CONFIRMED
240 SEQUENCE:0
241 LOCATION;LANGUAGE=en-US:
242 BEGIN:VALARM
243 DESCRIPTION:REMINDER
244 TRIGGER;RELATED=START:P
245 ACTION:DISPLAY
246 END:VALARM
247 END:VEVENT
250 describe('calendar', () => {
251     it('should parse vcalendar', () => {
252         const result = parse(`BEGIN:VCALENDAR
253 PRODID:-//Google Inc//Google Calendar 70.9054//EN
254 VERSION:2.0
255 CALSCALE:GREGORIAN
256 METHOD:PUBLISH
257 X-WR-CALNAME:Daily
258 X-WR-TIMEZONE:Europe/Vilnius
259 END:VCALENDAR`);
260         expect(result).toEqual({
261             component: 'vcalendar',
262             version: {
263                 value: '2.0',
264             },
265             prodid: {
266                 value: '-//Google Inc//Google Calendar 70.9054//EN',
267             },
268             calscale: {
269                 value: 'GREGORIAN',
270             },
271             method: {
272                 value: 'PUBLISH',
273             },
274             'x-wr-timezone': {
275                 value: 'Europe/Vilnius',
276             },
277             'x-wr-calname': {
278                 value: 'Daily',
279             },
280         });
281     });
283     it('should parse vevent', () => {
284         const result = parse(vevent);
286         expect(result).toEqual({
287             component: 'vevent',
288             uid: {
289                 value: '7E018059-2165-4170-B32F-6936E88E61E5',
290             },
291             dtstamp: {
292                 value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 8, seconds: 54, isUTC: true },
293             },
294             dtstart: {
295                 value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: false },
296                 parameters: { tzid: 'America/New_York' },
297             },
298             dtend: {
299                 value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: false },
300                 parameters: { tzid: 'Europe/Zurich' },
301             },
302             sequence: {
303                 value: 0,
304             },
305             categories: [
306                 {
307                     value: ['ANNIVERSARY', 'PERSONAL', 'SPECIAL OCCASION'],
308                 },
309             ],
310             summary: {
311                 value: 'Our Blissful Anniversary',
312             },
313         });
314     });
316     it('should parse Boolean properties', () => {
317         expect(parse(veventWithTrueBoolean)).toEqual({
318             component: 'vevent',
319             'x-pm-proton-reply': { value: 'true', parameters: { type: 'boolean' } },
320         });
321         expect(parse(veventWithFalseBoolean)).toEqual({
322             component: 'vevent',
323             'x-pm-proton-reply': { value: 'false', parameters: { type: 'boolean' } },
324         });
325         expect(parse(veventWithRandomBoolean)).toEqual({
326             component: 'vevent',
327             'x-pm-proton-reply': { value: 'false', parameters: { type: 'boolean' } },
328         });
329     });
331     it('should parse valarm', () => {
332         const result = parse(valarm) as VcalValarmComponent;
334         expect(result).toEqual({
335             component: 'valarm',
336             trigger: {
337                 value: { weeks: 0, days: 0, hours: 15, minutes: 0, seconds: 0, isNegative: true },
338             },
339             action: {
340                 value: 'DISPLAY',
341             },
342             description: {
343                 value: 'asd',
344             },
345         });
346     });
348     it('should parse vfreebusy2', () => {
349         expect(parse(vfreebusy2)).toEqual({
350             component: 'vfreebusy',
351             uid: {
352                 value: '19970901T115957Z-76A912@example.com',
353             },
354             dtstamp: {
355                 value: { year: 1997, month: 9, day: 1, hours: 12, minutes: 0, seconds: 0, isUTC: true },
356             },
357             organizer: {
358                 value: 'jsmith@example.com',
359             },
360             dtstart: {
361                 value: {
362                     year: 1998,
363                     month: 3,
364                     day: 13,
365                     hours: 14,
366                     minutes: 17,
367                     seconds: 11,
368                     isUTC: true,
369                 },
370             },
371             dtend: {
372                 value: {
373                     year: 1998,
374                     month: 4,
375                     day: 10,
376                     hours: 14,
377                     minutes: 17,
378                     seconds: 11,
379                     isUTC: true,
380                 },
381             },
382             freebusy: [
383                 {
384                     value: [
385                         {
386                             start: {
387                                 year: 1998,
388                                 month: 3,
389                                 day: 14,
390                                 hours: 23,
391                                 minutes: 30,
392                                 seconds: 0,
393                                 isUTC: true,
394                             },
395                             end: {
396                                 year: 1998,
397                                 month: 3,
398                                 day: 15,
399                                 hours: 0,
400                                 minutes: 30,
401                                 seconds: 0,
402                                 isUTC: true,
403                             },
404                         },
405                     ],
406                 },
407                 {
408                     value: [
409                         {
410                             start: {
411                                 year: 1998,
412                                 month: 3,
413                                 day: 16,
414                                 hours: 15,
415                                 minutes: 30,
416                                 seconds: 0,
417                                 isUTC: true,
418                             },
419                             end: {
420                                 year: 1998,
421                                 month: 3,
422                                 day: 16,
423                                 hours: 16,
424                                 minutes: 30,
425                                 seconds: 0,
426                                 isUTC: true,
427                             },
428                         },
429                     ],
430                 },
431                 {
432                     value: [
433                         {
434                             start: {
435                                 year: 1998,
436                                 month: 3,
437                                 day: 18,
438                                 hours: 3,
439                                 minutes: 0,
440                                 seconds: 0,
441                                 isUTC: true,
442                             },
443                             end: {
444                                 year: 1998,
445                                 month: 3,
446                                 day: 18,
447                                 hours: 4,
448                                 minutes: 0,
449                                 seconds: 0,
450                                 isUTC: true,
451                             },
452                         },
453                     ],
454                 },
455             ],
456             url: {
457                 value: 'http://www.example.com/calendar/busytime/jsmith.ifb',
458             },
459         });
460     });
462     it('should parse vfreebusy', () => {
463         expect(parse(vfreebusy)).toEqual({
464             component: 'vfreebusy',
465             uid: {
466                 value: '19970901T095957Z-76A912@example.com',
467             },
468             dtstamp: {
469                 value: { year: 1997, month: 9, day: 1, hours: 10, minutes: 0, seconds: 0, isUTC: true },
470             },
471             organizer: {
472                 value: 'mailto:jane_doe@example.com',
473             },
474             attendee: [
475                 {
476                     value: 'mailto:john_public@example.com',
477                 },
478             ],
479             freebusy: [
480                 {
481                     value: [
482                         {
483                             start: { year: 1997, month: 10, day: 15, hours: 5, minutes: 0, seconds: 0, isUTC: true },
484                             duration: { weeks: 0, days: 0, hours: 8, minutes: 30, seconds: 0, isNegative: false },
485                         },
486                         {
487                             start: { year: 1997, month: 10, day: 15, hours: 16, minutes: 0, seconds: 0, isUTC: true },
488                             duration: { weeks: 0, days: 0, hours: 5, minutes: 30, seconds: 0, isNegative: false },
489                         },
490                         {
491                             start: { year: 1997, month: 10, day: 15, hours: 22, minutes: 30, seconds: 0, isUTC: true },
492                             duration: { weeks: 0, days: 0, hours: 6, minutes: 30, seconds: 0, isNegative: false },
493                         },
494                     ],
495                 },
496             ],
497             comment: [
498                 {
499                     value: 'This iCalendar file contains busy time information for the next three months.',
500                 },
501             ],
502             url: {
503                 value: 'http://example.com/pub/busy/jpublic-01.ifb',
504             },
505         });
506     });
508     it('should parse all day vevent', () => {
509         const { dtstart } = parse(allDayVevent) as VcalVeventComponent;
511         expect(dtstart).toEqual({
512             value: { year: 2019, month: 8, day: 12 },
513             parameters: { type: 'date' },
514         });
515     });
517     it('should parse vevent with recurrence id', () => {
518         const { dtstart, 'recurrence-id': recurrenceId } = parse(veventWithRecurrenceId) as VcalVeventComponent;
520         expect(recurrenceId).toEqual({
521             value: { year: 2020, month: 3, day: 11, hours: 10, minutes: 0, seconds: 0, isUTC: false },
522             parameters: { tzid: 'Europe/Zurich' },
523         });
525         expect(dtstart).toEqual({
526             value: { year: 2020, month: 3, day: 11, hours: 10, minutes: 0, seconds: 0, isUTC: false },
527             parameters: { tzid: 'Europe/Zurich' },
528         });
529     });
531     it('should parse valarm in vevent', () => {
532         const component = parse(valarmInVevent) as VcalVeventComponent;
534         expect(component).toEqual({
535             component: 'vevent',
536             components: [
537                 {
538                     component: 'valarm',
539                     action: { value: 'DISPLAY' },
540                     trigger: {
541                         value: { weeks: 0, days: 0, hours: 15, minutes: 0, seconds: 0, isNegative: true },
542                     },
543                 },
544             ],
545             uid: {
546                 value: '7E018059-2165-4170-B32F-6936E88E61E5',
547             },
548             dtstamp: {
549                 value: { year: 2019, month: 7, day: 19, hours: 11, minutes: 0, seconds: 0, isUTC: true },
550             },
551             dtstart: {
552                 value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
553             },
554             dtend: {
555                 value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
556             },
557         });
558     });
560     it('should parse daily rrule in vevent', () => {
561         const components = veventsRruleDaily.map(parse);
563         expect(components).toEqual([
564             {
565                 component: 'vevent',
566                 uid: {
567                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
568                 },
569                 dtstart: {
570                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
571                 },
572                 dtend: {
573                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
574                 },
575                 rrule: {
576                     value: {
577                         freq: 'DAILY',
578                         count: 10,
579                         interval: 3,
580                     },
581                 },
582             },
583             {
584                 component: 'vevent',
585                 uid: {
586                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
587                 },
588                 dtstart: {
589                     value: { year: 2019, month: 7, day: 19 },
590                     parameters: { type: 'date' },
591                 },
592                 dtend: {
593                     value: { year: 2019, month: 7, day: 19 },
594                     parameters: { type: 'date' },
595                 },
596                 rrule: {
597                     value: {
598                         freq: 'DAILY',
599                         until: { year: 2020, month: 1, day: 30 },
600                     },
601                 },
602             },
603             {
604                 component: 'vevent',
605                 uid: {
606                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
607                 },
608                 dtstart: {
609                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
610                 },
611                 dtend: {
612                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
613                 },
614                 rrule: {
615                     value: {
616                         freq: 'DAILY',
617                         until: { year: 2020, month: 1, day: 30, hours: 22, minutes: 59, seconds: 59, isUTC: true },
618                     },
619                 },
620             },
621             {
622                 component: 'vevent',
623                 uid: {
624                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
625                 },
626                 dtstart: {
627                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: false },
628                     parameters: { tzid: 'America/New_York' },
629                 },
630                 dtend: {
631                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
632                 },
633                 rrule: {
634                     value: {
635                         freq: 'DAILY',
636                         until: { year: 2020, month: 1, day: 30, hours: 22, minutes: 59, seconds: 59, isUTC: true },
637                     },
638                 },
639             },
640         ]);
641     });
643     it('should parse weekly rrule in vevent', () => {
644         const components = veventsRruleWeekly.map(parse);
646         expect(components).toEqual([
647             {
648                 component: 'vevent',
649                 uid: {
650                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
651                 },
652                 dtstart: {
653                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
654                 },
655                 dtend: {
656                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
657                 },
658                 rrule: {
659                     value: {
660                         freq: 'WEEKLY',
661                         count: 10,
662                         interval: 3,
663                         byday: ['WE', 'TH'],
664                     },
665                 },
666             },
667             {
668                 component: 'vevent',
669                 uid: {
670                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
671                 },
672                 dtstart: {
673                     value: { year: 2019, month: 7, day: 19 },
674                     parameters: { type: 'date' },
675                 },
676                 dtend: {
677                     value: { year: 2019, month: 7, day: 19 },
678                     parameters: { type: 'date' },
679                 },
680                 rrule: {
681                     value: {
682                         freq: 'WEEKLY',
683                         byday: 'MO',
684                         until: { year: 2020, month: 1, day: 30 },
685                     },
686                 },
687             },
688             {
689                 component: 'vevent',
690                 uid: {
691                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
692                 },
693                 dtstart: {
694                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
695                 },
696                 dtend: {
697                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
698                 },
699                 rrule: {
700                     value: {
701                         freq: 'WEEKLY',
702                         byday: 'MO',
703                         until: { year: 2020, month: 1, day: 30, hours: 22, minutes: 59, seconds: 59, isUTC: true },
704                     },
705                 },
706             },
707             {
708                 component: 'vevent',
709                 uid: {
710                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
711                 },
712                 dtstart: {
713                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: false },
714                     parameters: { tzid: 'America/New_York' },
715                 },
716                 dtend: {
717                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
718                 },
719                 rrule: {
720                     value: {
721                         freq: 'WEEKLY',
722                         byday: 'MO',
723                         until: { year: 2020, month: 1, day: 30, hours: 22, minutes: 59, seconds: 59, isUTC: true },
724                     },
725                 },
726             },
727         ]);
728     });
730     it('should parse monthly rrule in vevent', () => {
731         const components = veventsRruleMonthly.map(parse);
733         expect(components).toEqual([
734             {
735                 component: 'vevent',
736                 uid: {
737                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
738                 },
739                 dtstart: {
740                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
741                 },
742                 dtend: {
743                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
744                 },
745                 rrule: {
746                     value: {
747                         freq: 'MONTHLY',
748                         interval: 2,
749                         bymonthday: 13,
750                         until: { year: 2020, month: 1, day: 30, hours: 23, minutes: 0, seconds: 0, isUTC: true },
751                     },
752                 },
753             },
754             {
755                 component: 'vevent',
756                 uid: {
757                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
758                 },
759                 dtstart: {
760                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
761                 },
762                 dtend: {
763                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
764                 },
765                 rrule: {
766                     value: {
767                         freq: 'MONTHLY',
768                         bysetpos: 2,
769                         byday: 'TU',
770                         count: 4,
771                     },
772                 },
773             },
774             {
775                 component: 'vevent',
776                 uid: {
777                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
778                 },
779                 dtstart: {
780                     value: { year: 2019, month: 7, day: 19 },
781                     parameters: { type: 'date' },
782                 },
783                 dtend: {
784                     value: { year: 2019, month: 7, day: 19 },
785                     parameters: { type: 'date' },
786                 },
787                 rrule: {
788                     value: {
789                         freq: 'MONTHLY',
790                         bysetpos: -1,
791                         byday: 'MO',
792                         until: { year: 2020, month: 1, day: 30 },
793                     },
794                 },
795             },
796             {
797                 component: 'vevent',
798                 uid: {
799                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
800                 },
801                 dtstart: {
802                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: false },
803                     parameters: { tzid: 'America/New_York' },
804                 },
805                 dtend: {
806                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
807                 },
808                 rrule: {
809                     value: {
810                         freq: 'MONTHLY',
811                         bymonthday: 2,
812                         until: { year: 2020, month: 1, day: 30, hours: 22, minutes: 59, seconds: 59, isUTC: true },
813                     },
814                 },
815             },
816         ]);
817     });
819     it('should parse yearly rrule in vevent', () => {
820         const components = veventsRruleYearly.map(parse);
822         expect(components).toEqual([
823             {
824                 component: 'vevent',
825                 uid: {
826                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
827                 },
828                 dtstart: {
829                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
830                 },
831                 dtend: {
832                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
833                 },
834                 rrule: {
835                     value: {
836                         freq: 'YEARLY',
837                         bymonth: 7,
838                         bymonthday: 25,
839                         count: 4,
840                     },
841                 },
842             },
843             {
844                 component: 'vevent',
845                 uid: {
846                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
847                 },
848                 dtstart: {
849                     value: { year: 2019, month: 7, day: 19 },
850                     parameters: { type: 'date' },
851                 },
852                 dtend: {
853                     value: { year: 2019, month: 7, day: 19 },
854                     parameters: { type: 'date' },
855                 },
856                 rrule: {
857                     value: {
858                         freq: 'YEARLY',
859                         interval: 2,
860                         until: { year: 2020, month: 1, day: 30 },
861                     },
862                 },
863             },
864             {
865                 component: 'vevent',
866                 uid: {
867                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
868                 },
869                 dtstart: {
870                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: true },
871                 },
872                 dtend: {
873                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
874                 },
875                 rrule: {
876                     value: {
877                         freq: 'YEARLY',
878                         interval: 2,
879                         bymonth: 7,
880                         bymonthday: 25,
881                         until: { year: 2020, month: 1, day: 30, hours: 22, minutes: 59, seconds: 59, isUTC: true },
882                     },
883                 },
884             },
885             {
886                 component: 'vevent',
887                 uid: {
888                     value: '7E018059-2165-4170-B32F-6936E88E61E5',
889                 },
890                 dtstart: {
891                     value: { year: 2019, month: 7, day: 19, hours: 12, minutes: 0, seconds: 0, isUTC: false },
892                     parameters: { tzid: 'America/New_York' },
893                 },
894                 dtend: {
895                     value: { year: 2019, month: 7, day: 19, hours: 13, minutes: 0, seconds: 0, isUTC: true },
896                 },
897                 rrule: {
898                     value: {
899                         freq: 'YEARLY',
900                         until: { year: 2020, month: 1, day: 30, hours: 22, minutes: 59, seconds: 59, isUTC: true },
901                     },
902                 },
903             },
904         ]);
905     });
907     it('should parse attendees in vevent', () => {
908         const component = parse(veventWithAttendees);
910         expect(component).toEqual({
911             component: 'vevent',
912             uid: {
913                 value: '7E018059-2165-4170-B32F-6936E88E61E5',
914             },
915             dtstart: {
916                 value: { year: 2019, month: 8, day: 12 },
917                 parameters: { type: 'date' },
918             },
919             dtend: {
920                 value: { year: 2019, month: 8, day: 13 },
921                 parameters: { type: 'date' },
922             },
923             summary: {
924                 value: 'text',
925             },
926             attendee: [
927                 {
928                     value: 'mailto:james@bond.co.uk',
929                     parameters: {
930                         cutype: 'INDIVIDUAL',
931                         role: 'REQ-PARTICIPANT',
932                         rsvp: 'TRUE',
933                         'x-pm-token': '123',
934                         cn: 'james@bond.co.uk',
935                     },
936                 },
937                 {
938                     value: 'mailto:dr.no@mi6.co.uk',
939                     parameters: {
940                         cutype: 'INDIVIDUAL',
941                         role: 'REQ-PARTICIPANT',
942                         rsvp: 'TRUE',
943                         'x-pm-token': '123',
944                         cn: 'Dr No.',
945                     },
946                 },
947                 {
948                     value: 'mailto:moneypenny@mi6.co.uk',
949                     parameters: {
950                         cutype: 'INDIVIDUAL',
951                         role: 'NON-PARTICIPANT',
952                         rsvp: 'FALSE',
953                         cn: 'Miss Moneypenny',
954                     },
955                 },
956             ],
957         });
958     });
960     const trimAll = (str: string) => str.replace(/\r?\n ?|\r/g, '');
962     it('should round trip valarm in vevent', () => {
963         const result = serialize(parse(valarmInVevent));
964         expect(trimAll(result)).toEqual(trimAll(valarmInVevent));
965     });
967     it('should round trip vfreebusy', () => {
968         const result = serialize(parse(vfreebusy));
969         expect(trimAll(result)).toEqual(trimAll(vfreebusy));
970     });
972     it('should round trip vfreebusy2', () => {
973         const result = serialize(parse(vfreebusy2));
974         expect(trimAll(result)).toEqual(trimAll(vfreebusy2));
975     });
977     it('should round trip rrule in vevent', () => {
978         const vevents = [...veventsRruleDaily, ...veventsRruleWeekly, ...veventsRruleMonthly, ...veventsRruleYearly];
979         const results = vevents.map((vevent) => serialize(parse(vevent)));
980         expect(results.map(trimAll)).toEqual(vevents.map(trimAll));
981     });
983     it('should round trip vevent', () => {
984         const result = serialize(parse(vevent));
985         expect(trimAll(result)).toEqual(trimAll(vevent));
986     });
988     it('should round trip vevent with recurrence-id', () => {
989         const result = serialize(parse(veventWithRecurrenceId));
990         expect(trimAll(result)).toEqual(trimAll(veventWithRecurrenceId));
991     });
993     it('should round trip vevent with attendees', () => {
994         const result = serialize(parse(veventWithAttendees));
995         expect(trimAll(result)).toEqual(trimAll(veventWithAttendees));
996     });
998     it('should round trip all day vevent', () => {
999         const result = serialize(parse(allDayVevent));
1000         expect(trimAll(result)).toEqual(trimAll(allDayVevent));
1001     });
1003     it('should normalize exdate', () => {
1004         const veventWithExdate = `BEGIN:VEVENT
1005 RRULE:FREQ=DAILY;COUNT=6
1006 DTSTART;TZID=Europe/Zurich:20200309T043000
1007 DTEND;TZID=Europe/Zurich:20200309T063000
1008 EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
1009 EXDATE;TZID=Europe/Zurich:20200311T043000
1010 EXDATE;TZID=Europe/Zurich:20200313T043000
1011 EXDATE;VALUE=DATE:20200311
1012 END:VEVENT
1014         const normalizedVevent = `BEGIN:VEVENT
1015 RRULE:FREQ=DAILY;COUNT=6
1016 DTSTART;TZID=Europe/Zurich:20200309T043000
1017 DTEND;TZID=Europe/Zurich:20200309T063000
1018 EXDATE:19960402T010000Z
1019 EXDATE:19960403T010000Z
1020 EXDATE:19960404T010000Z
1021 EXDATE;TZID=Europe/Zurich:20200311T043000
1022 EXDATE;TZID=Europe/Zurich:20200313T043000
1023 EXDATE;VALUE=DATE:20200311
1024 END:VEVENT`;
1025         const result = serialize(parse(veventWithExdate));
1026         expect(trimAll(result)).toEqual(trimAll(normalizedVevent));
1027     });
1029     it('should parse trigger string', () => {
1030         expect(fromTriggerString('-PT30M')).toEqual({
1031             weeks: 0,
1032             days: 0,
1033             hours: 0,
1034             minutes: 30,
1035             seconds: 0,
1036             isNegative: true,
1037         });
1038     });
1040     it('should convert trigger strings into milliseconds', () => {
1041         expect(getMillisecondsFromTriggerString('-PT30M')).toEqual(-30 * MINUTE);
1042         expect(getMillisecondsFromTriggerString('PT1H')).toEqual(HOUR);
1043         expect(getMillisecondsFromTriggerString('-P1D')).toEqual(-DAY);
1044         expect(getMillisecondsFromTriggerString('-PT2H34M12S')).toEqual(-2 * HOUR - 34 * MINUTE - 12 * SECOND);
1045         expect(getMillisecondsFromTriggerString('P2W1DT1S')).toEqual(2 * WEEK + DAY + SECOND);
1046     });
1048     it('should filter out invalid vAlarm', () => {
1049         const parsed = parseVcalendarWithRecoveryAndMaybeErrors(
1050             veventWithInvalidVAlarm
1051         ) as VcalVeventComponentWithMaybeErrors;
1052         expect(getVeventWithoutErrors(parsed)).toEqual({
1053             component: 'vevent',
1054             components: [],
1055             description: { value: '', parameters: { language: 'en-US' } },
1056             uid: {
1057                 value: '040000008200E00074C5B7101A82E00800000000B058B6A2A081D901000000000000000   0100000004A031FE80ACD7C418A7A1762749176F121',
1058             },
1059             summary: {
1060                 value: 'Calendar test',
1061             },
1062             dtstart: {
1063                 value: { year: 2023, month: 5, day: 13, hours: 12, minutes: 30, seconds: 0, isUTC: false },
1064                 parameters: { tzid: 'Eastern Standard Time' },
1065             },
1066             dtend: {
1067                 value: { year: 2023, month: 5, day: 13, hours: 13, minutes: 0, seconds: 0, isUTC: false },
1068                 parameters: {
1069                     tzid: 'Eastern Standard Time',
1070                 },
1071             },
1072             class: { value: 'PUBLIC' },
1073             priority: { value: 5 },
1074             dtstamp: {
1075                 value: { year: 2023, month: 5, day: 8, hours: 15, minutes: 32, seconds: 4, isUTC: true },
1076             },
1077             transp: { value: 'OPAQUE' },
1078             status: { value: 'CONFIRMED' },
1079             sequence: { value: 0 },
1080             location: { value: '', parameters: { language: 'en-US' } },
1081         });
1082     });
1085 describe('parseVcalendarWithRecoveryAndMaybeErrors', () => {
1086     it('should add missing mandatory properties', () => {
1087         const ics = `BEGIN:VCALENDAR
1088 END:VCALENDAR`;
1089         const result = parseVcalendarWithRecoveryAndMaybeErrors(ics) as VcalVcalendarWithMaybeErrors;
1091         expect(result.component).toEqual('vcalendar');
1092         expect(result.version.value).toEqual('2.0');
1093         expect(result.prodid.value).toEqual('');
1094     });
1096     it('should catch errors from badly formatted all-day dates (with recovery for those off)', () => {
1097         const ics = `BEGIN:VCALENDAR
1098 BEGIN:VEVENT
1099 UID:test-uid
1100 DTSTAMP:20200405T143241Z
1101 DTSTART:20200309
1102 END:VEVENT
1103 END:VCALENDAR`;
1104         const result = parseVcalendarWithRecoveryAndMaybeErrors(ics, {
1105             retryDateTimes: false,
1106         }) as VcalVcalendarWithMaybeErrors;
1108         expect(result.component).toEqual('vcalendar');
1109         expect((result.components as VcalErrorComponent[])[0].error).toMatch('invalid date-time value');
1110     });
1112     it('should catch errors from badly formatted date-times (with recovery for those off)', () => {
1113         const ics = `BEGIN:VCALENDAR
1114 X-LOTUS-CHARSET:UTF-8
1115 VERSION:2.0
1116 PRODID:http://www.bahn.de
1117 METHOD:PUBLISH
1118 BEGIN:VEVENT
1119 UID:bahn2023-06-21082400
1120 CLASS:PUBLIC
1121 SUMMARY:Hamburg Hbf -> Paris Est
1122 DTSTART;TZID=Europe/Berlin:2023-06-21T082400
1123 DTEND;TZID=Europe/Berlin:2023-06-21T165400
1124 DTSTAMP:2023-06-13T212500Z
1125 END:VEVENT
1126 END:VCALENDAR`;
1128         const result = parseVcalendarWithRecoveryAndMaybeErrors(ics, {
1129             retryDateTimes: false,
1130         }) as VcalVcalendarWithMaybeErrors;
1132         expect(result.component).toEqual('vcalendar');
1133         expect((result.components as VcalErrorComponent[])[0].error).toMatch('invalid date-time value');
1134     });
1136     it('should catch errors from badly formatted dates (with recovery for those off)', () => {
1137         const ics = `BEGIN:VCALENDAR
1138 X-LOTUS-CHARSET:UTF-8
1139 VERSION:2.0
1140 PRODID:http://www.bahn.de
1141 METHOD:PUBLISH
1142 BEGIN:VEVENT
1143 UID:bahn2023-06-21082400
1144 CLASS:PUBLIC
1145 SUMMARY:Hamburg Hbf -> Paris Est
1146 DTSTART;VALUE=DATE:2023-06-21
1147 DTEND;VALUE=DATE:2023-06-21
1148 DTSTAMP:20230613T212500Z
1149 END:VEVENT
1150 END:VCALENDAR`;
1152         const result = parseVcalendarWithRecoveryAndMaybeErrors(ics, {
1153             retryDateTimes: false,
1154         }) as VcalVcalendarWithMaybeErrors;
1156         expect(result.component).toEqual('vcalendar');
1157         expect((result.components as VcalErrorComponent[])[0].error).toMatch('could not extract integer');
1158     });