Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / contacts / email / ContactEmailSettingsModal.test.tsx
blob4e63a1a15e640c7b7791ff51e552b53ab986a573
1 import { fireEvent, screen, waitFor, within } from '@testing-library/react';
3 import { CryptoProxy } from '@proton/crypto';
4 import { API_CODES, API_KEY_SOURCE, CONTACT_CARD_TYPE, KEY_FLAG } from '@proton/shared/lib/constants';
5 import { parseToVCard } from '@proton/shared/lib/contacts/vcard';
6 import type { RequireSome } from '@proton/shared/lib/interfaces';
7 import type { VCardContact, VCardProperty } from '@proton/shared/lib/interfaces/contacts/VCard';
8 import { addApiMock } from '@proton/testing/lib/api';
10 import { clearAll, mockedCryptoApi, notificationManager, renderWithProviders } from '../tests/render';
11 import type { ContactEmailSettingsProps } from './ContactEmailSettingsModal';
12 import ContactEmailSettingsModal from './ContactEmailSettingsModal';
14 describe('ContactEmailSettingsModal', () => {
15     const props: ContactEmailSettingsProps = {
16         contactID: 'ContactID',
17         vCardContact: { fn: [] },
18         emailProperty: {} as VCardProperty<string>,
19         onClose: jest.fn(),
20     };
22     beforeEach(clearAll);
24     afterEach(async () => {
25         await CryptoProxy.releaseEndpoint();
26     });
28     it('should save a contact with updated email settings (no keys)', async () => {
29         CryptoProxy.setEndpoint(mockedCryptoApi);
31         const vcard = `BEGIN:VCARD
32 VERSION:4.0
33 FN;PREF=1:J. Doe
34 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
35 ITEM1.EMAIL;PREF=1:jdoe@example.com
36 END:VCARD`;
38         const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
40         const saveRequestSpy = jest.fn();
42         addApiMock('core/v4/keys/all', () => {
43             return { Address: { Keys: [] } };
44         });
45         addApiMock('contacts/v4/contacts', (args) => {
46             saveRequestSpy(args.data);
47             return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
48         });
49         addApiMock('contacts/v4/contacts/ContactID', (args) => {
50             saveRequestSpy(args.data);
51             return { Code: API_CODES.SINGLE_SUCCESS };
52         });
54         renderWithProviders(
55             <ContactEmailSettingsModal
56                 open={true}
57                 {...props}
58                 vCardContact={vCardContact}
59                 emailProperty={vCardContact.email[0]}
60             />
61         );
63         const showMoreButton = screen.getByRole('button', { name: 'Expand' });
64         await waitFor(() => expect(showMoreButton).not.toBeDisabled());
65         fireEvent.click(showMoreButton);
67         const encryptToggle = document.getElementById('encrypt-toggle');
68         expect(encryptToggle).toBeDisabled();
70         const signSelect = screen.getByText("Use global default (Don't sign)", { exact: false });
71         fireEvent.click(signSelect);
72         const signOption = screen.getByTitle('Sign');
73         fireEvent.click(signOption);
75         const pgpSelect = screen.getByText('Use global default (PGP/MIME)', { exact: false });
76         fireEvent.click(pgpSelect);
77         const pgpInlineOption = screen.getByTitle('PGP/Inline');
78         fireEvent.click(pgpInlineOption);
80         const saveButton = screen.getByText('Save');
81         fireEvent.click(saveButton);
83         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
85         const sentData = saveRequestSpy.mock.calls[0][0];
86         const cards = sentData.Cards;
88         const expectedEncryptedCard = `BEGIN:VCARD
89 VERSION:4.0
90 FN;PREF=1:J. Doe
91 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
92 ITEM1.EMAIL;PREF=1:jdoe@example.com
93 ITEM1.X-PM-MIMETYPE:text/plain
94 ITEM1.X-PM-SIGN:true
95 ITEM1.X-PM-SCHEME:pgp-inline
96 END:VCARD`.replaceAll('\n', '\r\n');
98         const signedCardContent = cards.find(
99             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
100         ).Data;
102         expect(signedCardContent).toBe(expectedEncryptedCard);
103     });
105     it('should not store X-PM-SIGN if global default signing setting is selected', async () => {
106         CryptoProxy.setEndpoint(mockedCryptoApi);
108         const vcard = `BEGIN:VCARD
109 VERSION:4.0
110 FN;PREF=1:J. Doe
111 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
112 ITEM1.EMAIL;PREF=1:jdoe@example.com
113 ITEM1.X-PM-SIGN:true
114 END:VCARD`;
116         const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
118         const saveRequestSpy = jest.fn();
120         addApiMock('core/v4/keys/all', () => {
121             return { Address: { Keys: [] } };
122         });
123         addApiMock('contacts/v4/contacts', (args) => {
124             saveRequestSpy(args.data);
125             return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
126         });
127         addApiMock('contacts/v4/contacts/ContactID', (args) => {
128             saveRequestSpy(args.data);
129             return { Code: API_CODES.SINGLE_SUCCESS };
130         });
132         renderWithProviders(
133             <ContactEmailSettingsModal
134                 open={true}
135                 {...props}
136                 vCardContact={vCardContact}
137                 emailProperty={vCardContact.email[0]}
138             />
139         );
141         const showMoreButton = screen.getByRole('button', { name: 'Expand' });
142         await waitFor(() => expect(showMoreButton).not.toBeDisabled());
143         fireEvent.click(showMoreButton);
145         const signSelect = screen.getByText('Sign', { exact: true });
146         fireEvent.click(signSelect);
147         const signOption = screen.getByTitle("Use global default (Don't sign)");
148         fireEvent.click(signOption);
150         const saveButton = screen.getByText('Save');
151         fireEvent.click(saveButton);
153         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
155         const sentData = saveRequestSpy.mock.calls[0][0];
156         const cards = sentData.Cards;
158         const expectedCard = `BEGIN:VCARD
159 VERSION:4.0
160 FN;PREF=1:J. Doe
161 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
162 ITEM1.EMAIL;PREF=1:jdoe@example.com
163 END:VCARD`.replaceAll('\n', '\r\n');
165         const signedCardContent = cards.find(
166             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
167         ).Data;
169         expect(signedCardContent).toBe(expectedCard);
170     });
172     it('should update the stored pinned key order when a different uploaded key is selected for sending', async () => {
173         CryptoProxy.setEndpoint({
174             ...mockedCryptoApi,
175             importPublicKey: jest.fn().mockImplementation(async ({ binaryKey }) => ({
176                 getFingerprint: () => new TextDecoder().decode(binaryKey),
177                 getCreationTime: () => new Date(0),
178                 getExpirationTime: () => new Date(0),
179                 getAlgorithmInfo: () => ({ algorithm: 'eddsa', curve: 'curve25519' }),
180                 subkeys: [],
181                 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
182             })),
183             canKeyEncrypt: jest.fn().mockImplementation(() => true),
184             exportPublicKey: jest.fn().mockImplementation(({ key }) => new TextEncoder().encode(key.getFingerprint())),
185             isExpiredKey: jest.fn().mockImplementation(() => false),
186             isRevokedKey: jest.fn().mockImplementation(() => false),
187         });
189         const dummyKey1Base64 = btoa('dummy-pinned-key-1');
190         const dummyKey2Base64 = btoa('dummy-pinned-key-2');
191         const vcard = `BEGIN:VCARD
192 VERSION:4.0
193 FN;PREF=1:pinned
194 UID:proton-web-b2dc0409-262d-96db-8925-3ee4f0030fd8
195 ITEM1.EMAIL;PREF=1:pinned1@test.com
196 ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,${dummyKey1Base64}
197 ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,${dummyKey2Base64}
198 ITEM1.X-PM-ENCRYPT:true
199 ITEM1.X-PM-SIGN:true
200 END:VCARD`;
202         const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
204         const saveRequestSpy = jest.fn();
206         addApiMock('core/v4/keys/all', () => {
207             return { Address: { Keys: [] } };
208         });
209         addApiMock('contacts/v4/contacts', (args) => {
210             saveRequestSpy(args.data);
211             return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
212         });
213         addApiMock('contacts/v4/contacts/ContactID', (args) => {
214             saveRequestSpy(args.data);
215             return { Code: API_CODES.SINGLE_SUCCESS };
216         });
218         renderWithProviders(
219             <ContactEmailSettingsModal
220                 open={true}
221                 {...props}
222                 vCardContact={vCardContact}
223                 emailProperty={vCardContact.email[0]}
224             />
225         );
227         const showMoreButton = screen.getByRole('button', { name: 'Expand' });
229         await waitFor(() => expect(showMoreButton).not.toBeDisabled());
230         fireEvent.click(showMoreButton);
232         const contactKeysTable = await screen.findByTestId('contact-keys-table', undefined, { timeout: 5000 });
233         const contactKeysTableRows = await within(contactKeysTable).findAllByRole('row');
234         expect(contactKeysTableRows).toHaveLength(2);
235         // test primary key row
236         await within(contactKeysTableRows[0]).findByText('dummy-pinned-key-1'); // fingerprint
237         await within(contactKeysTableRows[0]).findByTestId('primary-key-label');
238         // test other key row
239         await within(contactKeysTableRows[1]).findByText('dummy-pinned-key-2'); // fingerprint
240         expect(within(contactKeysTableRows[1]).queryByTestId('primary-key-label')).toBeNull();
242         // mark second key as primary
243         const dropdownButton = within(contactKeysTableRows[1]).getByTitle('Open actions dropdown');
244         fireEvent.click(dropdownButton);
245         const useForSendingButton = screen.getByText('Use for sending');
246         fireEvent.click(useForSendingButton);
248         const saveButton = screen.getByText('Save');
249         fireEvent.click(saveButton);
251         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
253         const sentData = saveRequestSpy.mock.calls[0][0];
254         const cards = sentData.Cards;
256         const signedCardContent = cards.find(
257             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
258         ).Data;
260         // confirm that pinned key order has been changed
261         expect(signedCardContent.includes(`ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,${dummyKey2Base64}`)).toBe(
262             true
263         );
264         expect(signedCardContent.includes(`ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,${dummyKey1Base64}`)).toBe(
265             true
266         );
267         expect(signedCardContent.includes('ITEM1.X-PM-ENCRYPT')).toBe(true);
268         expect(signedCardContent.includes('ITEM1.X-PM-SIGN')).toBe(true);
269     });
271     it('should warn if encryption is enabled and uploaded keys are not valid for sending', async () => {
272         CryptoProxy.setEndpoint({
273             ...mockedCryptoApi,
274             importPublicKey: jest.fn().mockImplementation(async () => ({
275                 getFingerprint: () => `abcdef`,
276                 getCreationTime: () => new Date(0),
277                 getExpirationTime: () => new Date(0),
278                 getAlgorithmInfo: () => ({ algorithm: 'eddsa', curve: 'curve25519' }),
279                 subkeys: [],
280                 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
281             })),
282             canKeyEncrypt: jest.fn().mockImplementation(() => false),
283             exportPublicKey: jest.fn().mockImplementation(() => new Uint8Array()),
284             isExpiredKey: jest.fn().mockImplementation(() => true),
285             isRevokedKey: jest.fn().mockImplementation(() => false),
286         });
288         const vcard = `BEGIN:VCARD
289 VERSION:4.0
290 FN;PREF=1:expired
291 UID:proton-web-b2dc0409-262d-96db-8925-3ee4f0030fd8
292 ITEM1.EMAIL;PREF=1:expired@test.com
293 ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,xjMEYS376BYJKwYBBAHaRw8BA
294  QdAm9ZJKSCnCg28vJ/1Iegycsiq9wKxFP5/BMDeP51C/jbNGmV4cGlyZWQgPGV4cGlyZWRAdGVz
295  dC5jb20+wpIEEBYKACMFAmEt++gFCQAAAPoECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRDs7cn
296  9e8csRhYhBP/xHanO8KRS6sPFiOztyf17xyxGhYIBANpMcbjGa3w3qPzWDfb3b/TgfbJuYFQ49Y
297  ik/Zd/ZZQZAP42rtyxbSz/XfKkNdcJPbZ+MQa2nalOZ6+uXm9ScCQtBc44BGEt++gSCisGAQQBl
298  1UBBQEBB0Dj+ZNzODXqLeZchFOVE4E87HD8QsoSI60bDkpklgK3eQMBCAfCfgQYFggADwUCYS37
299  6AUJAAAA+gIbDAAhCRDs7cn9e8csRhYhBP/xHanO8KRS6sPFiOztyf17xyxGbyIA/2Jz6p/6WBo
300  yh279kjiKpX8NWde/2/O7M7W7deYulO4oAQDWtYZNTw1OTYfYI2PBcs1kMbB3hhBr1VEG0pLvtz
301  xoAA==
302 ITEM1.X-PM-ENCRYPT:true
303 ITEM1.X-PM-SIGN:true
304 END:VCARD`;
306         const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
308         const saveRequestSpy = jest.fn();
310         addApiMock('core/v4/keys/all', () => {
311             return { Address: { Keys: [] } };
312         });
313         addApiMock('contacts/v4/contacts', (args) => {
314             saveRequestSpy(args.data);
315             return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
316         });
317         addApiMock('contacts/v4/contacts/ContactID', (args) => {
318             saveRequestSpy(args.data);
319             return { Code: API_CODES.SINGLE_SUCCESS };
320         });
322         renderWithProviders(
323             <ContactEmailSettingsModal
324                 open={true}
325                 {...props}
326                 vCardContact={vCardContact}
327                 emailProperty={vCardContact.email[0]}
328             />
329         );
331         const showMoreButton = screen.getByRole('button', { name: 'Expand' });
333         await waitFor(() => expect(showMoreButton).not.toBeDisabled());
334         fireEvent.click(showMoreButton);
336         await waitFor(() => {
337             const keyFingerprint = screen.getByText('abcdef');
338             return expect(keyFingerprint).toBeVisible();
339         });
341         const warningInvalidKey = screen.getByText(/None of the uploaded keys are valid for encryption/);
342         expect(warningInvalidKey).toBeVisible();
344         const encryptToggleLabel = screen.getByText('Encrypt emails');
345         fireEvent.click(encryptToggleLabel);
347         expect(warningInvalidKey).not.toBeVisible();
349         const saveButton = screen.getByText('Save');
350         fireEvent.click(saveButton);
352         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
354         const sentData = saveRequestSpy.mock.calls[0][0];
355         const cards = sentData.Cards;
357         const signedCardContent = cards.find(
358             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
359         ).Data;
361         expect(signedCardContent.includes('ITEM1.X-PM-ENCRYPT:false')).toBe(true);
362     });
364     it('should enable encryption by default if WKD keys are found', async () => {
365         CryptoProxy.setEndpoint({
366             ...mockedCryptoApi,
367             importPublicKey: jest.fn().mockImplementation(async () => ({
368                 getFingerprint: () => `abcdef`,
369                 getCreationTime: () => new Date(0),
370                 getExpirationTime: () => new Date(0),
371                 getAlgorithmInfo: () => ({ algorithm: 'eddsa', curve: 'curve25519' }),
372                 subkeys: [],
373                 getUserIDs: jest.fn().mockImplementation(() => ['<jdoe@example.com>']),
374             })),
375             canKeyEncrypt: jest.fn().mockImplementation(() => true),
376             exportPublicKey: jest.fn().mockImplementation(() => new Uint8Array()),
377             isExpiredKey: jest.fn().mockImplementation(() => false),
378             isRevokedKey: jest.fn().mockImplementation(() => false),
379         });
380         const armoredPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
382 xjMEYRaiLRYJKwYBBAHaRw8BAQdAMrsrfniSJuxOLn+Q3VKP0WWqgizG4VOF
383 6t0HZYx8mSnNEHRlc3QgPHRlc3RAYS5pdD7CjAQQFgoAHQUCYRaiLQQLCQcI
384 AxUICgQWAAIBAhkBAhsDAh4BACEJEKaNwv/NOLSZFiEEnJT1OMsrVBCZa+wE
385 po3C/804tJnYOAD/YR2og60sJ2VVhPwYRL258dYIHnJXI2dDXB+m76GK9x4A
386 /imlPnTOgIJAV1xOqkvO96QcbawjKgvH829zxN9DZEgMzjgEYRaiLRIKKwYB
387 BAGXVQEFAQEHQN5UswYds0RWr4I7xNKNK+fOn+o9pYkkYzJwCbqxCsBwAwEI
388 B8J4BBgWCAAJBQJhFqItAhsMACEJEKaNwv/NOLSZFiEEnJT1OMsrVBCZa+wE
389 po3C/804tJkeKgEA0ruKx9rcMTi4LxfYgijjPrI+GgrfegfREt/YN2KQ75gA
390 /Rs9S+8arbQVoniq7izz3uisWxfjMup+IVEC5uqMld8L
391 =8+ep
392 -----END PGP PUBLIC KEY BLOCK-----`;
394         const vcard = `BEGIN:VCARD
395 VERSION:4.0
396 FN;PREF=1:J. Doe
397 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
398 ITEM1.EMAIL;PREF=1:jdoe@example.com
399 ITEM1.X-PM-ENCRYPT:true
400 END:VCARD`;
402         const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
404         const saveRequestSpy = jest.fn();
406         addApiMock('core/v4/keys/all', () => {
407             return {
408                 Address: { Keys: [] },
409                 Unverified: {
410                     Keys: [{ Flags: 3, PublicKey: armoredPublicKey }],
411                 },
412             };
413         });
414         addApiMock('contacts/v4/contacts', (args) => {
415             saveRequestSpy(args.data);
416             return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
417         });
418         addApiMock('contacts/v4/contacts/ContactID', (args) => {
419             saveRequestSpy(args.data);
420             return { Code: API_CODES.SINGLE_SUCCESS };
421         });
423         renderWithProviders(
424             <ContactEmailSettingsModal
425                 open={true}
426                 {...props}
427                 vCardContact={vCardContact}
428                 emailProperty={vCardContact.email[0]}
429             />
430         );
432         const showMoreButton = screen.getByRole('button', { name: 'Expand' });
433         await waitFor(() => expect(showMoreButton).not.toBeDisabled());
434         fireEvent.click(showMoreButton);
436         const encryptToggle = document.getElementById('encrypt-toggle');
437         expect(encryptToggle).not.toBeDisabled();
438         expect(encryptToggle).toBeChecked();
440         const signSelectDropdown = document.getElementById('sign-select');
441         expect(signSelectDropdown).toBeDisabled();
442         signSelectDropdown?.innerHTML.includes('Sign');
444         const saveButton = screen.getByText('Save');
445         fireEvent.click(saveButton);
447         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
449         const sentData = saveRequestSpy.mock.calls[0][0];
450         const cards = sentData.Cards;
452         const expectedEncryptedCard = `BEGIN:VCARD
453 VERSION:4.0
454 FN;PREF=1:J. Doe
455 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
456 ITEM1.EMAIL;PREF=1:jdoe@example.com
457 ITEM1.X-PM-ENCRYPT-UNTRUSTED:true
458 ITEM1.X-PM-SIGN:true
459 END:VCARD`.replaceAll('\n', '\r\n');
461         const signedCardContent = cards.find(
462             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
463         ).Data;
465         expect(signedCardContent).toBe(expectedEncryptedCard);
466     });
468     it('should warn if encryption is enabled and WKD keys are not valid for sending', async () => {
469         CryptoProxy.setEndpoint({
470             ...mockedCryptoApi,
471             importPublicKey: jest.fn().mockImplementation(async () => ({
472                 getFingerprint: () => `abcdef`,
473                 getCreationTime: () => new Date(0),
474                 getExpirationTime: () => new Date(0),
475                 getAlgorithmInfo: () => ({ algorithm: 'eddsa', curve: 'curve25519' }),
476                 subkeys: [],
477                 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
478             })),
479             canKeyEncrypt: jest.fn().mockImplementation(() => false),
480             exportPublicKey: jest.fn().mockImplementation(() => new Uint8Array()),
481             isExpiredKey: jest.fn().mockImplementation(() => true),
482             isRevokedKey: jest.fn().mockImplementation(() => false),
483         });
485         const armoredPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
487 xjMEYS376BYJKwYBBAHaRw8BAQdAm9ZJKSCnCg28vJ/1Iegycsiq9wKxFP5/
488 BMDeP51C/jbNGmV4cGlyZWQgPGV4cGlyZWRAdGVzdC5jb20+wpIEEBYKACMF
489 AmEt++gFCQAAAPoECwkHCAMVCAoEFgACAQIZAQIbAwIeAQAhCRDs7cn9e8cs
490 RhYhBP/xHanO8KRS6sPFiOztyf17xyxGhYIBANpMcbjGa3w3qPzWDfb3b/Tg
491 fbJuYFQ49Yik/Zd/ZZQZAP42rtyxbSz/XfKkNdcJPbZ+MQa2nalOZ6+uXm9S
492 cCQtBc44BGEt++gSCisGAQQBl1UBBQEBB0Dj+ZNzODXqLeZchFOVE4E87HD8
493 QsoSI60bDkpklgK3eQMBCAfCfgQYFggADwUCYS376AUJAAAA+gIbDAAhCRDs
494 7cn9e8csRhYhBP/xHanO8KRS6sPFiOztyf17xyxGbyIA/2Jz6p/6WBoyh279
495 kjiKpX8NWde/2/O7M7W7deYulO4oAQDWtYZNTw1OTYfYI2PBcs1kMbB3hhBr
496 1VEG0pLvtzxoAA==
497 =456g
498 -----END PGP PUBLIC KEY BLOCK-----`;
500         const vcard = `BEGIN:VCARD
501 VERSION:4.0
502 FN;PREF=1:J. Doe
503 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
504 ITEM1.EMAIL;PREF=1:jdoe@example.com
505 END:VCARD`;
507         const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
509         const saveRequestSpy = jest.fn();
511         addApiMock('core/v4/keys/all', () => {
512             return {
513                 Address: { Keys: [] },
514                 Unverified: { Keys: [{ Flags: 3, PublicKey: armoredPublicKey }] },
515             };
516         });
517         addApiMock('contacts/v4/contacts', (args) => {
518             saveRequestSpy(args.data);
519             return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
520         });
521         addApiMock('contacts/v4/contacts/ContactID', (args) => {
522             saveRequestSpy(args.data);
523             return { Code: API_CODES.SINGLE_SUCCESS };
524         });
526         renderWithProviders(
527             <ContactEmailSettingsModal
528                 open={true}
529                 {...props}
530                 vCardContact={vCardContact}
531                 emailProperty={vCardContact.email[0]}
532             />
533         );
535         const showMoreButton = screen.getByRole('button', { name: 'Expand' });
537         await waitFor(() => expect(showMoreButton).not.toBeDisabled());
538         fireEvent.click(showMoreButton);
540         await waitFor(() => {
541             const keyFingerprint = screen.getByText('abcdef');
542             return expect(keyFingerprint).toBeVisible();
543         });
545         const warningInvalidKey = screen.getByText(/None of the uploaded keys are valid for encryption/);
546         expect(warningInvalidKey).toBeVisible();
548         const encryptToggleLabel = screen.getByText('Encrypt emails');
549         fireEvent.click(encryptToggleLabel);
551         expect(warningInvalidKey).not.toBeVisible();
553         const saveButton = screen.getByText('Save');
554         fireEvent.click(saveButton);
556         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
558         const sentData = saveRequestSpy.mock.calls[0][0];
559         const cards = sentData.Cards;
561         const signedCardContent = cards.find(
562             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
563         ).Data;
565         expect(signedCardContent.includes('ITEM1.X-PM-ENCRYPT-UNTRUSTED:false')).toBe(true);
566     });
568     it('should indicate that end-to-end encryption is disabled for internal addresses whose keys have e2ee-disabled flags', async () => {
569         CryptoProxy.setEndpoint({
570             ...mockedCryptoApi,
571             importPublicKey: jest.fn().mockImplementation(async () => ({
572                 getFingerprint: () => `abcdef`,
573                 getCreationTime: () => new Date(0),
574                 getExpirationTime: () => new Date(0),
575                 getAlgorithmInfo: () => ({ algorithm: 'eddsa', curve: 'curve25519' }),
576                 subkeys: [],
577                 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
578             })),
579             canKeyEncrypt: jest.fn().mockImplementation(() => true),
580             exportPublicKey: jest.fn().mockImplementation(() => new Uint8Array()),
581             isExpiredKey: jest.fn().mockImplementation(() => false),
582             isRevokedKey: jest.fn().mockImplementation(() => false),
583         });
585         const vcard = `BEGIN:VCARD
586 VERSION:4.0
587 FN;PREF=1:J. Doe
588 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
589 ITEM1.EMAIL;PREF=1:jdoe@proton.me
590 END:VCARD`;
592         const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
594         const saveRequestSpy = jest.fn();
596         addApiMock('core/v4/keys/all', () => {
597             return {
598                 Address: {
599                     Keys: [
600                         {
601                             PublicKey: 'mocked armored key',
602                             Flags: KEY_FLAG.FLAG_EMAIL_NO_ENCRYPT | KEY_FLAG.FLAG_NOT_COMPROMISED,
603                         },
604                     ],
605                 },
606                 ProtonMX: true, // internal address
607             };
608         });
609         addApiMock('contacts/v4/contacts', (args) => {
610             saveRequestSpy(args.data);
611             return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
612         });
613         addApiMock('contacts/v4/contacts/ContactID', (args) => {
614             saveRequestSpy(args.data);
615             return { Code: API_CODES.SINGLE_SUCCESS };
616         });
618         renderWithProviders(
619             <ContactEmailSettingsModal
620                 open={true}
621                 {...props}
622                 vCardContact={vCardContact}
623                 emailProperty={vCardContact.email[0]}
624             />
625         );
627         const showMoreButton = screen.getByRole('button', { name: 'Expand' });
629         await waitFor(() => expect(showMoreButton).not.toBeDisabled());
630         fireEvent.click(showMoreButton);
632         await waitFor(() => {
633             const keyFingerprint = screen.getByText('abcdef');
634             return expect(keyFingerprint).toBeVisible();
635         });
637         const infoEncryptionDisabled = screen.getByText(/The owner of this address has disabled end-to-end encryption/);
638         expect(infoEncryptionDisabled).toBeVisible();
640         expect(screen.queryByText('Encrypt emails')).toBeNull();
641         expect(screen.queryByText('Sign emails')).toBeNull();
642         expect(screen.queryByText('Upload keys')).toBeNull();
644         const dropdownButton = screen.getByTitle('Open actions dropdown');
645         fireEvent.click(dropdownButton);
646         const trustKeyButton = screen.getByText('Trust');
647         fireEvent.click(trustKeyButton);
649         const saveButton = screen.getByText('Save');
650         fireEvent.click(saveButton);
652         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
654         const sentData = saveRequestSpy.mock.calls[0][0];
655         const cards = sentData.Cards;
657         const signedCardContent = cards.find(
658             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
659         ).Data;
660         expect(signedCardContent.includes('ITEM1.KEY;PREF=1:data:application/pgp-keys')).toBe(true);
661         expect(signedCardContent.includes('ITEM1.X-PM-ENCRYPT')).toBe(false);
662         expect(signedCardContent.includes('ITEM1.X-PM-SIGN')).toBe(false);
663     });
665     it('should display WKD keys but not internal address keys for external account with internal address keys', async () => {
666         CryptoProxy.setEndpoint({
667             ...mockedCryptoApi,
668             importPublicKey: jest.fn().mockImplementation(async ({ armoredKey }) => ({
669                 getFingerprint: () => armoredKey,
670                 getCreationTime: () => new Date(0),
671                 getExpirationTime: () => new Date(0),
672                 getAlgorithmInfo: () => ({ algorithm: 'eddsa', curve: 'curve25519' }),
673                 subkeys: [],
674                 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
675             })),
676             canKeyEncrypt: jest.fn().mockImplementation(() => true),
677             exportPublicKey: jest.fn().mockImplementation(() => new Uint8Array()),
678             isExpiredKey: jest.fn().mockImplementation(() => false),
679             isRevokedKey: jest.fn().mockImplementation(() => false),
680         });
682         const vcard = `BEGIN:VCARD
683 VERSION:4.0
684 FN;PREF=1:J. Doe
685 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
686 ITEM1.EMAIL;PREF=1:jdoe@example.com
687 END:VCARD`;
689         const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
691         const saveRequestSpy = jest.fn();
693         addApiMock('core/v4/keys/all', () => {
694             return {
695                 Address: {
696                     Keys: [
697                         {
698                             PublicKey: 'internal mocked armored key',
699                             Flags: KEY_FLAG.FLAG_EMAIL_NO_ENCRYPT | KEY_FLAG.FLAG_NOT_COMPROMISED,
700                         },
701                     ],
702                 },
703                 Unverified: {
704                     Keys: [{ PublicKey: 'wkd mocked armored key', Flags: KEY_FLAG.FLAG_NOT_COMPROMISED }],
705                 },
706                 ProtonMX: false, // external account
707             };
708         });
709         addApiMock('contacts/v4/contacts', (args) => {
710             saveRequestSpy(args.data);
711             return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
712         });
713         addApiMock('contacts/v4/contacts/ContactID', (args) => {
714             saveRequestSpy(args.data);
715             return { Code: API_CODES.SINGLE_SUCCESS };
716         });
718         renderWithProviders(
719             <ContactEmailSettingsModal
720                 open={true}
721                 {...props}
722                 vCardContact={vCardContact}
723                 emailProperty={vCardContact.email[0]}
724             />
725         );
727         const showMoreButton = screen.getByRole('button', { name: 'Expand' });
729         await waitFor(() => expect(showMoreButton).not.toBeDisabled());
730         fireEvent.click(showMoreButton);
732         await waitFor(() => {
733             const internalAddressKeyFingerprint = screen.queryByText('internal mocked armored key');
734             return expect(internalAddressKeyFingerprint).toBeNull();
735         });
737         await waitFor(() => {
738             const wkdKeyFingerprint = screen.getByText('wkd mocked armored key');
739             return expect(wkdKeyFingerprint).toBeVisible();
740         });
742         const infoEncryptionDisabled = screen.queryByText(
743             /The owner of this address has disabled end-to-end encryption/
744         );
745         expect(infoEncryptionDisabled).toBeNull(); // only shown to internal accounts
747         // Ensure the UI matches that of external recipients with WKD keys:
748         // - encryption should be enabled by default, and toggable
749         // - key uploads are not permitted
750         // - key pinning works stores the X-PM-ENCRYPT flag
751         const encryptToggle = document.getElementById('encrypt-toggle');
752         expect(encryptToggle).not.toBeDisabled();
753         expect(encryptToggle).toBeChecked();
755         const signSelectDropdown = document.getElementById('sign-select');
756         expect(signSelectDropdown).toBeDisabled();
757         signSelectDropdown?.innerHTML.includes('Sign');
759         expect(screen.queryByText('Upload keys')).toBeNull();
761         const dropdownButton = screen.getByTitle('Open actions dropdown');
762         fireEvent.click(dropdownButton);
763         const trustKeyButton = screen.getByText('Trust');
764         fireEvent.click(trustKeyButton);
766         const saveButton = screen.getByText('Save');
767         fireEvent.click(saveButton);
769         await waitFor(() => expect(notificationManager.createNotification).toHaveBeenCalled());
771         const sentData = saveRequestSpy.mock.calls[0][0];
772         const cards = sentData.Cards;
774         const signedCardContent = cards.find(
775             ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
776         ).Data;
778         expect(signedCardContent.includes('ITEM1.KEY;PREF=1:data:application/pgp-keys')).toBe(true);
779         expect(signedCardContent.includes('ITEM1.X-PM-ENCRYPT:true')).toBe(true);
780         expect(signedCardContent.includes('ITEM1.X-PM-SIGN:true')).toBe(true);
781     });
783     describe('contact key origin label', () => {
784         const vcard = `BEGIN:VCARD
785 VERSION:4.0
786 FN;PREF=1:J. Doe
787 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
788 ITEM1.EMAIL;PREF=1:jdoe@example.com
789 END:VCARD`;
791         beforeEach(() =>
792             CryptoProxy.setEndpoint({
793                 ...mockedCryptoApi,
794                 importPublicKey: jest.fn().mockImplementation(async ({ armoredKey }) => ({
795                     getFingerprint: () => armoredKey,
796                     getCreationTime: () => new Date(0),
797                     getExpirationTime: () => new Date(0),
798                     getAlgorithmInfo: () => ({ algorithm: 'eddsa', curve: 'curve25519' }),
799                     subkeys: [],
800                     getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
801                 })),
802                 canKeyEncrypt: jest.fn().mockImplementation(() => true),
803                 exportPublicKey: jest.fn().mockImplementation(() => new Uint8Array()),
804                 isExpiredKey: jest.fn().mockImplementation(() => false),
805                 isRevokedKey: jest.fn().mockImplementation(() => false),
806             })
807         );
809         it('should display no origin label for internal keys', async () => {
810             const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
812             addApiMock('core/v4/keys/all', () => ({
813                 Address: {
814                     Keys: [
815                         {
816                             PublicKey: 'internally mocked armored key',
817                             Flags: KEY_FLAG.FLAG_NOT_COMPROMISED,
818                         },
819                     ],
820                 },
821                 ProtonMX: true, // internal address
822             }));
824             renderWithProviders(
825                 <ContactEmailSettingsModal
826                     open={true}
827                     {...props}
828                     vCardContact={vCardContact}
829                     emailProperty={vCardContact.email[0]}
830                 />
831             );
833             const showMoreButton = screen.getByRole('button', { name: 'Expand' });
834             await waitFor(() => expect(showMoreButton).not.toBeDisabled());
835             fireEvent.click(showMoreButton);
837             await waitFor(() => {
838                 const wkdKeyFingerprint = screen.getByText('internally mocked armored key');
839                 return expect(wkdKeyFingerprint).toBeVisible();
840             });
842             expect(screen.queryByTestId('wkd-origin-label')).toBeNull();
843             expect(screen.queryByTestId('koo-origin-label')).toBeNull();
844         });
845         it('should display WKD label for WKD keys', async () => {
846             const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
848             addApiMock('core/v4/keys/all', () => ({
849                 Address: {
850                     Keys: [],
851                 },
852                 Unverified: {
853                     Keys: [
854                         {
855                             PublicKey: 'externally fetched mocked armored key',
856                             Flags: KEY_FLAG.FLAG_NOT_COMPROMISED,
857                             Source: API_KEY_SOURCE.WKD,
858                         },
859                     ],
860                 },
861                 ProtonMX: false, // external account
862             }));
864             renderWithProviders(
865                 <ContactEmailSettingsModal
866                     open={true}
867                     {...props}
868                     vCardContact={vCardContact}
869                     emailProperty={vCardContact.email[0]}
870                 />
871             );
873             const showMoreButton = screen.getByRole('button', { name: 'Expand' });
875             await waitFor(() => expect(showMoreButton).not.toBeDisabled());
876             fireEvent.click(showMoreButton);
878             await waitFor(() => {
879                 const wkdOriginLabel = screen.getByTestId('wkd-origin-label');
880                 return expect(wkdOriginLabel).toBeVisible();
881             });
883             expect(screen.queryByTestId('koo-origin-label')).toBeNull();
884         });
886         it('should display KOO label for KOO keys', async () => {
887             const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
889             addApiMock('core/v4/keys/all', () => ({
890                 Address: {
891                     Keys: [],
892                 },
893                 Unverified: {
894                     Keys: [
895                         {
896                             PublicKey: 'externally fetched mocked armored key',
897                             Flags: KEY_FLAG.FLAG_NOT_COMPROMISED,
898                             Source: API_KEY_SOURCE.KOO,
899                         },
900                     ],
901                 },
902                 ProtonMX: false, // external account
903             }));
905             renderWithProviders(
906                 <ContactEmailSettingsModal
907                     open={true}
908                     {...props}
909                     vCardContact={vCardContact}
910                     emailProperty={vCardContact.email[0]}
911                 />
912             );
914             const showMoreButton = screen.getByRole('button', { name: 'Expand' });
915             await waitFor(() => expect(showMoreButton).not.toBeDisabled());
916             fireEvent.click(showMoreButton);
918             await waitFor(() => {
919                 const kooOriginLabel = screen.getByTestId('koo-origin-label');
920                 return expect(kooOriginLabel).toBeVisible();
921             });
923             expect(screen.queryByTestId('wkd-origin-label')).toBeNull();
924         });
925     });