Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / test / calendar / import.spec.ts
blob451488b080f93a377faa71cf530d309cf3687ed5
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';
9 import {
10     extractSupportedEvent,
11     getComponentIdentifier,
12     getSupportedEventsOrErrors,
13     parseIcs,
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';
18 import type {
19     VcalDateTimeProperty,
20     VcalErrorComponent,
21     VcalVcalendar,
22     VcalVeventComponent,
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('');
29     });
31     it('should return the tzid for a VTIMEZONE', () => {
32         const vtimezone = `BEGIN:VTIMEZONE
33 TZID:Europe/Vilnius
34 BEGIN:DAYLIGHT
35 TZOFFSETFROM:+0200
36 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
37 DTSTART:20030330T030000
38 TZNAME:EEST
39 TZOFFSETTO:+0300
40 END:DAYLIGHT
41 BEGIN:STANDARD
42 TZOFFSETFROM:+0300
43 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
44 DTSTART:20031026T040000
45 TZNAME:EET
46 TZOFFSETTO:+0200
47 END:STANDARD
48 END:VTIMEZONE`;
49         const timezone = parse(vtimezone) as VcalVtimezoneComponent;
50         expect(getComponentIdentifier(timezone)).toEqual('Europe/Vilnius');
51     });
53     it('should return the uid for an event with a normal uid', () => {
54         const vevent = `BEGIN:VEVENT
55 DTSTAMP:19980309T231000Z
56 UID:test-event
57 DTSTART;TZID=America/New_York:20690312T083000
58 DTEND;TZID=America/New_York:20690312T093000
59 LOCATION:1CP Conference Room 4350
60 END:VEVENT`;
61         const event = parse(vevent) as VcalVeventComponent;
62         expect(getComponentIdentifier(event)).toEqual('test-event');
63     });
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
72 END:VEVENT`;
73         const event = parse(vevent) as VcalVeventComponent;
74         expect(getComponentIdentifier(event)).toEqual('stmyce9lb3ef@domain.com');
75     });
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
84 END:VEVENT`;
85         const event = parse(vevent) as VcalVeventComponent;
86         expect(getComponentIdentifier(event)).toEqual('stmyce9lb3ef@domain.com');
87     });
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
95 SUMMARY:Test event
96 LOCATION:1CP Conference Room 4350
97 END:VEVENT`;
98         const event = parse(vevent) as VcalVeventComponent;
99         expect(getComponentIdentifier(event)).toEqual('Test event');
100     });
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
108 END:VEVENT`;
109         const event = parse(vevent) as VcalVeventComponent;
110         expect(getComponentIdentifier(event, { locale: enUS })).toEqual('Mar 12, 2069, 8:30:00 AM');
111     });
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
118 END:VEVENT`;
119         const event = parse(vevent) as VcalVeventComponent;
120         expect(getComponentIdentifier(event, { locale: enUS })).toEqual('Mar 12, 2069');
121     });
124 describe('getSupportedEvent', () => {
125     // dummy component identifiers
126     const componentIdentifiers = {
127         component: 'vevent',
128         componentId: '',
129         prodId: '',
130         domain: '',
131     };
132     it('should catch events with start time before 1970', () => {
133         const vevent = `BEGIN:VEVENT
134 DTSTAMP:19980309T231000Z
135 UID:test-event
136 DTSTART;TZID=America/New_York:19690312T083000
137 DTEND;TZID=/America/New_York:19690312T093000
138 LOCATION:1CP Conference Room 4350
139 END:VEVENT`;
140         const event = parse(vevent) as VcalVeventComponent;
141         expect(() =>
142             getSupportedEvent({
143                 vcalVeventComponent: event,
144                 hasXWrTimezone: false,
145                 guessTzid: 'Asia/Seoul',
146                 componentIdentifiers,
147             })
148         ).toThrowError('Start time out of bounds');
149     });
151     it('should catch events with start time after 2038', () => {
152         const vevent = `BEGIN:VEVENT
153 DTSTAMP:19980309T231000Z
154 UID:test-event
155 DTSTART;VALUE=DATE:20380101
156 DTEND;VALUE=DATE:20380102
157 LOCATION:1CP Conference Room 4350
158 END:VEVENT`;
159         const event = parse(vevent) as VcalVeventComponent;
160         expect(() =>
161             getSupportedEvent({
162                 vcalVeventComponent: event,
163                 hasXWrTimezone: false,
164                 guessTzid: 'Asia/Seoul',
165                 componentIdentifiers,
166             })
167         ).toThrowError('Start time out of bounds');
168     });
170     it('should catch malformed all-day events', () => {
171         const vevent = `BEGIN:VEVENT
172 DTSTAMP:19980309T231000Z
173 UID:test-event
174 DTSTART;VALUE=DATE:20180101
175 DTEND:20191231T203000Z
176 LOCATION:1CP Conference Room 4350
177 END:VEVENT`;
178         const event = parse(vevent) as VcalVeventComponent;
179         expect(() =>
180             getSupportedEvent({
181                 vcalVeventComponent: event,
182                 hasXWrTimezone: false,
183                 guessTzid: 'Asia/Seoul',
184                 componentIdentifiers,
185             })
186         ).toThrowError('Malformed all-day event');
187     });
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
192 UID:test-event
193 DTSTART;TZID=America/New_York:20371231T203000
194 DTEND;TZID=America/New_York:20380101T003000
195 LOCATION:1CP Conference Room 4350
196 END:VEVENT`;
197         const event = parse(vevent) as VcalVeventComponent;
198         expect(() =>
199             getSupportedEvent({
200                 vcalVeventComponent: event,
201                 hasXWrTimezone: false,
202                 guessTzid: 'Asia/Seoul',
203                 componentIdentifiers,
204             })
205         ).toThrowError('Start time out of bounds');
206     });
208     it('should accept events with sequence', () => {
209         const vevent = `BEGIN:VEVENT
210 DTSTAMP:19980309T231000Z
211 UID:test-event
212 DTSTART;TZID=America/New_York:20020312T083000
213 DTEND;TZID=America/New_York:20020312T082959
214 SEQUENCE:11
215 END:VEVENT`;
216         const event = parse(vevent) as VcalVeventComponent;
217         expect(
218             getSupportedEvent({
219                 vcalVeventComponent: event,
220                 hasXWrTimezone: false,
221                 guessTzid: 'Asia/Seoul',
222                 componentIdentifiers,
223             })
224         ).toEqual({
225             component: 'vevent',
226             uid: { value: 'test-event' },
227             dtstamp: {
228                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
229             },
230             dtstart: {
231                 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
232                 parameters: { tzid: 'America/New_York' },
233             },
234             sequence: { value: 11 },
235         });
236     });
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
242 UID:test-event
243 DTSTART;TZID=America/New_York:20020312T083000
244 DTEND;TZID=America/New_York:20020312T082959
245 SEQUENCE:${sequenceOutOfBounds}
246 END:VEVENT`;
247         const event = parse(vevent) as VcalVeventComponent;
248         expect(
249             getSupportedEvent({
250                 vcalVeventComponent: event,
251                 hasXWrTimezone: false,
252                 guessTzid: 'Asia/Seoul',
253                 componentIdentifiers,
254             })
255         ).toEqual({
256             component: 'vevent',
257             uid: { value: 'test-event' },
258             dtstamp: {
259                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
260             },
261             dtstart: {
262                 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
263                 parameters: { tzid: 'America/New_York' },
264             },
265             sequence: { value: 3 },
266         });
267     });
269     it('should accept (and re-format) events with negative duration and negative sequence', () => {
270         const vevent = `BEGIN:VEVENT
271 DTSTAMP:19980309T231000Z
272 UID:test-event
273 DTSTART;TZID=/America/New_York:20020312T083000
274 DTEND;TZID=/America/New_York:20020312T082959
275 LOCATION:1CP Conference Room 4350
276 SEQUENCE:-1
277 END:VEVENT`;
278         const event = parse(vevent) as VcalVeventComponent;
279         expect(
280             getSupportedEvent({
281                 vcalVeventComponent: event,
282                 hasXWrTimezone: false,
283                 guessTzid: 'Asia/Seoul',
284                 componentIdentifiers,
285             })
286         ).toEqual({
287             component: 'vevent',
288             uid: { value: 'test-event' },
289             dtstamp: {
290                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
291             },
292             dtstart: {
293                 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
294                 parameters: { tzid: 'America/New_York' },
295             },
296             location: { value: '1CP Conference Room 4350' },
297             sequence: { value: 0 },
298         });
299     });
301     it('should drop DTEND for part-day events with zero duration', () => {
302         const vevent = `BEGIN:VEVENT
303 DTSTAMP:19980309T231000Z
304 UID:test-event
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
308 END:VEVENT`;
309         const event = parse(vevent) as VcalVeventComponent;
310         expect(
311             getSupportedEvent({
312                 vcalVeventComponent: event,
313                 hasXWrTimezone: false,
314                 guessTzid: 'Asia/Seoul',
315                 componentIdentifiers,
316             })
317         ).toEqual({
318             component: 'vevent',
319             uid: { value: 'test-event' },
320             dtstamp: {
321                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
322             },
323             dtstart: {
324                 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
325                 parameters: { tzid: 'America/New_York' },
326             },
327             location: { value: '1CP Conference Room 4350' },
328             sequence: { value: 0 },
329         });
330     });
332     it('should drop DTEND for all-day events with zero duration', () => {
333         const vevent = `BEGIN:VEVENT
334 DTSTAMP:19980309T231000Z
335 UID:test-event
336 DTSTART;VALUE=DATE:20020312
337 DTEND;VALUE=DATE:20020312
338 LOCATION:1CP Conference Room 4350
339 END:VEVENT`;
340         const event = parse(vevent) as VcalVeventComponent;
341         expect(
342             getSupportedEvent({
343                 vcalVeventComponent: event,
344                 hasXWrTimezone: false,
345                 guessTzid: 'Asia/Seoul',
346                 componentIdentifiers,
347             })
348         ).toEqual({
349             component: 'vevent',
350             uid: { value: 'test-event' },
351             dtstamp: {
352                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
353             },
354             dtstart: {
355                 value: { year: 2002, month: 3, day: 12 },
356                 parameters: { type: 'date' },
357             },
358             location: { value: '1CP Conference Room 4350' },
359             sequence: { value: 0 },
360         });
361     });
363     it('should modify events whose duration is specified to convert that into a dtend', () => {
364         const vevent = `BEGIN:VEVENT
365 DTSTAMP:19980309T231000Z
366 UID:test-event
367 DTSTART;TZID=America/New_York:20020312T083000
368 DURATION:PT1H0M0S
369 LOCATION:1CP Conference Room 4350
370 END:VEVENT`;
371         const event = parse(vevent) as VcalVeventComponent;
372         expect(
373             getSupportedEvent({
374                 vcalVeventComponent: event,
375                 hasXWrTimezone: false,
376                 guessTzid: 'Asia/Seoul',
377                 componentIdentifiers,
378             })
379         ).toEqual({
380             component: 'vevent',
381             uid: { value: 'test-event' },
382             dtstamp: { value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true } },
383             dtstart: {
384                 value: { year: 2002, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
385                 parameters: { tzid: 'America/New_York' },
386             },
387             location: { value: '1CP Conference Room 4350' },
388             sequence: { value: 0 },
389             dtend: {
390                 value: { year: 2002, month: 3, day: 12, hours: 9, minutes: 30, seconds: 0, isUTC: false },
391                 parameters: { tzid: 'America/New_York' },
392             },
393         });
394     });
396     it('should filter out notifications out of bounds', () => {
397         const vevent = `BEGIN:VEVENT
398 DTSTAMP:19980309T231000Z
399 UID:test-event
400 DTSTART;TZID=America/New_York:19990312T083000
401 DTEND;TZID=America/New_York:19990312T093000
402 BEGIN:VALARM
403 ACTION:DISPLAY
404 TRIGGER:-PT10000M
405 END:VALARM
406 LOCATION:1CP Conference Room 4350
407 END:VEVENT`;
408         const event = parse(vevent) as VcalVeventComponent;
409         expect(
410             getSupportedEvent({
411                 vcalVeventComponent: event,
412                 hasXWrTimezone: false,
413                 guessTzid: 'Asia/Seoul',
414                 componentIdentifiers,
415             })
416         ).toEqual({
417             component: 'vevent',
418             uid: { value: 'test-event' },
419             dtstamp: {
420                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
421             },
422             dtstart: {
423                 value: { year: 1999, month: 3, day: 12, hours: 8, minutes: 30, seconds: 0, isUTC: false },
424                 parameters: { tzid: 'America/New_York' },
425             },
426             dtend: {
427                 value: { year: 1999, month: 3, day: 12, hours: 9, minutes: 30, seconds: 0, isUTC: false },
428                 parameters: { tzid: 'America/New_York' },
429             },
430             location: { value: '1CP Conference Room 4350' },
431             sequence: { value: 0 },
432         });
433     });
435     it('should normalize notifications', () => {
436         const vevent = `BEGIN:VEVENT
437 DTSTAMP:19980309T231000Z
438 UID:test-event
439 DTSTART;VALUE=DATE:19990312
440 DTEND;VALUE=DATE:19990313
441 BEGIN:VALARM
442 ACTION:DISPLAY
443 TRIGGER;VALUE=DATE-TIME:19960401T005545Z
444 END:VALARM
445 LOCATION:1CP Conference Room 4350
446 END:VEVENT`;
447         const event = parse(vevent) as VcalVeventComponent;
448         expect(
449             getSupportedEvent({
450                 vcalVeventComponent: event,
451                 hasXWrTimezone: false,
452                 guessTzid: 'Asia/Seoul',
453                 componentIdentifiers,
454             })
455         ).toEqual({
456             component: 'vevent',
457             uid: { value: 'test-event' },
458             dtstamp: {
459                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
460             },
461             dtstart: {
462                 value: { year: 1999, month: 3, day: 12 },
463                 parameters: { type: 'date' },
464             },
465             location: { value: '1CP Conference Room 4350' },
466             sequence: { value: 0 },
467             components: [
468                 {
469                     component: 'valarm',
470                     action: { value: 'DISPLAY' },
471                     trigger: {
472                         value: {
473                             weeks: 0,
474                             days: 1074,
475                             hours: 23,
476                             minutes: 4,
477                             seconds: 0,
478                             isNegative: true,
479                         },
480                     },
481                 },
482             ],
483         });
484     });
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
493 END:VEVENT`;
494         const eventNoOccurrenceOnDtstart = parse(veventNoOccurrenceOnDtstart) as VcalVeventComponent;
495         expect(() =>
496             getSupportedEvent({
497                 vcalVeventComponent: eventNoOccurrenceOnDtstart,
498                 hasXWrTimezone: false,
499                 guessTzid: 'Asia/Seoul',
500                 componentIdentifiers,
501             })
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
509 END:VEVENT`;
510         const eventWithByyeardayNotYearly = parse(veventWithByyeardayNotYearly) as VcalVeventComponent;
511         expect(() =>
512             getSupportedEvent({
513                 vcalVeventComponent: eventWithByyeardayNotYearly,
514                 hasXWrTimezone: false,
515                 guessTzid: 'Asia/Seoul',
516                 componentIdentifiers,
517             })
518         ).toThrowError('Malformed recurring event');
519     });
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
528 END:VEVENT`;
529         const event = parse(vevent) as VcalVeventComponent;
530         expect(() =>
531             getSupportedEvent({
532                 vcalVeventComponent: event,
533                 hasXWrTimezone: false,
534                 guessTzid: 'Asia/Seoul',
535                 componentIdentifiers,
536             })
537         ).toThrowError('Malformed recurring event');
538     });
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
547 END:VEVENT`;
548         const event = parse(vevent) as VcalVeventComponent;
549         expect(() =>
550             getSupportedEvent({
551                 vcalVeventComponent: event,
552                 hasXWrTimezone: false,
553                 guessTzid: 'Asia/Seoul',
554                 componentIdentifiers,
555             })
556         ).toThrowError('Malformed recurring event');
557     });
559     it('should catch recurring single edits', () => {
560         const vevent = `BEGIN:VEVENT
561 DTSTART;TZID=Europe/Vilnius:20200503T150000
562 DTEND;TZID=Europe/Vilnius:20200503T160000
563 RRULE:FREQ=DAILY
564 RECURRENCE-ID;TZID=Europe/Vilnius:20200505T150000
565 DTSTAMP:20200508T121218Z
566 UID:71hdoqnevmnq80hfaeadnq8d0v@google.com
567 END:VEVENT`;
568         const event = parse(vevent) as VcalVeventComponent;
569         expect(() =>
570             getSupportedEvent({
571                 vcalVeventComponent: event,
572                 hasXWrTimezone: false,
573                 guessTzid: 'Asia/Seoul',
574                 componentIdentifiers,
575             })
576         ).toThrowError('Edited event not supported');
577     });
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
590 DESCRIPTION:
591 LAST-MODIFIED:20130902T220905Z
592 LOCATION:Twinpigs - Å»ory\\, Katowicka 4
593 SEQUENCE:0
594 STATUS:CONFIRMED
595 SUMMARY:Scenka: napad na bank
596 TRANSP:OPAQUE
597 END:VEVENT`;
598         const event = parse(vevent) as VcalVeventComponent;
599         expect(() =>
600             getSupportedEvent({
601                 vcalVeventComponent: event,
602                 hasXWrTimezone: false,
603                 guessTzid: 'Asia/Seoul',
604                 componentIdentifiers,
605             })
606         ).toThrowError('Recurring event has no occurrences');
607     });
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
617 DESCRIPTION:
618 LAST-MODIFIED:20130902T220905Z
619 LOCATION:Twinpigs - Å»ory\\, Katowicka 4
620 SEQUENCE:0
621 STATUS:CONFIRMED
622 SUMMARY:Scenka: napad na bank
623 TRANSP:OPAQUE
624 END:VEVENT`;
625         const event = parse(vevent) as VcalVeventComponent;
626         expect(() =>
627             getSupportedEvent({
628                 vcalVeventComponent: event,
629                 hasXWrTimezone: false,
630                 guessTzid: 'Asia/Seoul',
631                 componentIdentifiers,
632             })
633         ).toThrowError('Recurring event has no occurrences');
634     });
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
644 DESCRIPTION:
645 LAST-MODIFIED:20130902T220905Z
646 LOCATION:Twinpigs - Å»ory\\, Katowicka 4
647 SEQUENCE:0
648 STATUS:CONFIRMED
649 SUMMARY:Scenka: napad na bank
650 TRANSP:OPAQUE
651 END:VEVENT`;
652         const event = parse(vevent) as VcalVeventComponent;
653         expect(() =>
654             getSupportedEvent({
655                 vcalVeventComponent: event,
656                 hasXWrTimezone: false,
657                 guessTzid: 'Asia/Seoul',
658                 componentIdentifiers,
659             })
660         ).toThrowError('Malformed recurring event');
661     });
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
670 END:VEVENT`;
671         const event = parse(vevent) as VcalVeventComponent;
672         expect(() =>
673             getSupportedEvent({
674                 vcalVeventComponent: event,
675                 hasXWrTimezone: false,
676                 guessTzid: 'Asia/Seoul',
677                 componentIdentifiers,
678             })
679         ).toThrowError('Recurring rule not supported');
680     });
682     it('should normalize exdate', () => {
683         const vevent = `BEGIN:VEVENT
684 DTSTAMP:19980309T231000Z
685 UID:test-event
686 DTSTART;TZID=W. Europe Standard Time:20021230T203000
687 RRULE:FREQ=DAILY
688 EXDATE;TZID=W. Europe Standard Time:20200610T170000,20200611T170000
689 END:VEVENT`;
690         const event = parse(vevent) as VcalVeventComponent;
691         expect(
692             getSupportedEvent({
693                 vcalVeventComponent: event,
694                 hasXWrTimezone: false,
695                 guessTzid: 'Asia/Seoul',
696                 componentIdentifiers,
697             })
698         ).toEqual({
699             component: 'vevent',
700             uid: { value: 'test-event' },
701             dtstamp: {
702                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
703             },
704             dtstart: {
705                 value: { year: 2002, month: 12, day: 30, hours: 20, minutes: 30, seconds: 0, isUTC: false },
706                 parameters: { tzid: 'Europe/Berlin' },
707             },
708             sequence: { value: 0 },
709             exdate: [
710                 {
711                     parameters: {
712                         tzid: 'Europe/Berlin',
713                     },
714                     value: {
715                         day: 10,
716                         hours: 17,
717                         isUTC: false,
718                         minutes: 0,
719                         month: 6,
720                         seconds: 0,
721                         year: 2020,
722                     },
723                 },
724                 {
725                     parameters: {
726                         tzid: 'Europe/Berlin',
727                     },
728                     value: {
729                         day: 11,
730                         hours: 17,
731                         isUTC: false,
732                         minutes: 0,
733                         month: 6,
734                         seconds: 0,
735                         year: 2020,
736                     },
737                 },
738             ],
739             rrule: {
740                 value: {
741                     freq: 'DAILY',
742                 },
743             },
744         });
745     });
747     it('should reformat some invalid exdates', () => {
748         const vevent = `BEGIN:VEVENT
749 DTSTAMP:19980309T231000Z
750 UID:test-event
751 DTSTART;VALUE=DATE:20021230
752 RRULE:FREQ=DAILY
753 EXDATE;TZID=W. Europe Standard Time:20200610T170000,20200611T170000
754 END:VEVENT`;
755         const event = parse(vevent) as VcalVeventComponent;
756         expect(
757             getSupportedEvent({
758                 vcalVeventComponent: event,
759                 hasXWrTimezone: false,
760                 guessTzid: 'Asia/Seoul',
761                 componentIdentifiers,
762             })
763         ).toEqual({
764             component: 'vevent',
765             uid: { value: 'test-event' },
766             dtstamp: {
767                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
768             },
769             dtstart: {
770                 value: { year: 2002, month: 12, day: 30 },
771                 parameters: { type: 'date' },
772             },
773             sequence: { value: 0 },
774             exdate: [
775                 {
776                     parameters: { type: 'date' },
777                     value: { day: 10, month: 6, year: 2020 },
778                 },
779                 {
780                     parameters: { type: 'date' },
781                     value: { day: 11, month: 6, year: 2020 },
782                 },
783             ],
784             rrule: { value: { freq: 'DAILY' } },
785         });
786     });
788     it('should support unofficial time zones in our database and normalize recurrence-id', () => {
789         const vevent = `BEGIN:VEVENT
790 DTSTAMP:19980309T231000Z
791 UID:test-event
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
796 END:VEVENT`;
797         const event = parse(vevent) as VcalVeventComponent;
798         expect(
799             getSupportedEvent({
800                 vcalVeventComponent: event,
801                 hasXWrTimezone: false,
802                 guessTzid: 'Asia/Seoul',
803                 componentIdentifiers,
804             })
805         ).toEqual({
806             component: 'vevent',
807             uid: { value: 'test-event' },
808             dtstamp: {
809                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
810             },
811             dtstart: {
812                 value: { year: 2002, month: 12, day: 30, hours: 20, minutes: 30, seconds: 0, isUTC: false },
813                 parameters: { tzid: 'America/Denver' },
814             },
815             dtend: {
816                 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
817                 parameters: { tzid: 'Europe/Berlin' },
818             },
819             sequence: { value: 0 },
820             'recurrence-id': {
821                 value: { year: 2003, month: 1, day: 2, hours: 0, minutes: 30, seconds: 0, isUTC: false },
822                 parameters: { tzid: 'Europe/Sarajevo' },
823             },
824             location: { value: '1CP Conference Room 4350' },
825         });
826     });
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
831 UID:test-event
832 DTSTART:20110613T150000Z
833 DTEND:20110613T160000Z
834 LOCATION:1CP Conference Room 4350
835 END:VEVENT`;
836         const event = parse(vevent) as VcalVeventComponent;
837         expect(
838             getSupportedEvent({
839                 vcalVeventComponent: event,
840                 hasXWrTimezone: true,
841                 calendarTzid: 'Europe/Zurich',
842                 guessTzid: 'Asia/Seoul',
843                 componentIdentifiers,
844             })
845         ).toEqual({
846             component: 'vevent',
847             uid: { value: 'test-event' },
848             dtstamp: {
849                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
850             },
851             dtstart: {
852                 value: { year: 2011, month: 6, day: 13, hours: 17, minutes: 0, seconds: 0, isUTC: false },
853                 parameters: { tzid: 'Europe/Zurich' },
854             },
855             dtend: {
856                 value: { year: 2011, month: 6, day: 13, hours: 18, minutes: 0, seconds: 0, isUTC: false },
857                 parameters: { tzid: 'Europe/Zurich' },
858             },
859             sequence: { value: 0 },
860             location: { value: '1CP Conference Room 4350' },
861         });
862     });
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
867 UID:test-event
868 DTSTART:20110613T150000Z
869 DTEND:20110613T160000Z
870 RECURRENCE-ID:20110618T150000Z
871 LOCATION:1CP Conference Room 4350
872 END:VEVENT`;
873         const event = parse(vevent) as VcalVeventComponent;
874         expect(
875             getSupportedEvent({
876                 vcalVeventComponent: event,
877                 hasXWrTimezone: true,
878                 calendarTzid: 'Europe/Zurich',
879                 guessTzid: 'Asia/Seoul',
880                 componentIdentifiers,
881             })
882         ).toEqual({
883             component: 'vevent',
884             uid: { value: 'test-event' },
885             dtstamp: {
886                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
887             },
888             dtstart: {
889                 value: { year: 2011, month: 6, day: 13, hours: 15, minutes: 0, seconds: 0, isUTC: true },
890             },
891             dtend: {
892                 value: { year: 2011, month: 6, day: 13, hours: 16, minutes: 0, seconds: 0, isUTC: true },
893             },
894             'recurrence-id': {
895                 value: { year: 2011, month: 6, day: 18, hours: 15, minutes: 0, seconds: 0, isUTC: true },
896             },
897             location: { value: '1CP Conference Room 4350' },
898             sequence: { value: 0 },
899         });
900     });
902     it('should localize events with floating times with the guess time zone if no global time zone has been specified', () => {
903         const vevent = `
904 BEGIN:VEVENT
905 DTSTAMP:19980309T231000Z
906 UID:test-event
907 DTSTART:20021231T203000
908 DTEND:20030101T003000
909 RECURRENCE-ID:20030102T003000
910 LOCATION:1CP Conference Room 4350
911 END:VEVENT`;
912         const event = parse(vevent) as VcalVeventComponent;
913         expect(
914             getSupportedEvent({
915                 vcalVeventComponent: event,
916                 hasXWrTimezone: false,
917                 guessTzid: 'Asia/Seoul',
918                 componentIdentifiers,
919             })
920         ).toEqual({
921             ...event,
922             dtstart: { value: event.dtstart.value, parameters: { tzid: 'Asia/Seoul' } } as VcalDateTimeProperty,
923             dtend: { value: event.dtend!.value, parameters: { tzid: 'Asia/Seoul' } } as VcalDateTimeProperty,
924             'recurrence-id': {
925                 value: event['recurrence-id']!.value,
926                 parameters: { tzid: 'Asia/Seoul' },
927             } as VcalDateTimeProperty,
928             sequence: { value: 0 },
929         });
930     });
932     it(`should reject events with floating times if a non-supported global time zone has been specified`, () => {
933         const vevent = `
934 BEGIN:VEVENT
935 DTSTAMP:19980309T231000Z
936 UID:test-event
937 DTSTART:20021231T203000
938 DTEND:20030101T003000
939 LOCATION:1CP Conference Room 4350
940 END:VEVENT`;
941         const event = parse(vevent) as VcalVeventComponent;
942         expect(() =>
943             getSupportedEvent({
944                 vcalVeventComponent: event,
945                 hasXWrTimezone: true,
946                 guessTzid: 'Asia/Seoul',
947                 componentIdentifiers,
948             })
949         ).toThrowError('Calendar time zone not supported');
950     });
952     it('should support floating times if a supported global time zone has been specified', () => {
953         const vevent = `
954 BEGIN:VEVENT
955 DTSTAMP:19980309T231000Z
956 UID:test-event
957 DTSTART:20021231T203000
958 DTEND:20030101T003000
959 LOCATION:1CP Conference Room 4350
960 END:VEVENT`;
961         const tzid = 'Europe/Brussels';
962         const event = parse(vevent) as VcalVeventComponent & Required<Pick<VcalVeventComponent, 'dtend'>>;
963         expect(
964             getSupportedEvent({
965                 vcalVeventComponent: event,
966                 calendarTzid: tzid,
967                 hasXWrTimezone: true,
968                 guessTzid: 'Asia/Seoul',
969                 componentIdentifiers,
970             })
971         ).toEqual({
972             ...event,
973             dtstart: { value: event.dtstart.value, parameters: { tzid } } as VcalDateTimeProperty,
974             dtend: { value: event.dtend.value, parameters: { tzid } } as VcalDateTimeProperty,
975             sequence: { value: 0 },
976         });
977     });
979     it('should ignore global time zone if part-day event time is not floating', () => {
980         const vevent = `
981 BEGIN:VEVENT
982 DTSTAMP:19980309T231000Z
983 UID:test-event
984 DTSTART;TZID=Europe/Vilnius:20200518T150000
985 DTEND;TZID=Europe/Vilnius:20200518T160000
986 LOCATION:1CP Conference Room 4350
987 SEQUENCE:0
988 END:VEVENT`;
989         const tzid = 'Europe/Brussels';
990         const event = parse(vevent) as VcalVeventComponent;
991         expect(
992             getSupportedEvent({
993                 vcalVeventComponent: event,
994                 calendarTzid: tzid,
995                 hasXWrTimezone: true,
996                 guessTzid: 'Asia/Seoul',
997                 componentIdentifiers,
998             })
999         ).toEqual(event);
1000     });
1002     it('should ignore global time zone for all-day events', () => {
1003         const vevent = `
1004 BEGIN:VEVENT
1005 DTSTAMP:19980309T231000Z
1006 UID:test-event
1007 DTSTART;VALUE=DATE:20200518
1008 DTEND;VALUE=DATE:20200520
1009 LOCATION:1CP Conference Room 4350
1010 SEQUENCE:1
1011 END:VEVENT`;
1012         const tzid = 'Europe/Brussels';
1013         const event = parse(vevent) as VcalVeventComponent;
1014         expect(
1015             getSupportedEvent({
1016                 vcalVeventComponent: event,
1017                 calendarTzid: tzid,
1018                 hasXWrTimezone: true,
1019                 guessTzid: 'Asia/Seoul',
1020                 componentIdentifiers,
1021             })
1022         ).toEqual(event);
1023     });
1025     it('should not support other time zones not in our list', () => {
1026         const vevent = `BEGIN:VEVENT
1027 DTSTAMP:19980309T231000Z
1028 UID:test-event
1029 DTSTART;TZID=Chamorro Standard Time:20021231T203000
1030 DTEND;TZID=Chamorro Standard Time:20030101T003000
1031 LOCATION:1CP Conference Room 4350
1032 END:VEVENT`;
1033         const event = parse(vevent) as VcalVeventComponent;
1034         expect(() =>
1035             getSupportedEvent({
1036                 vcalVeventComponent: event,
1037                 hasXWrTimezone: false,
1038                 guessTzid: 'Asia/Seoul',
1039                 componentIdentifiers,
1040             })
1041         ).toThrowError('Time zone not supported');
1042     });
1044     it('should crop long UIDs and truncate titles, descriptions and locations', () => {
1045         const loremIpsum =
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.';
1047         const longUID =
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.
1058 END:VEVENT`;
1059         const event = parse(vevent) as VcalVeventComponent;
1060         expect(croppedUID.length === MAX_CHARS_API.UID);
1061         expect(
1062             getSupportedEvent({
1063                 vcalVeventComponent: event,
1064                 hasXWrTimezone: false,
1065                 guessTzid: 'Asia/Seoul',
1066                 componentIdentifiers,
1067             })
1068         ).toEqual({
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 },
1075         });
1076     });
1078     it('should accept events with colors', () => {
1079         const vevent = `BEGIN:VEVENT
1080 COLOR:turquoise
1081 DTSTAMP:19980309T231000Z
1082 UID:uid@proton.me
1083 DTSTART;VALUE=DATE:20080101
1084 DTEND;VALUE=DATE:20080102
1085 END:VEVENT`;
1087         const event = parse(vevent) as VcalVeventComponent;
1089         expect(
1090             getSupportedEvent({
1091                 vcalVeventComponent: event,
1092                 hasXWrTimezone: false,
1093                 guessTzid: 'Asia/Seoul',
1094                 canImportEventColor: true,
1095                 componentIdentifiers,
1096             })
1097         ).toEqual({
1098             ...omit(event, ['dtend']),
1099             uid: { value: 'uid@proton.me' },
1100             sequence: { value: 0 },
1101             color: { value: ACCENT_COLORS_MAP.enzian.color },
1102         });
1103     });
1105     it('should ignore colors when user is free', () => {
1106         const vevent = `BEGIN:VEVENT
1107 COLOR:turquoise
1108 DTSTAMP:19980309T231000Z
1109 UID:uid@proton.me
1110 DTSTART;VALUE=DATE:20080101
1111 DTEND;VALUE=DATE:20080102
1112 END:VEVENT`;
1114         const event = parse(vevent) as VcalVeventComponent;
1116         expect(
1117             getSupportedEvent({
1118                 vcalVeventComponent: event,
1119                 hasXWrTimezone: false,
1120                 guessTzid: 'Asia/Seoul',
1121                 canImportEventColor: false,
1122                 componentIdentifiers,
1123             })
1124         ).toEqual({
1125             ...omit(event, ['dtend', 'color']),
1126             uid: { value: 'uid@proton.me' },
1127             sequence: { value: 0 },
1128         });
1129     });
1131     it('should ignore colors when the format is invalid', () => {
1132         const vevent = `BEGIN:VEVENT
1133 COLOR:invalidColor
1134 DTSTAMP:19980309T231000Z
1135 UID:uid@proton.me
1136 DTSTART;VALUE=DATE:20080101
1137 DTEND;VALUE=DATE:20080102
1138 END:VEVENT`;
1140         const event = parse(vevent) as VcalVeventComponent;
1142         expect(
1143             getSupportedEvent({
1144                 vcalVeventComponent: event,
1145                 hasXWrTimezone: false,
1146                 guessTzid: 'Asia/Seoul',
1147                 canImportEventColor: true,
1148                 componentIdentifiers,
1149             })
1150         ).toEqual({
1151             ...omit(event, ['dtend', 'color']),
1152             uid: { value: 'uid@proton.me' },
1153             sequence: { value: 0 },
1154         });
1155     });
1158 describe('extractSupportedEvent', () => {
1159     it('should add a uid if the event has none', async () => {
1160         const vevent = `
1161 BEGIN:VEVENT
1162 DTSTAMP:19980309T231000Z
1163 DTSTART;TZID=Europe/Brussels:20021231T203000
1164 DTEND;TZID=Europe/Brussels:20030101T003000
1165 LOCATION:1CP Conference Room 4350
1166 END:VEVENT`;
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,
1172             calendarTzid: tzid,
1173             hasXWrTimezone: true,
1174             guessTzid: 'Europe/Zurich',
1175             prodId: '',
1176         });
1177         expect(supportedEvent).toEqual({
1178             component: 'vevent',
1179             uid: { value: 'sha1-uid-0ff30d1f26a94abe627d9f715db16714b01be84c' },
1180             dtstamp: {
1181                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1182             },
1183             dtstart: {
1184                 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1185                 parameters: { tzid: 'Europe/Brussels' },
1186             },
1187             dtend: {
1188                 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1189                 parameters: { tzid: 'Europe/Brussels' },
1190             },
1191             location: { value: '1CP Conference Room 4350' },
1192             sequence: { value: 0 },
1193         });
1194     });
1196     it('should override the uid if the event is an invitation, preserve it in the new uid, and drop recurrence-id', async () => {
1197         const vevent = `
1198 BEGIN:VEVENT
1199 UID:lalalala
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
1205 END:VEVENT`;
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,
1211             calendarTzid: tzid,
1212             hasXWrTimezone: true,
1213             guessTzid: 'Europe/Zurich',
1214             prodId: '',
1215         });
1216         expect(supportedEvent).toEqual({
1217             component: 'vevent',
1218             uid: { value: 'original-uid-lalalala-sha1-uid-d39ba53e577d2eae6ba0baf8539e1fa468fbeabb' },
1219             dtstamp: {
1220                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1221             },
1222             dtstart: {
1223                 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1224                 parameters: { tzid: 'Europe/Brussels' },
1225             },
1226             dtend: {
1227                 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1228                 parameters: { tzid: 'Europe/Brussels' },
1229             },
1230             location: { value: '1CP Conference Room 4350' },
1231             sequence: { value: 0 },
1232         });
1233     });
1235     it('should drop the recurrence id if we overrode the uid', async () => {
1236         const vevent = `
1237 BEGIN:VEVENT
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
1243 END:VEVENT`;
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,
1249             calendarTzid: tzid,
1250             hasXWrTimezone: true,
1251             guessTzid: 'Europe/Zurich',
1252             prodId: '',
1253         });
1254         expect(supportedEvent).toEqual({
1255             component: 'vevent',
1256             uid: { value: 'sha1-uid-ab36432982bccb6dad294500ece330c5829f93ad' },
1257             dtstamp: {
1258                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1259             },
1260             dtstart: {
1261                 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1262                 parameters: { tzid: 'Europe/Brussels' },
1263             },
1264             dtend: {
1265                 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1266                 parameters: { tzid: 'Europe/Brussels' },
1267             },
1268             location: { value: '1CP Conference Room 4350' },
1269             sequence: { value: 0 },
1270         });
1271     });
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
1279 SEQUENCE:0
1280 END:VEVENT`;
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',
1287             prodId: '',
1288         });
1289         expect(supportedEvent).toEqual({
1290             component: 'vevent',
1291             uid: { value: '11353R6@voltigeursbourget.com' },
1292             dtstamp: {
1293                 value: { year: 2022, month: 10, day: 7, hours: 19, minutes: 16, seconds: 46, isUTC: true },
1294             },
1295             dtstart: {
1296                 value: { year: 2022, month: 10, day: 12, hours: 17, minutes: 15, seconds: 0, isUTC: false },
1297                 parameters: { tzid: 'America/New_York' },
1298             },
1299             dtend: {
1300                 value: { year: 2022, month: 10, day: 12, hours: 18, minutes: 25, seconds: 0, isUTC: false },
1301                 parameters: { tzid: 'America/New_York' },
1302             },
1303             sequence: { value: 0 },
1304         });
1305     });
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
1312 SEQUENCE:0
1313 END:VEVENT`;
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',
1320             prodId: '',
1321         });
1322         expect(Object.keys(supportedEvent)).toContain('dtstamp');
1323     });
1325     it('should not import alarms for invitations', async () => {
1326         const vevent = `BEGIN:VEVENT
1327 UID:test-event
1328 DTSTAMP:19980309T231000Z
1329 DTSTART;TZID=Europe/Brussels:20021231T203000
1330 DTEND;TZID=Europe/Brussels:20030101T003000
1331 LOCATION:1CP Conference Room 4350
1332 BEGIN:VALARM
1333 TRIGGER:-PT15H
1334 ACTION:DISPLAY
1335 END:VALARM
1336 BEGIN:VALARM
1337 TRIGGER:-PT1W2D
1338 ACTION:EMAIL
1339 END:VALARM
1340 END:VEVENT`;
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,
1346             calendarTzid: tzid,
1347             hasXWrTimezone: true,
1348             guessTzid: 'Europe/Zurich',
1349             prodId: '',
1350         });
1351         expect(supportedEvent).toEqual({
1352             component: 'vevent',
1353             uid: { value: 'original-uid-test-event-sha1-uid-a9c06d78f4755f736bfd046b3deb3d76f99ab285' },
1354             dtstamp: {
1355                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1356             },
1357             dtstart: {
1358                 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1359                 parameters: { tzid: 'Europe/Brussels' },
1360             },
1361             dtend: {
1362                 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1363                 parameters: { tzid: 'Europe/Brussels' },
1364             },
1365             location: { value: '1CP Conference Room 4350' },
1366             sequence: { value: 0 },
1367         });
1368     });
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
1379 CLASS:PUBLIC
1380 PRIORITY:5
1381 DTSTAMP:20230508T153204Z
1382 TRANSP:OPAQUE
1383 STATUS:CONFIRMED
1384 SEQUENCE:0
1385 LOCATION;LANGUAGE=en-US:
1386 BEGIN:VALARM
1387 DESCRIPTION:REMINDER
1388 TRIGGER;RELATED=START:P
1389 ACTION:DISPLAY
1390 END:VALARM
1391 END:VEVENT`;
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',
1399             prodId: '',
1400         });
1402         expect(supportedEvent).toEqual({
1403             component: 'vevent',
1404             uid: {
1405                 value: 'original-uid-040000008200E00074C5B7101A82E00800000000B058B6A2A081D901000000000000000   0100000004A031FE80ACD7C418A7A1762749176F121-sha1-uid-f286aba29df21425cbf3bcec44de9b7fc5e93ce5',
1406             },
1407             dtstamp: {
1408                 value: { year: 2023, month: 5, day: 8, hours: 15, minutes: 32, seconds: 4, isUTC: true },
1409             },
1410             dtstart: {
1411                 value: { year: 2023, month: 5, day: 13, hours: 12, minutes: 30, seconds: 0, isUTC: false },
1412                 parameters: { tzid: 'America/New_York' },
1413             },
1414             sequence: { value: 0 },
1415             summary: { value: 'Calendar test' },
1416             dtend: {
1417                 value: { year: 2023, month: 5, day: 13, hours: 13, minutes: 0, seconds: 0, isUTC: false },
1418                 parameters: { tzid: 'America/New_York' },
1419             },
1420         });
1421     });
1424 describe('getSupportedEventsOrErrors', () => {
1425     describe('should guess a time zone to localize floating dates', () => {
1426         const generateVcalSetup = (
1427             primaryTimezone = 'Asia/Seoul',
1428             xWrTimezone = '',
1429             vtimezonesTzids: string[] = []
1430         ) => {
1431             const xWrTimezoneString = xWrTimezone ? `X-WR-TIMEZONE:${xWrTimezone}` : '';
1432             const vtimezonesString = vtimezonesTzids
1433                 .map(
1434                     (tzid) => `BEGIN:VTIMEZONE
1435 TZID:${tzid}
1436 END:VTIMEZONE`
1437                 )
1438                 .join('\n');
1439             const vcal = `BEGIN:VCALENDAR
1440 PRODID:Proton Calendar
1441 VERSION:2.0
1442 METHOD:PUBLISH
1443 CALSCALE:GREGORIAN
1444 ${xWrTimezoneString}
1445 ${vtimezonesString}
1446 BEGIN:VEVENT
1447 UID:test-uid
1448 DTSTAMP:19980309T231000Z
1449 DTSTART:20021231T203000
1450 DTEND:20030101T003000
1451 SUMMARY:Floating date-time
1452 END:VEVENT
1453 END:VCALENDAR`;
1454             const {
1455                 components = [],
1456                 calscale: calscaleProperty,
1457                 'x-wr-timezone': xWrTimezoneProperty,
1458                 method: methodProperty,
1459             } = parse(vcal) as VcalVcalendar;
1460             return {
1461                 components,
1462                 calscale: calscaleProperty?.value,
1463                 xWrTimezone: xWrTimezoneProperty?.value,
1464                 method: getIcalMethod(methodProperty) || ICAL_METHOD.PUBLISH,
1465                 primaryTimezone,
1466             };
1467         };
1468         const localizedVevent = (tzid: string) => ({
1469             component: 'vevent',
1470             uid: { value: 'test-uid' },
1471             dtstamp: {
1472                 value: { year: 1998, month: 3, day: 9, hours: 23, minutes: 10, seconds: 0, isUTC: true },
1473             },
1474             dtstart: {
1475                 value: { year: 2002, month: 12, day: 31, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1476                 parameters: { tzid },
1477             },
1478             dtend: {
1479                 value: { year: 2003, month: 1, day: 1, hours: 0, minutes: 30, seconds: 0, isUTC: false },
1480                 parameters: { tzid },
1481             },
1482             summary: { value: 'Floating date-time' },
1483             sequence: { value: 0 },
1484         });
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'])
1489             );
1490             expect(timeZoneIgnoredError).toEqual(new Error('Time zone component ignored') as ImportEventError);
1491             expect(supportedEvent).toEqual(localizedVevent('Europe/Brussels'));
1492         });
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'])
1497             );
1498             expect(timeZoneIgnoredError).toEqual(new Error('Time zone component ignored') as ImportEventError);
1499             expect(supportedEvent).toEqual(localizedVevent('Europe/Vilnius'));
1500         });
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'])
1505             );
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);
1508         });
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' },
1515                 dtstamp: {
1516                     value: {
1517                         year: 1998,
1518                         month: 3,
1519                         day: 9,
1520                         hours: 23,
1521                         minutes: 10,
1522                         seconds: 0,
1523                         isUTC: true,
1524                     },
1525                 },
1526                 dtstart: {
1527                     value: {
1528                         year: 2002,
1529                         month: 12,
1530                         day: 31,
1531                         hours: 20,
1532                         minutes: 30,
1533                         seconds: 0,
1534                         isUTC: false,
1535                     },
1536                     parameters: { tzid: 'Asia/Seoul' },
1537                 },
1538                 summary: { value: 'Floating date-time' },
1539                 sequence: { value: 0 },
1540                 dtend: {
1541                     value: {
1542                         year: 2003,
1543                         month: 1,
1544                         day: 1,
1545                         hours: 0,
1546                         minutes: 30,
1547                         seconds: 0,
1548                         isUTC: false,
1549                     },
1550                     parameters: { tzid: 'Asia/Seoul' },
1551                 },
1552             });
1553         });
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'])
1558             );
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' },
1564                 dtstamp: {
1565                     value: {
1566                         year: 1998,
1567                         month: 3,
1568                         day: 9,
1569                         hours: 23,
1570                         minutes: 10,
1571                         seconds: 0,
1572                         isUTC: true,
1573                     },
1574                 },
1575                 dtstart: {
1576                     value: {
1577                         year: 2002,
1578                         month: 12,
1579                         day: 31,
1580                         hours: 20,
1581                         minutes: 30,
1582                         seconds: 0,
1583                         isUTC: false,
1584                     },
1585                     parameters: { tzid: 'Asia/Seoul' },
1586                 },
1587                 summary: { value: 'Floating date-time' },
1588                 sequence: { value: 0 },
1589                 dtend: {
1590                     value: {
1591                         year: 2003,
1592                         month: 1,
1593                         day: 1,
1594                         hours: 0,
1595                         minutes: 30,
1596                         seconds: 0,
1597                         isUTC: false,
1598                     },
1599                     parameters: { tzid: 'Asia/Seoul' },
1600                 },
1601             });
1602         });
1603     });
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
1610 BEGIN:VTIMEZONE
1611 TZID:UTC
1612 X-LIC-LOCATION:UTC
1613 BEGIN:STANDARD
1614 DTSTART:20200101T000000
1615 RRULE:FREQ=YEARLY;BYDAY=1WE;BYMONTH=1
1616 TZNAME:UTC
1617 TZOFFSETFROM:+0000
1618 TZOFFSETTO:+0000
1619 END:STANDARD
1620 END:VTIMEZONE
1621 BEGIN:VEVENT
1622 ATTENDEE;CN=Ham Burger;RSVP=TRUE:mailto:hamburgerc@pm.me
1623 CLASS:PUBLIC
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
1631 SEQUENCE:0
1632 SUMMARY:Another one bites the dust
1633 UID:81383944-3775313411-20210429T131519@howdyhow.com
1634 BEGIN:VALARM
1635 ACTION:DISPLAY
1636 DESCRIPTION:Reminder
1637 TRIGGER:-PT1H
1638 END:VALARM
1639 END:VEVENT
1640 END:VCALENDAR`;
1641         const ics = new File([new Blob([icsString])], 'invite.ics');
1642         const expectedVtimezone = {
1643             component: 'vtimezone',
1644             components: [
1645                 {
1646                     component: 'standard',
1647                     dtstart: {
1648                         value: { year: 2020, month: 1, day: 1, hours: 0, minutes: 0, seconds: 0, isUTC: false },
1649                     },
1650                     rrule: { value: { freq: 'YEARLY', byday: '1WE', bymonth: 1 } },
1651                     tzname: [{ value: 'UTC' }],
1652                     tzoffsetfrom: [{ value: '+00:00' }],
1653                     tzoffsetto: [{ value: '+00:00' }],
1654                 },
1655             ],
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' },
1663             dtstamp: {
1664                 value: { year: 2021, month: 4, day: 29, hours: 17, minutes: 15, seconds: 19, isUTC: true },
1665             },
1666             dtstart: {
1667                 value: { year: 2021, month: 4, day: 30, hours: 18, minutes: 30, seconds: 0, isUTC: false },
1668             },
1669             dtend: {
1670                 value: { year: 2021, month: 4, day: 30, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1671             },
1672             summary: {
1673                 value: 'Another one bites the dust',
1674             },
1675             description: {
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',
1677             },
1678             sequence: { value: 0 },
1679             organizer: {
1680                 value: 'mailto:belzebu@evil.com',
1681                 parameters: { cn: '' },
1682             },
1683             attendee: [
1684                 {
1685                     value: 'mailto:hamburgerc@pm.me',
1686                     parameters: { cn: 'Ham Burger', rsvp: 'TRUE' },
1687                 },
1688             ],
1689             components: [
1690                 {
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 } },
1695                 },
1696             ],
1697         };
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',
1705         });
1706     });
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
1711 BEGIN:VTIMEZONE
1712 TZID:UTC
1713 X-LIC-LOCATION:UTC
1714 BEGIN:STANDARD
1715 DTSTART:20200101T000000
1716 RRULE:FREQ=YEARLY;BYDAY=1WE;BYMONTH=1
1717 TZNAME:UTC
1718 TZOFFSETFROM:+0000
1719 TZOFFSETTO:+0000
1720 END:STANDARD
1721 END:VTIMEZONE
1722 BEGIN:VEVENT
1723 ATTENDEE;CN=Ham Burger;RSVP=TRUE:mailto:hamburgerc@pm.me
1724 CLASS:PUBLIC
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
1732 SEQUENCE:0
1733 SUMMARY:Another one bites the dust
1734 UID:81383944-3775313411-20210429T131519@howdyhow.com
1735 BEGIN:VALARM
1736 ACTION:DISPLAY
1737 DESCRIPTION:Reminder
1738 TRIGGER:-PT1H
1739 END:VALARM
1740 END:VEVENT
1741 BEGIN:VEVENT
1742 DESCRIPTION:We can't recover from the bad DTSTART
1743 DTSTAMP:20210429T171519Z
1744 DTSTART:2021.04.30T18:35:00
1745 SUMMARY:I'm broken
1746 UID:oh-no@howdyhow.com
1747 END:VEVENT
1748 END:VCALENDAR`;
1749         const ics = new File([new Blob([icsString])], 'invite.ics');
1750         const expectedVtimezone = {
1751             component: 'vtimezone',
1752             components: [
1753                 {
1754                     component: 'standard',
1755                     dtstart: {
1756                         value: { year: 2020, month: 1, day: 1, hours: 0, minutes: 0, seconds: 0, isUTC: false },
1757                     },
1758                     rrule: { value: { freq: 'YEARLY', byday: '1WE', bymonth: 1 } },
1759                     tzname: [{ value: 'UTC' }],
1760                     tzoffsetfrom: [{ value: '+00:00' }],
1761                     tzoffsetto: [{ value: '+00:00' }],
1762                 },
1763             ],
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' },
1771             dtstamp: {
1772                 value: { year: 2021, month: 4, day: 29, hours: 17, minutes: 15, seconds: 19, isUTC: true },
1773             },
1774             dtstart: {
1775                 value: { year: 2021, month: 4, day: 30, hours: 18, minutes: 30, seconds: 0, isUTC: false },
1776             },
1777             dtend: {
1778                 value: { year: 2021, month: 4, day: 30, hours: 20, minutes: 30, seconds: 0, isUTC: false },
1779             },
1780             summary: {
1781                 value: 'Another one bites the dust',
1782             },
1783             description: {
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',
1785             },
1786             sequence: { value: 0 },
1787             organizer: {
1788                 value: 'mailto:belzebu@evil.com',
1789                 parameters: { cn: '' },
1790             },
1791             attendee: [
1792                 {
1793                     value: 'mailto:hamburgerc@pm.me',
1794                     parameters: { cn: 'Ham Burger', rsvp: 'TRUE' },
1795                 },
1796             ],
1797             components: [
1798                 {
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 } },
1803                 },
1804             ],
1805         };
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();
1815     });
1817     it('should trim calscale and method', async () => {
1818         const icsString = `BEGIN:VCALENDAR
1819 PRODID:-//Company Inc//Product Application//EN
1820 CALSCALE: GREGORIAN
1821 VERSION:2.0
1822 METHOD:    Publish
1823 BEGIN:VEVENT
1824 DTSTART:20220530T143000Z
1825 DTEND:20220530T153000Z
1826 DTSTAMP:20220528T075010Z
1827 ORGANIZER;CN=:
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
1833 SEQUENCE:0
1834 STATUS:CONFIRMED
1835 SUMMARY:Optical transform
1836 TRANSP:OPAQUE
1837 BEGIN:VALARM
1838 TRIGGER:-PT30M
1839 REPEAT:
1840 DURATION:PTM
1841 ACTION:DISPLAY
1842 DESCRIPTION:
1843 END:VALARM
1844 END:VEVENT
1845 END:VCALENDAR`;
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);
1850     });
1852     it('should not recognize unknown calscales', async () => {
1853         const icsString = `BEGIN:VCALENDAR
1854 PRODID:-//Company Inc//Product Application//EN
1855 CALSCALE: GREGORIANO
1856 VERSION:2.0
1857 BEGIN:VEVENT
1858 DTSTART:20220530T143000Z
1859 DTEND:20220530T153000Z
1860 DTSTAMP:20220528T075010Z
1861 ORGANIZER;CN=:
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
1867 SEQUENCE:0
1868 STATUS:CONFIRMED
1869 SUMMARY:Optical transform
1870 TRANSP:OPAQUE
1871 BEGIN:VALARM
1872 TRIGGER:-PT30M
1873 REPEAT:
1874 DURATION:PTM
1875 ACTION:DISPLAY
1876 DESCRIPTION:
1877 END:VALARM
1878 END:VEVENT
1879 END:VCALENDAR`;
1880         const ics = new File([new Blob([icsString])], 'invite.ics');
1881         const parsedIcs = await parseIcs(ics);
1882         expect(parsedIcs.calscale).toEqual(undefined);
1883     });
1885     it('should throw for unknown methods', async () => {
1886         const icsString = `BEGIN:VCALENDAR
1887 PRODID:-//Company Inc//Product Application//EN
1888 VERSION:2.0
1889 METHOD:ATTEND
1890 BEGIN:VEVENT
1891 DTSTART:20220530T143000Z
1892 DTEND:20220530T153000Z
1893 DTSTAMP:20220528T075010Z
1894 ORGANIZER;CN=:
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
1900 SEQUENCE:0
1901 STATUS:CONFIRMED
1902 SUMMARY:Optical transform
1903 TRANSP:OPAQUE
1904 BEGIN:VALARM
1905 TRIGGER:-PT30M
1906 REPEAT:
1907 DURATION:PTM
1908 ACTION:DISPLAY
1909 DESCRIPTION:
1910 END:VALARM
1911 END:VEVENT
1912 END:VCALENDAR`;
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.'
1916         );
1917     });