Remove client-side isLoggedIn value
[ProtonMail-WebClient.git] / packages / shared / test / calendar / serialize.spec.ts
blob04343b4fcd7a9add0d5cf0ede171bda33e022611
1 import type { PublicKeyReference, SessionKey } from '@proton/crypto';
2 import { CryptoProxy, toPublicKeyReference } from '@proton/crypto';
3 import { getIsAllDay } from '@proton/shared/lib/calendar/veventHelper';
4 import { ACCENT_COLORS_MAP } from '@proton/shared/lib/colors';
5 import { omit } from '@proton/shared/lib/helpers/object';
6 import { disableRandomMock, initRandomMock } from '@proton/testing/lib/mockRandomValues';
8 import {
9     ATTENDEE_STATUS_API,
10     CALENDAR_SHARE_BUSY_TIME_SLOTS,
11     EVENT_VERIFICATION_STATUS,
12 } from '../../lib/calendar/constants';
13 import { readCalendarEvent, readSessionKeys } from '../../lib/calendar/deserialize';
14 import { unwrap, wrap } from '../../lib/calendar/helper';
15 import { createCalendarEvent } from '../../lib/calendar/serialize';
16 import { setVcalProdId } from '../../lib/calendar/vcalConfig';
17 import { toCRLF } from '../../lib/helpers/string';
18 import type { RequireSome } from '../../lib/interfaces';
19 import type {
20     Attendee,
21     CalendarEventData,
22     CreateOrUpdateCalendarEventData,
23     VcalVeventComponent,
24 } from '../../lib/interfaces/calendar';
25 import { DecryptableKey, DecryptableKey2 } from '../keys/keys.data';
27 const getVevent = (hasColor = false): VcalVeventComponent => {
28     return {
29         component: 'vevent',
30         components: [
31             {
32                 component: 'valarm',
33                 action: { value: 'DISPLAY' },
34                 trigger: {
35                     value: { weeks: 0, days: 0, hours: 15, minutes: 0, seconds: 0, isNegative: true },
36                 },
37             },
38         ],
39         uid: { value: '123' },
40         dtstamp: {
41             value: { year: 2019, month: 12, day: 11, hours: 12, minutes: 12, seconds: 12, isUTC: true },
42         },
43         dtstart: {
44             value: { year: 2019, month: 12, day: 11, hours: 12, minutes: 12, seconds: 12, isUTC: true },
45         },
46         dtend: {
47             value: { year: 2019, month: 12, day: 12, hours: 12, minutes: 12, seconds: 12, isUTC: true },
48         },
49         summary: { value: 'my title' },
50         comment: [{ value: 'asdasd' }],
51         color: hasColor ? { value: ACCENT_COLORS_MAP.enzian.color } : undefined,
52         attendee: [
53             {
54                 value: 'mailto:james@bond.co.uk',
55                 parameters: {
56                     cutype: 'INDIVIDUAL',
57                     role: 'REQ-PARTICIPANT',
58                     rsvp: 'TRUE',
59                     partstat: 'NEEDS-ACTION',
60                     'x-pm-token': 'abc',
61                     cn: 'james@bond.co.uk',
62                 },
63             },
64             {
65                 value: 'mailto:dr.no@mi6.co.uk',
66                 parameters: {
67                     cutype: 'INDIVIDUAL',
68                     role: 'REQ-PARTICIPANT',
69                     rsvp: 'TRUE',
70                     partstat: 'TENTATIVE',
71                     'x-pm-token': 'bcd',
72                     cn: 'Dr No.',
73                 },
74             },
75             {
76                 value: 'mailto:moneypenny@mi6.co.uk',
77                 parameters: {
78                     cutype: 'INDIVIDUAL',
79                     role: 'NON-PARTICIPANT',
80                     partstat: 'ACCEPTED',
81                     rsvp: 'FALSE',
82                     cn: 'Miss Moneypenny',
83                     'x-pm-token': 'cde',
84                 },
85             },
86         ],
87     };
90 interface CreateCalendarEventData
91     extends RequireSome<
92         Partial<Omit<CreateOrUpdateCalendarEventData, 'Permissions'>>,
93         | 'SharedEventContent'
94         | 'CalendarEventContent'
95         | 'AttendeesEventContent'
96         | 'SharedKeyPacket'
97         | 'CalendarKeyPacket'
98     > {
99     AddressKeyPacket: string | null;
102 const transformToExternal = (
103     data: CreateCalendarEventData,
104     publicAddressKey: PublicKeyReference,
105     isAllDay: boolean,
106     sharedSessionKey?: SessionKey,
107     calendarSessionKey?: SessionKey
108 ) => {
109     const withAuthor = (x: Omit<CalendarEventData, 'Author'>[], author: string): CalendarEventData[] => {
110         return x.map((y) => ({ ...y, Author: author }));
111     };
112     const withFullAttendee = (
113         x?: Omit<Attendee, 'UpdateTime' | 'ID'>[],
114         ID = 'dummyID',
115         UpdateTime = 0
116     ): Attendee[] => {
117         return (x || []).map((y, i) => ({ ...y, ID: `${ID}-${i}`, UpdateTime }));
118     };
120     return {
121         event: {
122             SharedEvents: withAuthor(data.SharedEventContent, 'me'),
123             CalendarEvents: withAuthor(data.CalendarEventContent, 'me'),
124             AttendeesEvents: withAuthor(data.AttendeesEventContent, 'me'),
125             Attendees: withFullAttendee(data.Attendees),
126             Notifications: data.Notifications,
127             FullDay: +isAllDay,
128             CalendarID: 'calendarID',
129             ID: 'eventID',
130             Color: null,
131         },
132         publicKeysMap: {
133             me: [publicAddressKey],
134         },
135         sharedSessionKey,
136         calendarSessionKey,
137         calendarSettings: {
138             ID: 'settingsID',
139             CalendarID: 'calendarID',
140             DefaultEventDuration: 30,
141             DefaultPartDayNotifications: [],
142             DefaultFullDayNotifications: [],
143             MakesUserBusy: CALENDAR_SHARE_BUSY_TIME_SLOTS.YES,
144         },
145         addresses: [],
146     };
149 describe('calendar encryption', () => {
150     beforeAll(() => initRandomMock());
151     afterAll(() => disableRandomMock());
153     it('should encrypt and sign calendar events', async () => {
154         const dummyProdId = 'Proton Calendar';
155         setVcalProdId(dummyProdId);
156         const calendarKey = await CryptoProxy.importPrivateKey({
157             armoredKey: DecryptableKey.PrivateKey,
158             passphrase: '123',
159         });
160         const addressKey = await CryptoProxy.importPrivateKey({ armoredKey: DecryptableKey2, passphrase: '123' });
162         // without default notifications
163         expect(
164             await createCalendarEvent({
165                 eventComponent: getVevent(true),
166                 privateKey: addressKey,
167                 publicKey: calendarKey,
168                 isCreateEvent: true,
169                 isSwitchCalendar: false,
170                 hasDefaultNotifications: false,
171             })
172         ).toEqual({
173             SharedKeyPacket:
174                 'wV4DatuD4HBmK9ESAQdAh5aMHBZCvQYA9q2Gm4j5LJYj0N/ETwHe/+Icmt09yl8w81ByP+wHwvShTNdKZNv7ziSuGkYloQ9Y2hReRQR0Vdacz4LtBa2T3H17aBbI/rBs',
175             SharedEventContent: [
176                 {
177                     Type: 2,
178                     Data: wrap(
179                         'BEGIN:VEVENT\r\nUID:123\r\nDTSTAMP:20191211T121212Z\r\nDTSTART:20191211T121212Z\r\nDTEND:20191212T121212Z\r\nEND:VEVENT',
180                         dummyProdId
181                     ),
182                     Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/),
183                 },
184                 {
185                     Type: 3,
186                     // the following check is just to ensure some stability in the process generating the signatures
187                     // i.e. given the same input, we produce the same encrypted data
188                     Data: jasmine.stringMatching(/0sADAfKRArUuTJnXofqQYdEjeY\+U6lg.*/g),
189                     Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
190                 },
191             ],
192             CalendarKeyPacket:
193                 'wV4DatuD4HBmK9ESAQdAh5aMHBZCvQYA9q2Gm4j5LJYj0N/ETwHe/+Icmt09yl8w81ByP+wHwvShTNdKZNv7ziSuGkYloQ9Y2hReRQR0Vdacz4LtBa2T3H17aBbI/rBs',
194             CalendarEventContent: [
195                 {
196                     Type: 3,
197                     // the following check is just to ensure some stability in the process generating the signatures
198                     // i.e. given the same input, we produce the same encrypted data
199                     Data: jasmine.stringMatching(/0sABAfKRArUuTJnXofqQYdEjeY\+U6lg.*/g),
200                     Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
201                 },
202             ],
203             Notifications: [{ Type: 1, Trigger: '-PT15H' }],
204             AttendeesEventContent: [
205                 {
206                     Type: 3,
207                     // the following check is just to ensure some stability in the process generating the signatures
208                     // i.e. given the same input, we produce the same encrypted data
209                     Data: jasmine.stringMatching(/0sE8AfKRArUuTJnXofqQYdEjeY\+U6lh.*/g),
210                     Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
211                 },
212             ],
213             Attendees: [
214                 { Token: 'abc', Status: ATTENDEE_STATUS_API.NEEDS_ACTION },
215                 { Token: 'bcd', Status: ATTENDEE_STATUS_API.TENTATIVE },
216                 { Token: 'cde', Status: ATTENDEE_STATUS_API.ACCEPTED },
217             ],
218             Color: ACCENT_COLORS_MAP.enzian.color,
219         });
221         // with default notifications
222         expect(
223             await createCalendarEvent({
224                 eventComponent: getVevent(false),
225                 privateKey: addressKey,
226                 publicKey: calendarKey,
227                 isCreateEvent: true,
228                 isSwitchCalendar: false,
229                 hasDefaultNotifications: true,
230             })
231         ).toEqual({
232             SharedKeyPacket:
233                 'wV4DatuD4HBmK9ESAQdAh5aMHBZCvQYA9q2Gm4j5LJYj0N/ETwHe/+Icmt09yl8w81ByP+wHwvShTNdKZNv7ziSuGkYloQ9Y2hReRQR0Vdacz4LtBa2T3H17aBbI/rBs',
234             SharedEventContent: [
235                 {
236                     Type: 2,
237                     Data: wrap(
238                         'BEGIN:VEVENT\r\nUID:123\r\nDTSTAMP:20191211T121212Z\r\nDTSTART:20191211T121212Z\r\nDTEND:20191212T121212Z\r\nEND:VEVENT',
239                         dummyProdId
240                     ),
241                     Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/),
242                 },
243                 {
244                     Type: 3,
245                     // the following check is just to ensure some stability in the process generating the signatures
246                     // i.e. given the same input, we produce the same encrypted data
247                     Data: jasmine.stringMatching(/0sADAfKRArUuTJnXofqQYdEjeY\+U6lg.*/g),
248                     Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
249                 },
250             ],
251             CalendarKeyPacket:
252                 'wV4DatuD4HBmK9ESAQdAh5aMHBZCvQYA9q2Gm4j5LJYj0N/ETwHe/+Icmt09yl8w81ByP+wHwvShTNdKZNv7ziSuGkYloQ9Y2hReRQR0Vdacz4LtBa2T3H17aBbI/rBs',
253             CalendarEventContent: [
254                 {
255                     Type: 3,
256                     // the following check is just to ensure some stability in the process generating the signatures
257                     // i.e. given the same input, we produce the same encrypted data
258                     Data: jasmine.stringMatching(/0sABAfKRArUuTJnXofqQYdEjeY\+U6lg.*/g),
259                     Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
260                 },
261             ],
262             Notifications: null,
263             AttendeesEventContent: [
264                 {
265                     Type: 3,
266                     // the following check is just to ensure some stability in the process generating the signatures
267                     // i.e. given the same input, we produce the same encrypted data
268                     Data: jasmine.stringMatching(/0sE8AfKRArUuTJnXofqQYdEjeY\+U6lh.*/g),
269                     Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
270                 },
271             ],
272             Attendees: [
273                 { Token: 'abc', Status: ATTENDEE_STATUS_API.NEEDS_ACTION },
274                 { Token: 'bcd', Status: ATTENDEE_STATUS_API.TENTATIVE },
275                 { Token: 'cde', Status: ATTENDEE_STATUS_API.ACCEPTED },
276             ],
277             Color: null,
278         });
280         setVcalProdId('');
281     });
283     it('should roundtrip', async () => {
284         const veventComponent = getVevent(true);
285         const addressKey = await CryptoProxy.importPrivateKey({ armoredKey: DecryptableKey2, passphrase: '123' });
286         const calendarKey = await CryptoProxy.importPrivateKey({
287             armoredKey: DecryptableKey.PrivateKey,
288             passphrase: '123',
289         });
290         const publicKey = await toPublicKeyReference(calendarKey);
291         const publicAddressKey = await toPublicKeyReference(addressKey);
293         const data = (await createCalendarEvent({
294             eventComponent: veventComponent,
295             privateKey: addressKey,
296             publicKey,
297             isCreateEvent: true,
298             isSwitchCalendar: false,
299             hasDefaultNotifications: false,
300         })) as CreateCalendarEventData;
301         const [sharedSessionKey, calendarSessionKey] = await readSessionKeys({
302             calendarEvent: data,
303             privateKeys: calendarKey,
304         });
306         const { veventComponent: decryptedVeventComponent, verificationStatus } = await readCalendarEvent(
307             transformToExternal(
308                 data,
309                 publicAddressKey,
310                 getIsAllDay(veventComponent),
311                 sharedSessionKey,
312                 calendarSessionKey
313             )
314         );
316         expect(decryptedVeventComponent).toEqual(omit(veventComponent, ['color']));
317         expect(verificationStatus).toEqual(EVENT_VERIFICATION_STATUS.SUCCESSFUL);
318     });
321 describe('wrapping', () => {
322     it('should add wrapping', () => {
323         expect(wrap('asd')).toEqual(
324             toCRLF(`BEGIN:VCALENDAR
325 VERSION:2.0
327 END:VCALENDAR`)
328         );
329         expect(wrap('asd', 'gfd')).toEqual(
330             toCRLF(`BEGIN:VCALENDAR
331 VERSION:2.0
332 PRODID:gfd
334 END:VCALENDAR`)
335         );
336     });
337     it('should remove wrapping', () => {
338         expect(unwrap(wrap('BEGIN:VEVENT asd END:VEVENT', 'gfd'))).toEqual('BEGIN:VEVENT asd END:VEVENT');
339     });