1 import { enUS } from 'date-fns/locale';
3 import type { ImportEventError } from '@proton/shared/lib/calendar/icsSurgery/ImportEventError';
4 import { ACCENT_COLORS_MAP } from '@proton/shared/lib/colors';
5 import truncate from '@proton/utils/truncate';
7 import { ICAL_CALSCALE, ICAL_METHOD, MAX_CHARS_API } from '../../lib/calendar/constants';
8 import { getSupportedEvent } from '../../lib/calendar/icsSurgery/vevent';
10 extractSupportedEvent,
11 getComponentIdentifier,
12 getSupportedEventsOrErrors,
14 } from '../../lib/calendar/import/import';
15 import { parse, parseVcalendarWithRecoveryAndMaybeErrors } from '../../lib/calendar/vcal';
16 import { getIcalMethod } from '../../lib/calendar/vcalHelper';
17 import { omit } from '../../lib/helpers/object';
23 VcalVtimezoneComponent,
24 } from '../../lib/interfaces/calendar/VcalModel';
26 describe('getComponentIdentifier', () => {
27 it('should return the empty string if passed an error', () => {
28 expect(getComponentIdentifier({ error: new Error('error'), icalComponent: 'any' })).toEqual('');
31 it('should return the tzid for a VTIMEZONE', () => {
32 const vtimezone = `BEGIN:VTIMEZONE
36 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
37 DTSTART:20030330T030000
43 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
44 DTSTART:20031026T040000
49 const timezone = parse(vtimezone) as VcalVtimezoneComponent;
50 expect(getComponentIdentifier(timezone)).toEqual('Europe/Vilnius');
53 it('should return the uid for an event with a normal uid', () => {
54 const vevent = `BEGIN:VEVENT
55 DTSTAMP:19980309T231000Z
57 DTSTART;TZID=America/New_York:20690312T083000
58 DTEND;TZID=America/New_York:20690312T093000
59 LOCATION:1CP Conference Room 4350
61 const event = parse(vevent) as VcalVeventComponent;
62 expect(getComponentIdentifier(event)).toEqual('test-event');
65 it('should return the original uid for an event with a hash uid with legacy format', () => {
66 const vevent = `BEGIN:VEVENT
67 DTSTAMP:19980309T231000Z
68 UID:original-uid-stmyce9lb3ef@domain.com-sha1-uid-b8ae0238d0011a4961a2d259e33bd383672b9229
69 DTSTART;TZID=America/New_York:20690312T083000
70 DTEND;TZID=America/New_York:20690312T093000
71 LOCATION:1CP Conference Room 4350
73 const event = parse(vevent) as VcalVeventComponent;
74 expect(getComponentIdentifier(event)).toEqual('stmyce9lb3ef@domain.com');
77 it('should return the original uid for an event with a hash uid with legacy format', () => {
78 const vevent = `BEGIN:VEVENT
79 DTSTAMP:19980309T231000Z
80 UID:sha1-uid-b8ae0238d0011a4961a2d259e33bd383672b9229-original-uid-stmyce9lb3ef@domain.com
81 DTSTART;TZID=America/New_York:20690312T083000
82 DTEND;TZID=America/New_York:20690312T093000
83 LOCATION:1CP Conference Room 4350
85 const event = parse(vevent) as VcalVeventComponent;
86 expect(getComponentIdentifier(event)).toEqual('stmyce9lb3ef@domain.com');
89 it('should return the title when the event had no original uid', () => {
90 const vevent = `BEGIN:VEVENT
91 DTSTAMP:19980309T231000Z
92 UID:sha1-uid-b8ae0238d0011a4961a2d259e33bd383672b9229
93 DTSTART;TZID=America/New_York:20690312T083000
94 DTEND;TZID=America/New_York:20690312T093000
96 LOCATION:1CP Conference Room 4350
98 const event = parse(vevent) as VcalVeventComponent;
99 expect(getComponentIdentifier(event)).toEqual('Test event');
102 it('should return the date-time when the part-day event has no uid and no title', () => {
103 const vevent = `BEGIN:VEVENT
104 DTSTAMP:19980309T231000Z
105 DTSTART;TZID=America/New_York:20690312T083000
106 DTEND;TZID=America/New_York:20690312T093000
107 LOCATION:1CP Conference Room 4350
109 const event = parse(vevent) as VcalVeventComponent;
110 expect(getComponentIdentifier(event, { locale: enUS })).toEqual('Mar 12, 2069, 8:30:00 AM');
113 it('should return the date when the all-day event has no uid and no title', () => {
114 const vevent = `BEGIN:VEVENT
115 DTSTAMP:19980309T231000Z
116 DTSTART;VALUE=DATE:20690312
117 LOCATION:1CP Conference Room 4350
119 const event = parse(vevent) as VcalVeventComponent;
120 expect(getComponentIdentifier(event, { locale: enUS })).toEqual('Mar 12, 2069');
124 describe('getSupportedEvent', () => {
125 // dummy component identifiers
126 const componentIdentifiers = {
132 it('should catch events with start time before 1970', () => {
133 const vevent = `BEGIN:VEVENT
134 DTSTAMP:19980309T231000Z
136 DTSTART;TZID=America/New_York:19690312T083000
137 DTEND;TZID=/America/New_York:19690312T093000
138 LOCATION:1CP Conference Room 4350
140 const event = parse(vevent) as VcalVeventComponent;
143 vcalVeventComponent: event,
144 hasXWrTimezone: false,
145 guessTzid: 'Asia/Seoul',
146 componentIdentifiers,
148 ).toThrowError('Start time out of bounds');
151 it('should catch events with start time after 2038', () => {
152 const vevent = `BEGIN:VEVENT
153 DTSTAMP:19980309T231000Z
155 DTSTART;VALUE=DATE:20380101
156 DTEND;VALUE=DATE:20380102
157 LOCATION:1CP Conference Room 4350
159 const event = parse(vevent) as VcalVeventComponent;
162 vcalVeventComponent: event,
163 hasXWrTimezone: false,
164 guessTzid: 'Asia/Seoul',
165 componentIdentifiers,
167 ).toThrowError('Start time out of bounds');
170 it('should catch malformed all-day events', () => {
171 const vevent = `BEGIN:VEVENT
172 DTSTAMP:19980309T231000Z
174 DTSTART;VALUE=DATE:20180101
175 DTEND:20191231T203000Z
176 LOCATION:1CP Conference Room 4350
178 const event = parse(vevent) as VcalVeventComponent;
181 vcalVeventComponent: event,
182 hasXWrTimezone: false,
183 guessTzid: 'Asia/Seoul',
184 componentIdentifiers,
186 ).toThrowError('Malformed all-day event');
189 it('should catch events with start and end time after 2038 and take time zones into account', () => {
190 const vevent = `BEGIN:VEVENT
191 DTSTAMP:19980309T231000Z
193 DTSTART;TZID=America/New_York:20371231T203000
194 DTEND;TZID=America/New_York:20380101T003000
195 LOCATION:1CP Conference Room 4350
197 const event = parse(vevent) as VcalVeventComponent;
200 vcalVeventComponent: event,
201 hasXWrTimezone: false,
202 guessTzid: 'Asia/Seoul',
203 componentIdentifiers,
205 ).toThrowError('Start time out of bounds');
208 it('should accept events with sequence', () => {
209 const vevent = `BEGIN:VEVENT
210 DTSTAMP:19980309T231000Z
212 DTSTART;TZID=America/New_York:20020312T083000
213 DTEND;TZID=America/New_York:20020312T082959
216 const event = parse(vevent) as VcalVeventComponent;
219 vcalVeventComponent: event,
220 hasXWrTimezone: false,
221 guessTzid: 'Asia/Seoul',
222 componentIdentifiers,
226 uid: { value: 'test-event' },
228 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
231 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
232 parameters: { tzid: 'America/New_York' },
234 sequence: { value: 11 },
238 it('should fix events with a sequence that is too big', () => {
239 const sequenceOutOfBounds = 2 ** 31 + 3;
240 const vevent = `BEGIN:VEVENT
241 DTSTAMP:19980309T231000Z
243 DTSTART;TZID=America/New_York:20020312T083000
244 DTEND;TZID=America/New_York:20020312T082959
245 SEQUENCE:${sequenceOutOfBounds}
247 const event = parse(vevent) as VcalVeventComponent;
250 vcalVeventComponent: event,
251 hasXWrTimezone: false,
252 guessTzid: 'Asia/Seoul',
253 componentIdentifiers,
257 uid: { value: 'test-event' },
259 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
262 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
263 parameters: { tzid: 'America/New_York' },
265 sequence: { value: 3 },
269 it('should accept (and re-format) events with negative duration and negative sequence', () => {
270 const vevent = `BEGIN:VEVENT
271 DTSTAMP:19980309T231000Z
273 DTSTART;TZID=/America/New_York:20020312T083000
274 DTEND;TZID=/America/New_York:20020312T082959
275 LOCATION:1CP Conference Room 4350
278 const event = parse(vevent) as VcalVeventComponent;
281 vcalVeventComponent: event,
282 hasXWrTimezone: false,
283 guessTzid: 'Asia/Seoul',
284 componentIdentifiers,
288 uid: { value: 'test-event' },
290 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
293 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
294 parameters: { tzid: 'America/New_York' },
296 location: { value: '1CP Conference Room 4350' },
297 sequence: { value: 0 },
301 it('should drop DTEND for part-day events with zero duration', () => {
302 const vevent = `BEGIN:VEVENT
303 DTSTAMP:19980309T231000Z
305 DTSTART;TZID=/mozilla.org/20050126_1/America/New_York:20020312T083000
306 DTEND;TZID=/mozilla.org/20050126_1/America/New_York:20020312T083000
307 LOCATION:1CP Conference Room 4350
309 const event = parse(vevent) as VcalVeventComponent;
312 vcalVeventComponent: event,
313 hasXWrTimezone: false,
314 guessTzid: 'Asia/Seoul',
315 componentIdentifiers,
319 uid: { value: 'test-event' },
321 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
324 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
325 parameters: { tzid: 'America/New_York' },
327 location: { value: '1CP Conference Room 4350' },
328 sequence: { value: 0 },
332 it('should drop DTEND for all-day events with zero duration', () => {
333 const vevent = `BEGIN:VEVENT
334 DTSTAMP:19980309T231000Z
336 DTSTART;VALUE=DATE:20020312
337 DTEND;VALUE=DATE:20020312
338 LOCATION:1CP Conference Room 4350
340 const event = parse(vevent) as VcalVeventComponent;
343 vcalVeventComponent: event,
344 hasXWrTimezone: false,
345 guessTzid: 'Asia/Seoul',
346 componentIdentifiers,
350 uid: { value: 'test-event' },
352 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
355 value: { year: 2002, month: 3, day: 12 },
356 parameters: { type: 'date' },
358 location: { value: '1CP Conference Room 4350' },
359 sequence: { value: 0 },
363 it('should modify events whose duration is specified to convert that into a dtend', () => {
364 const vevent = `BEGIN:VEVENT
365 DTSTAMP:19980309T231000Z
367 DTSTART;TZID=America/New_York:20020312T083000
369 LOCATION:1CP Conference Room 4350
371 const event = parse(vevent) as VcalVeventComponent;
374 vcalVeventComponent: event,
375 hasXWrTimezone: false,
376 guessTzid: 'Asia/Seoul',
377 componentIdentifiers,
381 uid: { value: 'test-event' },
382 dtstamp: { value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true } },
384 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
385 parameters: { tzid: 'America/New_York' },
387 location: { value: '1CP Conference Room 4350' },
388 sequence: { value: 0 },
390 value: { year: 2002, month: 3, day: 12, hours: 9, minutes: 30, seconds: 0, isUTC: false },
391 parameters: { tzid: 'America/New_York' },
396 it('should filter out notifications out of bounds', () => {
397 const vevent = `BEGIN:VEVENT
398 DTSTAMP:19980309T231000Z
400 DTSTART;TZID=America/New_York:19990312T083000
401 DTEND;TZID=America/New_York:19990312T093000
406 LOCATION:1CP Conference Room 4350
408 const event = parse(vevent) as VcalVeventComponent;
411 vcalVeventComponent: event,
412 hasXWrTimezone: false,
413 guessTzid: 'Asia/Seoul',
414 componentIdentifiers,
418 uid: { value: 'test-event' },
420 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
423 value: { year: 1999, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
424 parameters: { tzid: 'America/New_York' },
427 value: { year: 1999, month: 3, day: 12, hours: 9, minutes: 30, seconds: 0, isUTC: false },
428 parameters: { tzid: 'America/New_York' },
430 location: { value: '1CP Conference Room 4350' },
431 sequence: { value: 0 },
435 it('should normalize notifications', () => {
436 const vevent = `BEGIN:VEVENT
437 DTSTAMP:19980309T231000Z
439 DTSTART;VALUE=DATE:19990312
440 DTEND;VALUE=DATE:19990313
443 TRIGGER;VALUE=DATE-TIME:19960401T005545Z
445 LOCATION:1CP Conference Room 4350
447 const event = parse(vevent) as VcalVeventComponent;
450 vcalVeventComponent: event,
451 hasXWrTimezone: false,
452 guessTzid: 'Asia/Seoul',
453 componentIdentifiers,
457 uid: { value: 'test-event' },
459 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
462 value: { year: 1999, month: 3, day: 12 },
463 parameters: { type: 'date' },
465 location: { value: '1CP Conference Room 4350' },
466 sequence: { value: 0 },
470 action: { value: 'DISPLAY' },
486 it('should catch inconsistent rrules', () => {
487 const veventNoOccurrenceOnDtstart = `BEGIN:VEVENT
488 DTSTART;TZID=Europe/Vilnius:20200503T150000
489 DTEND;TZID=Europe/Vilnius:20200503T160000
490 RRULE:FREQ=MONTHLY;BYDAY=1MO
491 DTSTAMP:20200508T121218Z
492 UID:71hdoqnevmnq80hfaeadnq8d0v@google.com
494 const eventNoOccurrenceOnDtstart = parse(veventNoOccurrenceOnDtstart) as VcalVeventComponent;
497 vcalVeventComponent: eventNoOccurrenceOnDtstart,
498 hasXWrTimezone: false,
499 guessTzid: 'Asia/Seoul',
500 componentIdentifiers,
502 ).toThrowError('Malformed recurring event');
504 const veventWithByyeardayNotYearly = `BEGIN:VEVENT
505 DTSTART;TZID=Europe/Vilnius:20200103T150000
506 RRULE:FREQ=MONTHLY;BYYEARDAY=3
507 DTSTAMP:20200508T121218Z
508 UID:71hdoqnevmnq80hfaeadnq8d0v@google.com
510 const eventWithByyeardayNotYearly = parse(veventWithByyeardayNotYearly) as VcalVeventComponent;
513 vcalVeventComponent: eventWithByyeardayNotYearly,
514 hasXWrTimezone: false,
515 guessTzid: 'Asia/Seoul',
516 componentIdentifiers,
518 ).toThrowError('Malformed recurring event');
521 it('should catch malformed rrules', () => {
522 const vevent = `BEGIN:VEVENT
523 DTSTART;TZID=Europe/Vilnius:20200503T150000
524 DTEND;TZID=Europe/Vilnius:20200503T160000
525 EXDATE;TZID=Europe/Vilnius:20200503T150000
526 DTSTAMP:20200508T121218Z
527 UID:71hdoqnevmnq80hfaeadnq8d0v@google.com
529 const event = parse(vevent) as VcalVeventComponent;
532 vcalVeventComponent: event,
533 hasXWrTimezone: false,
534 guessTzid: 'Asia/Seoul',
535 componentIdentifiers,
537 ).toThrowError('Malformed recurring event');
540 it('should catch inconsistent rrules after reformatting bad untils', () => {
541 const vevent = `BEGIN:VEVENT
542 DTSTART;TZID=Europe/Vilnius:20200503T150000
543 DTEND;TZID=Europe/Vilnius:20200503T160000
544 RRULE:FREQ=MONTHLY;BYDAY=1MO;UNTIL=20000101T000000Z
545 DTSTAMP:20200508T121218Z
546 UID:71hdoqnevmnq80hfaeadnq8d0v@google.com
548 const event = parse(vevent) as VcalVeventComponent;
551 vcalVeventComponent: event,
552 hasXWrTimezone: false,
553 guessTzid: 'Asia/Seoul',
554 componentIdentifiers,
556 ).toThrowError('Malformed recurring event');
559 it('should catch recurring single edits', () => {
560 const vevent = `BEGIN:VEVENT
561 DTSTART;TZID=Europe/Vilnius:20200503T150000
562 DTEND;TZID=Europe/Vilnius:20200503T160000
564 RECURRENCE-ID;TZID=Europe/Vilnius:20200505T150000
565 DTSTAMP:20200508T121218Z
566 UID:71hdoqnevmnq80hfaeadnq8d0v@google.com
568 const event = parse(vevent) as VcalVeventComponent;
571 vcalVeventComponent: event,
572 hasXWrTimezone: false,
573 guessTzid: 'Asia/Seoul',
574 componentIdentifiers,
576 ).toThrowError('Edited event not supported');
579 it('should catch recurring events with no occurrences because of EXDATE', () => {
580 const vevent = `BEGIN:VEVENT
581 DTSTART;TZID=Europe/Warsaw:20130820T145000
582 DTEND;TZID=Europe/Warsaw:20130820T152000
583 RRULE:FREQ=DAILY;UNTIL=20130822T125000Z
584 EXDATE;TZID=Europe/Warsaw:20130820T145000
585 EXDATE;TZID=Europe/Warsaw:20130821T145000
586 EXDATE;TZID=Europe/Warsaw:20130822T145000
587 DTSTAMP:20200708T215912Z
588 UID:qkbndaqtgkuj4nfr21adr86etk@google.com
589 CREATED:20130902T220905Z
591 LAST-MODIFIED:20130902T220905Z
592 LOCATION:Twinpigs - Żory\\, Katowicka 4
595 SUMMARY:Scenka: napad na bank
598 const event = parse(vevent) as VcalVeventComponent;
601 vcalVeventComponent: event,
602 hasXWrTimezone: false,
603 guessTzid: 'Asia/Seoul',
604 componentIdentifiers,
606 ).toThrowError('Recurring event has no occurrences');
609 it('should catch recurring events with no occurrences because of COUNT', () => {
610 const vevent = `BEGIN:VEVENT
611 DTSTART;TZID=Europe/Warsaw:20211020T145000
612 DTEND;TZID=Europe/Warsaw:20211020T152000
613 RRULE:FREQ=WEEKLY;COUNT=0
614 DTSTAMP:20200708T215912Z
615 UID:qkbndaqtgkuj4nfr21adr86etk@google.com
616 CREATED:20130902T220905Z
618 LAST-MODIFIED:20130902T220905Z
619 LOCATION:Twinpigs - Żory\\, Katowicka 4
622 SUMMARY:Scenka: napad na bank
625 const event = parse(vevent) as VcalVeventComponent;
628 vcalVeventComponent: event,
629 hasXWrTimezone: false,
630 guessTzid: 'Asia/Seoul',
631 componentIdentifiers,
633 ).toThrowError('Recurring event has no occurrences');
636 it('should catch malformed recurring events with no occurrences (throw because of malformed)', () => {
637 const vevent = `BEGIN:VEVENT
638 DTSTART;TZID=Europe/Warsaw:20211020T145000
639 DTEND;TZID=Europe/Warsaw:20211020T152000
640 RRULE:FREQ=WEEKLY;WKST=MO;BYDAY=SA;COUNT=0
641 DTSTAMP:20200708T215912Z
642 UID:qkbndaqtgkuj4nfr21adr86etk@google.com
643 CREATED:20130902T220905Z
645 LAST-MODIFIED:20130902T220905Z
646 LOCATION:Twinpigs - Żory\\, Katowicka 4
649 SUMMARY:Scenka: napad na bank
652 const event = parse(vevent) as VcalVeventComponent;
655 vcalVeventComponent: event,
656 hasXWrTimezone: false,
657 guessTzid: 'Asia/Seoul',
658 componentIdentifiers,
660 ).toThrowError('Malformed recurring event');
663 it('should catch non-supported rrules', () => {
664 const vevent = `BEGIN:VEVENT
665 DTSTART;TZID=Europe/Vilnius:20200518T150000
666 DTEND;TZID=Europe/Vilnius:20200518T160000
667 RRULE:FREQ=MONTHLY;BYDAY=-2MO
668 DTSTAMP:20200508T121218Z
669 UID:71hdoqnevmnq80hfaeadnq8d0v@google.com
671 const event = parse(vevent) as VcalVeventComponent;
674 vcalVeventComponent: event,
675 hasXWrTimezone: false,
676 guessTzid: 'Asia/Seoul',
677 componentIdentifiers,
679 ).toThrowError('Recurring rule not supported');
682 it('should normalize exdate', () => {
683 const vevent = `BEGIN:VEVENT
684 DTSTAMP:19980309T231000Z
686 DTSTART;TZID=W. Europe Standard Time:20021230T203000
688 EXDATE;TZID=W. Europe Standard Time:20200610T170000,20200611T170000
690 const event = parse(vevent) as VcalVeventComponent;
693 vcalVeventComponent: event,
694 hasXWrTimezone: false,
695 guessTzid: 'Asia/Seoul',
696 componentIdentifiers,
700 uid: { value: 'test-event' },
702 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
705 value: { year: 2002, month: 12, day: 30, hours: 20, minutes: 30, seconds: 0, isUTC: false },
706 parameters: { tzid: 'Europe/Berlin' },
708 sequence: { value: 0 },
712 tzid: 'Europe/Berlin',
726 tzid: 'Europe/Berlin',
747 it('should reformat some invalid exdates', () => {
748 const vevent = `BEGIN:VEVENT
749 DTSTAMP:19980309T231000Z
751 DTSTART;VALUE=DATE:20021230
753 EXDATE;TZID=W. Europe Standard Time:20200610T170000,20200611T170000
755 const event = parse(vevent) as VcalVeventComponent;
758 vcalVeventComponent: event,
759 hasXWrTimezone: false,
760 guessTzid: 'Asia/Seoul',
761 componentIdentifiers,
765 uid: { value: 'test-event' },
767 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
770 value: { year: 2002, month: 12, day: 30 },
771 parameters: { type: 'date' },
773 sequence: { value: 0 },
776 parameters: { type: 'date' },
777 value: { day: 10, month: 6, year: 2020 },
780 parameters: { type: 'date' },
781 value: { day: 11, month: 6, year: 2020 },
784 rrule: { value: { freq: 'DAILY' } },
788 it('should support unofficial time zones in our database and normalize recurrence-id', () => {
789 const vevent = `BEGIN:VEVENT
790 DTSTAMP:19980309T231000Z
792 DTSTART;TZID=Mountain Time (U.S. & Canada):20021230T203000
793 DTEND;TZID=W. Europe Standard Time:20030101T003000
794 RECURRENCE-ID;TZID=Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb:20030102T003000
795 LOCATION:1CP Conference Room 4350
797 const event = parse(vevent) as VcalVeventComponent;
800 vcalVeventComponent: event,
801 hasXWrTimezone: false,
802 guessTzid: 'Asia/Seoul',
803 componentIdentifiers,
807 uid: { value: 'test-event' },
809 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
812 value: { year: 2002, month: 12, day: 30, hours: 20, minutes: 30, seconds: 0, isUTC: false },
813 parameters: { tzid: 'America/Denver' },
816 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
817 parameters: { tzid: 'Europe/Berlin' },
819 sequence: { value: 0 },
821 value: { year: 2003, month: 1, day: 2, hours: 0, minutes: 30, seconds: 0, isUTC: false },
822 parameters: { tzid: 'Europe/Sarajevo' },
824 location: { value: '1CP Conference Room 4350' },
828 it('should localize Zulu times in the presence of a calendar time zone for non-recurring events', () => {
829 const vevent = `BEGIN:VEVENT
830 DTSTAMP:19980309T231000Z
832 DTSTART:20110613T150000Z
833 DTEND:20110613T160000Z
834 LOCATION:1CP Conference Room 4350
836 const event = parse(vevent) as VcalVeventComponent;
839 vcalVeventComponent: event,
840 hasXWrTimezone: true,
841 calendarTzid: 'Europe/Zurich',
842 guessTzid: 'Asia/Seoul',
843 componentIdentifiers,
847 uid: { value: 'test-event' },
849 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
852 value: { year: 2011, month: 6, day: 13, hours: 17, minutes: 0, seconds: 0, isUTC: false },
853 parameters: { tzid: 'Europe/Zurich' },
856 value: { year: 2011, month: 6, day: 13, hours: 18, minutes: 0, seconds: 0, isUTC: false },
857 parameters: { tzid: 'Europe/Zurich' },
859 sequence: { value: 0 },
860 location: { value: '1CP Conference Room 4350' },
864 it('should not localize Zulu times in the presence of a calendar time zone for recurring events', () => {
865 const vevent = `BEGIN:VEVENT
866 DTSTAMP:19980309T231000Z
868 DTSTART:20110613T150000Z
869 DTEND:20110613T160000Z
870 RECURRENCE-ID:20110618T150000Z
871 LOCATION:1CP Conference Room 4350
873 const event = parse(vevent) as VcalVeventComponent;
876 vcalVeventComponent: event,
877 hasXWrTimezone: true,
878 calendarTzid: 'Europe/Zurich',
879 guessTzid: 'Asia/Seoul',
880 componentIdentifiers,
884 uid: { value: 'test-event' },
886 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
889 value: { year: 2011, month: 6, day: 13, hours: 15, minutes: 0, seconds: 0, isUTC: true },
892 value: { year: 2011, month: 6, day: 13, hours: 16, minutes: 0, seconds: 0, isUTC: true },
895 value: { year: 2011, month: 6, day: 18, hours: 15, minutes: 0, seconds: 0, isUTC: true },
897 location: { value: '1CP Conference Room 4350' },
898 sequence: { value: 0 },
902 it('should localize events with floating times with the guess time zone if no global time zone has been specified', () => {
905 DTSTAMP:19980309T231000Z
907 DTSTART:20021231T203000
908 DTEND:20030101T003000
909 RECURRENCE-ID:20030102T003000
910 LOCATION:1CP Conference Room 4350
912 const event = parse(vevent) as VcalVeventComponent;
915 vcalVeventComponent: event,
916 hasXWrTimezone: false,
917 guessTzid: 'Asia/Seoul',
918 componentIdentifiers,
922 dtstart: { value: event.dtstart.value, parameters: { tzid: 'Asia/Seoul' } } as VcalDateTimeProperty,
923 dtend: { value: event.dtend!.value, parameters: { tzid: 'Asia/Seoul' } } as VcalDateTimeProperty,
925 value: event['recurrence-id']!.value,
926 parameters: { tzid: 'Asia/Seoul' },
927 } as VcalDateTimeProperty,
928 sequence: { value: 0 },
932 it(`should reject events with floating times if a non-supported global time zone has been specified`, () => {
935 DTSTAMP:19980309T231000Z
937 DTSTART:20021231T203000
938 DTEND:20030101T003000
939 LOCATION:1CP Conference Room 4350
941 const event = parse(vevent) as VcalVeventComponent;
944 vcalVeventComponent: event,
945 hasXWrTimezone: true,
946 guessTzid: 'Asia/Seoul',
947 componentIdentifiers,
949 ).toThrowError('Calendar time zone not supported');
952 it('should support floating times if a supported global time zone has been specified', () => {
955 DTSTAMP:19980309T231000Z
957 DTSTART:20021231T203000
958 DTEND:20030101T003000
959 LOCATION:1CP Conference Room 4350
961 const tzid = 'Europe/Brussels';
962 const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
965 vcalVeventComponent: event,
967 hasXWrTimezone: true,
968 guessTzid: 'Asia/Seoul',
969 componentIdentifiers,
973 dtstart: { value: event.dtstart.value, parameters: { tzid } } as VcalDateTimeProperty,
974 dtend: { value: event.dtend.value, parameters: { tzid } } as VcalDateTimeProperty,
975 sequence: { value: 0 },
979 it('should ignore global time zone if part-day event time is not floating', () => {
982 DTSTAMP:19980309T231000Z
984 DTSTART;TZID=Europe/Vilnius:20200518T150000
985 DTEND;TZID=Europe/Vilnius:20200518T160000
986 LOCATION:1CP Conference Room 4350
989 const tzid = 'Europe/Brussels';
990 const event = parse(vevent) as VcalVeventComponent;
993 vcalVeventComponent: event,
995 hasXWrTimezone: true,
996 guessTzid: 'Asia/Seoul',
997 componentIdentifiers,
1002 it('should ignore global time zone for all-day events', () => {
1005 DTSTAMP:19980309T231000Z
1007 DTSTART;VALUE=DATE:20200518
1008 DTEND;VALUE=DATE:20200520
1009 LOCATION:1CP Conference Room 4350
1012 const tzid = 'Europe/Brussels';
1013 const event = parse(vevent) as VcalVeventComponent;
1016 vcalVeventComponent: event,
1018 hasXWrTimezone: true,
1019 guessTzid: 'Asia/Seoul',
1020 componentIdentifiers,
1025 it('should not support other time zones not in our list', () => {
1026 const vevent = `BEGIN:VEVENT
1027 DTSTAMP:19980309T231000Z
1029 DTSTART;TZID=Chamorro Standard Time:20021231T203000
1030 DTEND;TZID=Chamorro Standard Time:20030101T003000
1031 LOCATION:1CP Conference Room 4350
1033 const event = parse(vevent) as VcalVeventComponent;
1036 vcalVeventComponent: event,
1037 hasXWrTimezone: false,
1038 guessTzid: 'Asia/Seoul',
1039 componentIdentifiers,
1041 ).toThrowError('Time zone not supported');
1044 it('should crop long UIDs and truncate titles, descriptions and locations', () => {
1046 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et. Ac tincidunt vitae semper quis lectus nulla at volutpat. Egestas congue quisque egestas diam in arcu. Cras adipiscing enim eu turpis. Ullamcorper eget nulla facilisi etiam dignissim diam quis. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor posuere. Pulvinar mattis nunc sed blandit libero volutpat sed. Enim nec dui nunc mattis enim ut tellus elementum. Vulputate dignissim suspendisse in est ante in nibh mauris. Malesuada pellentesque elit eget gravida cum. Amet aliquam id diam maecenas ultricies. Aliquam sem fringilla ut morbi tincidunt augue interdum velit. Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Adipiscing elit duis tristique sollicitudin nibh sit. Pulvinar proin gravida hendrerit lectus. Sit amet justo donec enim diam. Purus sit amet luctus venenatis lectus magna. Iaculis at erat pellentesque adipiscing commodo. Morbi quis commodo odio aenean. Sed cras ornare arcu dui vivamus arcu felis bibendum. Viverra orci sagittis eu volutpat. Tempor orci eu lobortis elementum nibh tellus molestie nunc non. Turpis egestas integer eget aliquet. Venenatis lectus magna fringilla urna porttitor. Neque gravida in fermentum et sollicitudin. Tempor commodo ullamcorper a lacus vestibulum sed arcu non odio. Ac orci phasellus egestas tellus rutrum tellus pellentesque eu. Et magnis dis parturient montes nascetur ridiculus mus mauris. Massa sapien faucibus et molestie ac feugiat sed lectus. Et malesuada fames ac turpis. Tristique nulla aliquet enim tortor at auctor urna. Sit amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Enim eu turpis egestas pretium aenean pharetra magna ac. Lacus luctus accumsan tortor posuere ac ut. Volutpat ac tincidunt vitae semper quis lectus nulla. Egestas sed sed risus pretium quam vulputate dignissim suspendisse in. Mauris in aliquam sem fringilla ut morbi tincidunt augue interdum. Pharetra et ultrices neque ornare aenean euismod. Vitae aliquet nec ullamcorper sit amet risus nullam eget felis. Egestas congue quisque egestas diam in arcu cursus euismod. Tellus rutrum tellus pellentesque eu. Nunc scelerisque viverra mauris in aliquam sem fringilla ut. Morbi tristique senectus et netus et malesuada fames ac. Risus sed vulputate odio ut enim blandit volutpat. Pellentesque sit amet porttitor eget. Pharetra convallis posuere morbi leo urna molestie at. Tempor commodo ullamcorper a lacus vestibulum sed. Convallis tellus id interdum velit laoreet id donec ultrices. Nec ultrices dui sapien eget mi proin sed libero enim. Sit amet mauris commodo quis imperdiet massa. Sagittis purus sit amet volutpat consequat mauris nunc. Neque aliquam vestibulum morbi blandit cursus risus at ultrices. Id aliquet risus feugiat in ante metus dictum at tempor. Dignissim sodales ut eu sem integer vitae justo. Laoreet sit amet cursus sit. Eget aliquet nibh praesent tristique. Scelerisque varius morbi enim nunc faucibus. In arcu cursus euismod quis viverra nibh. At volutpat diam ut venenatis tellus in. Sodales neque sodales ut etiam sit amet nisl. Turpis in eu mi bibendum neque egestas congue quisque. Eu consequat ac felis donec et odio. Rutrum quisque non tellus orci ac auctor augue mauris augue. Mollis nunc sed id semper risus. Euismod in pellentesque massa placerat duis ultricies lacus sed turpis. Tellus orci ac auctor augue mauris augue neque gravida. Mi sit amet mauris commodo quis imperdiet massa. Ullamcorper velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Ipsum faucibus vitae aliquet nec ullamcorper sit amet. Massa tincidunt dui ut ornare lectus sit.';
1048 'this-is-gonna-be-a-loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-uid@proton.me';
1049 const croppedUID = longUID.substring(longUID.length - MAX_CHARS_API.UID, longUID.length);
1050 const vevent = `BEGIN:VEVENT
1051 DTSTAMP:19980309T231000Z
1052 UID:this-is-gonna-be-a-loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong-uid@proton.me
1053 DTSTART;VALUE=DATE:20080101
1054 DTEND;VALUE=DATE:20080102
1055 SUMMARY:Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et. Ac tincidunt vitae semper quis lectus nulla at volutpat. Egestas congue quisque egestas diam in arcu. Cras adipiscing enim eu turpis. Ullamcorper eget nulla facilisi etiam dignissim diam quis. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor posuere. Pulvinar mattis nunc sed blandit libero volutpat sed. Enim nec dui nunc mattis enim ut tellus elementum. Vulputate dignissim suspendisse in est ante in nibh mauris. Malesuada pellentesque elit eget gravida cum. Amet aliquam id diam maecenas ultricies. Aliquam sem fringilla ut morbi tincidunt augue interdum velit. Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Adipiscing elit duis tristique sollicitudin nibh sit. Pulvinar proin gravida hendrerit lectus. Sit amet justo donec enim diam.
1056 DESCRIPTION:Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et. Ac tincidunt vitae semper quis lectus nulla at volutpat. Egestas congue quisque egestas diam in arcu. Cras adipiscing enim eu turpis. Ullamcorper eget nulla facilisi etiam dignissim diam quis. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor posuere. Pulvinar mattis nunc sed blandit libero volutpat sed. Enim nec dui nunc mattis enim ut tellus elementum. Vulputate dignissim suspendisse in est ante in nibh mauris. Malesuada pellentesque elit eget gravida cum. Amet aliquam id diam maecenas ultricies. Aliquam sem fringilla ut morbi tincidunt augue interdum velit. Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Adipiscing elit duis tristique sollicitudin nibh sit. Pulvinar proin gravida hendrerit lectus. Sit amet justo donec enim diam. Purus sit amet luctus venenatis lectus magna. Iaculis at erat pellentesque adipiscing commodo. Morbi quis commodo odio aenean. Sed cras ornare arcu dui vivamus arcu felis bibendum. Viverra orci sagittis eu volutpat. Tempor orci eu lobortis elementum nibh tellus molestie nunc non. Turpis egestas integer eget aliquet. Venenatis lectus magna fringilla urna porttitor. Neque gravida in fermentum et sollicitudin. Tempor commodo ullamcorper a lacus vestibulum sed arcu non odio. Ac orci phasellus egestas tellus rutrum tellus pellentesque eu. Et magnis dis parturient montes nascetur ridiculus mus mauris. Massa sapien faucibus et molestie ac feugiat sed lectus. Et malesuada fames ac turpis. Tristique nulla aliquet enim tortor at auctor urna. Sit amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Enim eu turpis egestas pretium aenean pharetra magna ac. Lacus luctus accumsan tortor posuere ac ut. Volutpat ac tincidunt vitae semper quis lectus nulla. Egestas sed sed risus pretium quam vulputate dignissim suspendisse in. Mauris in aliquam sem fringilla ut morbi tincidunt augue interdum. Pharetra et ultrices neque ornare aenean euismod. Vitae aliquet nec ullamcorper sit amet risus nullam eget felis. Egestas congue quisque egestas diam in arcu cursus euismod. Tellus rutrum tellus pellentesque eu. Nunc scelerisque viverra mauris in aliquam sem fringilla ut. Morbi tristique senectus et netus et malesuada fames ac. Risus sed vulputate odio ut enim blandit volutpat. Pellentesque sit amet porttitor eget. Pharetra convallis posuere morbi leo urna molestie at. Tempor commodo ullamcorper a lacus vestibulum sed. Convallis tellus id interdum velit laoreet id donec ultrices. Nec ultrices dui sapien eget mi proin sed libero enim. Sit amet mauris commodo quis imperdiet massa. Sagittis purus sit amet volutpat consequat mauris nunc. Neque aliquam vestibulum morbi blandit cursus risus at ultrices. Id aliquet risus feugiat in ante metus dictum at tempor. Dignissim sodales ut eu sem integer vitae justo. Laoreet sit amet cursus sit. Eget aliquet nibh praesent tristique. Scelerisque varius morbi enim nunc faucibus. In arcu cursus euismod quis viverra nibh. At volutpat diam ut venenatis tellus in. Sodales neque sodales ut etiam sit amet nisl. Turpis in eu mi bibendum neque egestas congue quisque. Eu consequat ac felis donec et odio. Rutrum quisque non tellus orci ac auctor augue mauris augue. Mollis nunc sed id semper risus. Euismod in pellentesque massa placerat duis ultricies lacus sed turpis. Tellus orci ac auctor augue mauris augue neque gravida. Mi sit amet mauris commodo quis imperdiet massa. Ullamcorper velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Ipsum faucibus vitae aliquet nec ullamcorper sit amet. Massa tincidunt dui ut ornare lectus sit.
1057 LOCATION:Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et. Ac tincidunt vitae semper quis lectus nulla at volutpat. Egestas congue quisque egestas diam in arcu. Cras adipiscing enim eu turpis. Ullamcorper eget nulla facilisi etiam dignissim diam quis. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor posuere. Pulvinar mattis nunc sed blandit libero volutpat sed. Enim nec dui nunc mattis enim ut tellus elementum. Vulputate dignissim suspendisse in est ante in nibh mauris. Malesuada pellentesque elit eget gravida cum. Amet aliquam id diam maecenas ultricies. Aliquam sem fringilla ut morbi tincidunt augue interdum velit. Nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur. Adipiscing elit duis tristique sollicitudin nibh sit. Pulvinar proin gravida hendrerit lectus. Sit amet justo donec enim diam.
1059 const event = parse(vevent) as VcalVeventComponent;
1060 expect(croppedUID.length === MAX_CHARS_API.UID);
1063 vcalVeventComponent: event,
1064 hasXWrTimezone: false,
1065 guessTzid: 'Asia/Seoul',
1066 componentIdentifiers,
1069 ...omit(event, ['dtend']),
1070 uid: { value: croppedUID },
1071 summary: { value: truncate(loremIpsum, MAX_CHARS_API.TITLE) },
1072 location: { value: truncate(loremIpsum, MAX_CHARS_API.LOCATION) },
1073 description: { value: truncate(loremIpsum, MAX_CHARS_API.EVENT_DESCRIPTION) },
1074 sequence: { value: 0 },
1078 it('should accept events with colors', () => {
1079 const vevent = `BEGIN:VEVENT
1081 DTSTAMP:19980309T231000Z
1083 DTSTART;VALUE=DATE:20080101
1084 DTEND;VALUE=DATE:20080102
1087 const event = parse(vevent) as VcalVeventComponent;
1091 vcalVeventComponent: event,
1092 hasXWrTimezone: false,
1093 guessTzid: 'Asia/Seoul',
1094 canImportEventColor: true,
1095 componentIdentifiers,
1098 ...omit(event, ['dtend']),
1099 uid: { value: 'uid@proton.me' },
1100 sequence: { value: 0 },
1101 color: { value: ACCENT_COLORS_MAP.enzian.color },
1105 it('should ignore colors when user is free', () => {
1106 const vevent = `BEGIN:VEVENT
1108 DTSTAMP:19980309T231000Z
1110 DTSTART;VALUE=DATE:20080101
1111 DTEND;VALUE=DATE:20080102
1114 const event = parse(vevent) as VcalVeventComponent;
1118 vcalVeventComponent: event,
1119 hasXWrTimezone: false,
1120 guessTzid: 'Asia/Seoul',
1121 canImportEventColor: false,
1122 componentIdentifiers,
1125 ...omit(event, ['dtend', 'color']),
1126 uid: { value: 'uid@proton.me' },
1127 sequence: { value: 0 },
1131 it('should ignore colors when the format is invalid', () => {
1132 const vevent = `BEGIN:VEVENT
1134 DTSTAMP:19980309T231000Z
1136 DTSTART;VALUE=DATE:20080101
1137 DTEND;VALUE=DATE:20080102
1140 const event = parse(vevent) as VcalVeventComponent;
1144 vcalVeventComponent: event,
1145 hasXWrTimezone: false,
1146 guessTzid: 'Asia/Seoul',
1147 canImportEventColor: true,
1148 componentIdentifiers,
1151 ...omit(event, ['dtend', 'color']),
1152 uid: { value: 'uid@proton.me' },
1153 sequence: { value: 0 },
1158 describe('extractSupportedEvent', () => {
1159 it('should add a uid if the event has none', async () => {
1162 DTSTAMP:19980309T231000Z
1163 DTSTART;TZID=Europe/Brussels:20021231T203000
1164 DTEND;TZID=Europe/Brussels:20030101T003000
1165 LOCATION:1CP Conference Room 4350
1167 const tzid = 'Europe/Brussels';
1168 const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
1169 const supportedEvent = await extractSupportedEvent({
1170 method: ICAL_METHOD.PUBLISH,
1171 vcalComponent: event,
1173 hasXWrTimezone: true,
1174 guessTzid: 'Europe/Zurich',
1177 expect(supportedEvent).toEqual({
1178 component: 'vevent',
1179 uid: { value: 'sha1-uid-0ff30d1f26a94abe627d9f715db16714b01be84c' },
1181 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1184 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1185 parameters: { tzid: 'Europe/Brussels' },
1188 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1189 parameters: { tzid: 'Europe/Brussels' },
1191 location: { value: '1CP Conference Room 4350' },
1192 sequence: { value: 0 },
1196 it('should override the uid if the event is an invitation, preserve it in the new uid, and drop recurrence-id', async () => {
1200 DTSTAMP:19980309T231000Z
1201 DTSTART;TZID=Europe/Brussels:20021231T203000
1202 DTEND;TZID=Europe/Brussels:20030101T003000
1203 RECURRENCE-ID:20110618T150000Z
1204 LOCATION:1CP Conference Room 4350
1206 const tzid = 'Europe/Brussels';
1207 const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
1208 const supportedEvent = await extractSupportedEvent({
1209 method: ICAL_METHOD.REQUEST,
1210 vcalComponent: event,
1212 hasXWrTimezone: true,
1213 guessTzid: 'Europe/Zurich',
1216 expect(supportedEvent).toEqual({
1217 component: 'vevent',
1218 uid: { value: 'original-uid-lalalala-sha1-uid-d39ba53e577d2eae6ba0baf8539e1fa468fbeabb' },
1220 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1223 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1224 parameters: { tzid: 'Europe/Brussels' },
1227 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1228 parameters: { tzid: 'Europe/Brussels' },
1230 location: { value: '1CP Conference Room 4350' },
1231 sequence: { value: 0 },
1235 it('should drop the recurrence id if we overrode the uid', async () => {
1238 DTSTAMP:19980309T231000Z
1239 DTSTART;TZID=Europe/Brussels:20021231T203000
1240 DTEND;TZID=Europe/Brussels:20030101T003000
1241 RECURRENCE-ID:20110618T150000Z
1242 LOCATION:1CP Conference Room 4350
1244 const tzid = 'Europe/Brussels';
1245 const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
1246 const supportedEvent = await extractSupportedEvent({
1247 method: ICAL_METHOD.PUBLISH,
1248 vcalComponent: event,
1250 hasXWrTimezone: true,
1251 guessTzid: 'Europe/Zurich',
1254 expect(supportedEvent).toEqual({
1255 component: 'vevent',
1256 uid: { value: 'sha1-uid-ab36432982bccb6dad294500ece330c5829f93ad' },
1258 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1261 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1262 parameters: { tzid: 'Europe/Brussels' },
1265 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1266 parameters: { tzid: 'Europe/Brussels' },
1268 location: { value: '1CP Conference Room 4350' },
1269 sequence: { value: 0 },
1273 it('should fix bad DTSTAMPs', async () => {
1274 const vevent = `BEGIN:VEVENT
1275 DTSTART;TZID=America/New_York:20221012T171500
1276 DTEND;TZID=America/New_York:20221012T182500
1277 DTSTAMP;TZID=America/New_York:20221007T151646
1278 UID:11353R6@voltigeursbourget.com
1281 const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
1282 const supportedEvent = await extractSupportedEvent({
1283 method: ICAL_METHOD.PUBLISH,
1284 vcalComponent: event,
1285 hasXWrTimezone: false,
1286 guessTzid: 'Europe/Zurich',
1289 expect(supportedEvent).toEqual({
1290 component: 'vevent',
1291 uid: { value: '11353R6@voltigeursbourget.com' },
1293 value: { year: 2022, month: 10, day: 7, hours: 19, minutes: 16, seconds: 46, isUTC: true },
1296 value: { year: 2022, month: 10, day: 12, hours: 17, minutes: 15, seconds: 0, isUTC: false },
1297 parameters: { tzid: 'America/New_York' },
1300 value: { year: 2022, month: 10, day: 12, hours: 18, minutes: 25, seconds: 0, isUTC: false },
1301 parameters: { tzid: 'America/New_York' },
1303 sequence: { value: 0 },
1307 it('should generate DTSTAMP if not present', async () => {
1308 const vevent = `BEGIN:VEVENT
1309 DTSTART;TZID=America/New_York:20221012T171500
1310 DTEND;TZID=America/New_York:20221012T182500
1311 UID:11353R6@voltigeursbourget.com
1314 const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
1315 const supportedEvent = await extractSupportedEvent({
1316 method: ICAL_METHOD.PUBLISH,
1317 vcalComponent: event,
1318 hasXWrTimezone: false,
1319 guessTzid: 'Europe/Zurich',
1322 expect(Object.keys(supportedEvent)).toContain('dtstamp');
1325 it('should not import alarms for invitations', async () => {
1326 const vevent = `BEGIN:VEVENT
1328 DTSTAMP:19980309T231000Z
1329 DTSTART;TZID=Europe/Brussels:20021231T203000
1330 DTEND;TZID=Europe/Brussels:20030101T003000
1331 LOCATION:1CP Conference Room 4350
1341 const tzid = 'Europe/Brussels';
1342 const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
1343 const supportedEvent = await extractSupportedEvent({
1344 method: ICAL_METHOD.REQUEST,
1345 vcalComponent: event,
1347 hasXWrTimezone: true,
1348 guessTzid: 'Europe/Zurich',
1351 expect(supportedEvent).toEqual({
1352 component: 'vevent',
1353 uid: { value: 'original-uid-test-event-sha1-uid-a9c06d78f4755f736bfd046b3deb3d76f99ab285' },
1355 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1358 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1359 parameters: { tzid: 'Europe/Brussels' },
1362 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1363 parameters: { tzid: 'Europe/Brussels' },
1365 location: { value: '1CP Conference Room 4350' },
1366 sequence: { value: 0 },
1370 it('should serialize event without potential errors to generate hash uid', async () => {
1371 // The alarm trigger is invalid in this vevent, so the VALARM component should be removed during event serialization prior to generate hash uid
1372 const veventIcs = `BEGIN:VEVENT
1373 DESCRIPTION;LANGUAGE=en-US:\n\n\n
1374 UID:040000008200E00074C5B7101A82E00800000000B058B6A2A081D901000000000000000
1375 0100000004A031FE80ACD7C418A7A1762749176F121
1376 SUMMARY:Calendar test
1377 DTSTART;TZID=Eastern Standard Time:20230513T123000
1378 DTEND;TZID=Eastern Standard Time:20230513T130000
1381 DTSTAMP:20230508T153204Z
1385 LOCATION;LANGUAGE=en-US:
1387 DESCRIPTION:REMINDER
1388 TRIGGER;RELATED=START:P
1393 const event = parseVcalendarWithRecoveryAndMaybeErrors(veventIcs);
1394 const supportedEvent = await extractSupportedEvent({
1395 method: ICAL_METHOD.REQUEST,
1396 vcalComponent: event,
1397 hasXWrTimezone: false,
1398 guessTzid: 'America/New_York',
1402 expect(supportedEvent).toEqual({
1403 component: 'vevent',
1405 value: 'original-uid-040000008200E00074C5B7101A82E00800000000B058B6A2A081D901000000000000000 0100000004A031FE80ACD7C418A7A1762749176F121-sha1-uid-f286aba29df21425cbf3bcec44de9b7fc5e93ce5',
1408 value: { year: 2023, month: 5, day: 8, hours: 15, minutes: 32, seconds: 4, isUTC: true },
1411 value: { year: 2023, month: 5, day: 13, hours: 12, minutes: 30, seconds: 0, isUTC: false },
1412 parameters: { tzid: 'America/New_York' },
1414 sequence: { value: 0 },
1415 summary: { value: 'Calendar test' },
1417 value: { year: 2023, month: 5, day: 13, hours: 13, minutes: 0, seconds: 0, isUTC: false },
1418 parameters: { tzid: 'America/New_York' },
1424 describe('getSupportedEventsOrErrors', () => {
1425 describe('should guess a time zone to localize floating dates', () => {
1426 const generateVcalSetup = (
1427 primaryTimezone = 'Asia/Seoul',
1429 vtimezonesTzids: string[] = []
1431 const xWrTimezoneString = xWrTimezone ? `X-WR-TIMEZONE:${xWrTimezone}` : '';
1432 const vtimezonesString = vtimezonesTzids
1434 (tzid) => `BEGIN:VTIMEZONE
1439 const vcal = `BEGIN:VCALENDAR
1440 PRODID:Proton Calendar
1444 ${xWrTimezoneString}
1448 DTSTAMP:19980309T231000Z
1449 DTSTART:20021231T203000
1450 DTEND:20030101T003000
1451 SUMMARY:Floating date-time
1456 calscale: calscaleProperty,
1457 'x-wr-timezone': xWrTimezoneProperty,
1458 method: methodProperty,
1459 } = parse(vcal) as VcalVcalendar;
1462 calscale: calscaleProperty?.value,
1463 xWrTimezone: xWrTimezoneProperty?.value,
1464 method: getIcalMethod(methodProperty) || ICAL_METHOD.PUBLISH,
1468 const localizedVevent = (tzid: string) => ({
1469 component: 'vevent',
1470 uid: { value: 'test-uid' },
1472 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1475 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1476 parameters: { tzid },
1479 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1480 parameters: { tzid },
1482 summary: { value: 'Floating date-time' },
1483 sequence: { value: 0 },
1486 it('when there is both x-wr-timezone and single vtimezone (use x-wr-timezone)', async () => {
1487 const [timeZoneIgnoredError, supportedEvent] = await getSupportedEventsOrErrors(
1488 generateVcalSetup('Asia/Seoul', 'Europe/Brussels', ['America/New_York'])
1490 expect(timeZoneIgnoredError).toEqual(new Error('Time zone component ignored') as ImportEventError);
1491 expect(supportedEvent).toEqual(localizedVevent('Europe/Brussels'));
1494 it('when there is a single vtimezone and no x-wr-timezone', async () => {
1495 const [timeZoneIgnoredError, supportedEvent] = await getSupportedEventsOrErrors(
1496 generateVcalSetup('Asia/Seoul', '', ['Europe/Vilnius'])
1498 expect(timeZoneIgnoredError).toEqual(new Error('Time zone component ignored') as ImportEventError);
1499 expect(supportedEvent).toEqual(localizedVevent('Europe/Vilnius'));
1502 it('when there is a single vtimezone and x-wr-timezone is not supported', async () => {
1503 const [timeZoneIgnoredError, supportedEvent] = await getSupportedEventsOrErrors(
1504 generateVcalSetup('Asia/Seoul', 'Moon/Tranquility', ['Europe/Vilnius'])
1506 expect(timeZoneIgnoredError).toEqual(new Error('Time zone component ignored') as ImportEventError);
1507 expect(supportedEvent).toEqual(new Error('Calendar time zone not supported') as ImportEventError);
1510 it('when there is no vtimezone nor x-wr-timezone (fall back to primary time zone)', async () => {
1511 const [supportedEvent] = await getSupportedEventsOrErrors(generateVcalSetup('Asia/Seoul'));
1512 expect(supportedEvent).toEqual({
1513 component: 'vevent',
1514 uid: { value: 'test-uid' },
1536 parameters: { tzid: 'Asia/Seoul' },
1538 summary: { value: 'Floating date-time' },
1539 sequence: { value: 0 },
1550 parameters: { tzid: 'Asia/Seoul' },
1555 it('when there is no x-wr-timezone and more than one vtimezone (fall back to primary time zone)', async () => {
1556 const [timeZoneIgnoredError1, timeZoneIgnoredError2, supportedEvent] = await getSupportedEventsOrErrors(
1557 generateVcalSetup('Asia/Seoul', '', ['Europe/Vilnius', 'America/New_York'])
1559 expect(timeZoneIgnoredError1).toEqual(new Error('Time zone component ignored') as ImportEventError);
1560 expect(timeZoneIgnoredError2).toEqual(new Error('Time zone component ignored') as ImportEventError);
1561 expect(supportedEvent).toEqual({
1562 component: 'vevent',
1563 uid: { value: 'test-uid' },
1585 parameters: { tzid: 'Asia/Seoul' },
1587 summary: { value: 'Floating date-time' },
1588 sequence: { value: 0 },
1599 parameters: { tzid: 'Asia/Seoul' },
1606 describe('parseIcs', () => {
1607 it('should parse an ics with no method or version', async () => {
1608 const icsString = `BEGIN:VCALENDAR
1609 PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 4.0//EN
1614 DTSTART:20200101T000000
1615 RRULE:FREQ=YEARLY;BYDAY=1WE;BYMONTH=1
1622 ATTENDEE;CN=Ham Burger;RSVP=TRUE:mailto:hamburgerc@pm.me
1624 DESCRIPTION:\\nHi there\\,\\nThis is a very weird description
1625 with tabs and \\n\t\t\tline
1626 jumps\\n\t\t\ta few\\n\t\t\tjumps\\n\t\t\tyaaay
1627 DTEND:20210430T203000
1628 DTSTAMP:20210429T171519Z
1629 DTSTART:20210430T183000
1630 ORGANIZER;CN=:mailto:belzebu@evil.com
1632 SUMMARY:Another one bites the dust
1633 UID:81383944-3775313411-20210429T131519@howdyhow.com
1636 DESCRIPTION:Reminder
1641 const ics = new File([new Blob([icsString])], 'invite.ics');
1642 const expectedVtimezone = {
1643 component: 'vtimezone',
1646 component: 'standard',
1648 value: { year: 2020, month: 1, day: 1, hours: 0, minutes: 0, seconds: 0, isUTC: false },
1650 rrule: { value: { freq: 'YEARLY', byday: '1WE', bymonth: 1 } },
1651 tzname: [{ value: 'UTC' }],
1652 tzoffsetfrom: [{ value: '+00:00' }],
1653 tzoffsetto: [{ value: '+00:00' }],
1656 tzid: { value: 'UTC' },
1657 'x-lic-location': [{ value: 'UTC' }],
1658 } as VcalVtimezoneComponent;
1659 const expectedVevent = {
1660 component: 'vevent',
1661 uid: { value: '81383944-3775313411-20210429T131519@howdyhow.com' },
1662 class: { value: 'PUBLIC' },
1664 value: { year: 2021, month: 4, day: 29, hours: 17, minutes: 15, seconds: 19, isUTC: true },
1667 value: { year: 2021, month: 4, day: 30, hours: 18, minutes: 30, seconds: 0, isUTC: false },
1670 value: { year: 2021, month: 4, day: 30, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1673 value: 'Another one bites the dust',
1676 value: '\nHi there,\nThis is a very weird description with tabs and \n\t\t\tlinejumps\n\t\t\ta few\n\t\t\tjumps\n\t\t\tyaaay',
1678 sequence: { value: 0 },
1680 value: 'mailto:belzebu@evil.com',
1681 parameters: { cn: '' },
1685 value: 'mailto:hamburgerc@pm.me',
1686 parameters: { cn: 'Ham Burger', rsvp: 'TRUE' },
1691 component: 'valarm',
1692 action: { value: 'DISPLAY' },
1693 description: { value: 'Reminder' },
1694 trigger: { value: { weeks: 0, days: 0, hours: 1, minutes: 0, seconds: 0, isNegative: true } },
1698 expect(await parseIcs(ics)).toEqual({
1699 method: ICAL_METHOD.PUBLISH,
1700 calscale: ICAL_CALSCALE.GREGORIAN,
1701 xWrTimezone: undefined,
1702 components: [expectedVtimezone, expectedVevent],
1703 prodId: '-//github.com/rianjs/ical.net//NONSGML ical.net 4.0//EN',
1704 hashedIcs: '0bfab119cf9b05996ee91ecd21bc47d90214a98a',
1708 it('should parse an ics with errors in some events', async () => {
1709 const icsString = `BEGIN:VCALENDAR
1710 PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 4.0//EN
1715 DTSTART:20200101T000000
1716 RRULE:FREQ=YEARLY;BYDAY=1WE;BYMONTH=1
1723 ATTENDEE;CN=Ham Burger;RSVP=TRUE:mailto:hamburgerc@pm.me
1725 DESCRIPTION:\\nHi there\\,\\nThis is a very weird description
1726 with tabs and \\n\t\t\tline
1727 jumps\\n\t\t\ta few\\n\t\t\tjumps\\n\t\t\tyaaay
1728 DTEND:20210430T203000
1729 DTSTAMP:20210429T171519Z
1730 DTSTART:20210430T183000
1731 ORGANIZER;CN=:mailto:belzebu@evil.com
1733 SUMMARY:Another one bites the dust
1734 UID:81383944-3775313411-20210429T131519@howdyhow.com
1737 DESCRIPTION:Reminder
1742 DESCRIPTION:We can't recover from the bad DTSTART
1743 DTSTAMP:20210429T171519Z
1744 DTSTART:2021.04.30T18:35:00
1746 UID:oh-no@howdyhow.com
1749 const ics = new File([new Blob([icsString])], 'invite.ics');
1750 const expectedVtimezone = {
1751 component: 'vtimezone',
1754 component: 'standard',
1756 value: { year: 2020, month: 1, day: 1, hours: 0, minutes: 0, seconds: 0, isUTC: false },
1758 rrule: { value: { freq: 'YEARLY', byday: '1WE', bymonth: 1 } },
1759 tzname: [{ value: 'UTC' }],
1760 tzoffsetfrom: [{ value: '+00:00' }],
1761 tzoffsetto: [{ value: '+00:00' }],
1764 tzid: { value: 'UTC' },
1765 'x-lic-location': [{ value: 'UTC' }],
1766 } as VcalVtimezoneComponent;
1767 const expectedVevent = {
1768 component: 'vevent',
1769 uid: { value: '81383944-3775313411-20210429T131519@howdyhow.com' },
1770 class: { value: 'PUBLIC' },
1772 value: { year: 2021, month: 4, day: 29, hours: 17, minutes: 15, seconds: 19, isUTC: true },
1775 value: { year: 2021, month: 4, day: 30, hours: 18, minutes: 30, seconds: 0, isUTC: false },
1778 value: { year: 2021, month: 4, day: 30, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1781 value: 'Another one bites the dust',
1784 value: '\nHi there,\nThis is a very weird description with tabs and \n\t\t\tlinejumps\n\t\t\ta few\n\t\t\tjumps\n\t\t\tyaaay',
1786 sequence: { value: 0 },
1788 value: 'mailto:belzebu@evil.com',
1789 parameters: { cn: '' },
1793 value: 'mailto:hamburgerc@pm.me',
1794 parameters: { cn: 'Ham Burger', rsvp: 'TRUE' },
1799 component: 'valarm',
1800 action: { value: 'DISPLAY' },
1801 description: { value: 'Reminder' },
1802 trigger: { value: { weeks: 0, days: 0, hours: 1, minutes: 0, seconds: 0, isNegative: true } },
1807 const { method, calscale, xWrTimezone, components } = await parseIcs(ics);
1808 expect(method).toEqual(ICAL_METHOD.PUBLISH);
1809 expect(calscale).toEqual(ICAL_CALSCALE.GREGORIAN);
1810 expect(xWrTimezone).toBe(undefined);
1811 expect(components[0]).toEqual(expectedVtimezone);
1812 expect(components[1]).toEqual(expectedVevent);
1813 expect((components[2] as VcalErrorComponent).error).toBeDefined();
1814 expect((components[2] as VcalErrorComponent).icalComponent).toBeDefined();
1817 it('should trim calscale and method', async () => {
1818 const icsString = `BEGIN:VCALENDAR
1819 PRODID:-//Company Inc//Product Application//EN
1824 DTSTART:20220530T143000Z
1825 DTEND:20220530T153000Z
1826 DTSTAMP:20220528T075010Z
1828 UID:6c56b58d-488a-41bf-90a2-9b0d555c3780
1829 CREATED:20220528T075010Z
1830 X-ALT-DESC;FMTTYPE=text/html:
1831 LAST-MODIFIED:20220528T075010Z
1832 LOCATION:Sunny danny 8200 Aarhus N
1835 SUMMARY:Optical transform
1846 const ics = new File([new Blob([icsString])], 'invite.ics');
1847 const parsedIcs = await parseIcs(ics);
1848 expect(parsedIcs.calscale).toEqual(ICAL_CALSCALE.GREGORIAN);
1849 expect(parsedIcs.method).toEqual(ICAL_METHOD.PUBLISH);
1852 it('should not recognize unknown calscales', async () => {
1853 const icsString = `BEGIN:VCALENDAR
1854 PRODID:-//Company Inc//Product Application//EN
1855 CALSCALE: GREGORIANO
1858 DTSTART:20220530T143000Z
1859 DTEND:20220530T153000Z
1860 DTSTAMP:20220528T075010Z
1862 UID:6c56b58d-488a-41bf-90a2-9b0d555c3780
1863 CREATED:20220528T075010Z
1864 X-ALT-DESC;FMTTYPE=text/html:
1865 LAST-MODIFIED:20220528T075010Z
1866 LOCATION:Sunny danny 8200 Aarhus N
1869 SUMMARY:Optical transform
1880 const ics = new File([new Blob([icsString])], 'invite.ics');
1881 const parsedIcs = await parseIcs(ics);
1882 expect(parsedIcs.calscale).toEqual(undefined);
1885 it('should throw for unknown methods', async () => {
1886 const icsString = `BEGIN:VCALENDAR
1887 PRODID:-//Company Inc//Product Application//EN
1891 DTSTART:20220530T143000Z
1892 DTEND:20220530T153000Z
1893 DTSTAMP:20220528T075010Z
1895 UID:6c56b58d-488a-41bf-90a2-9b0d555c3780
1896 CREATED:20220528T075010Z
1897 X-ALT-DESC;FMTTYPE=text/html:
1898 LAST-MODIFIED:20220528T075010Z
1899 LOCATION:Sunny danny 8200 Aarhus N
1902 SUMMARY:Optical transform
1913 const ics = new File([new Blob([icsString])], 'invite.ics');
1914 await expectAsync(parseIcs(ics)).toBeRejectedWithError(
1915 'Your file "invite.ics" has an invalid method and cannot be imported.'