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>,
24 afterEach(async () => {
25 await CryptoProxy.releaseEndpoint();
28 it('should save a contact with updated email settings (no keys)', async () => {
29 CryptoProxy.setEndpoint(mockedCryptoApi);
31 const vcard = `BEGIN:VCARD
34 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
35 ITEM1.EMAIL;PREF=1:jdoe@example.com
38 const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
40 const saveRequestSpy = jest.fn();
42 addApiMock('core/v4/keys/all', () => {
43 return { Address: { Keys: [] } };
45 addApiMock('contacts/v4/contacts', (args) => {
46 saveRequestSpy(args.data);
47 return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
49 addApiMock('contacts/v4/contacts/ContactID', (args) => {
50 saveRequestSpy(args.data);
51 return { Code: API_CODES.SINGLE_SUCCESS };
55 <ContactEmailSettingsModal
58 vCardContact={vCardContact}
59 emailProperty={vCardContact.email[0]}
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
91 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
92 ITEM1.EMAIL;PREF=1:jdoe@example.com
93 ITEM1.X-PM-MIMETYPE:text/plain
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
102 expect(signedCardContent).toBe(expectedEncryptedCard);
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
111 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
112 ITEM1.EMAIL;PREF=1:jdoe@example.com
116 const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
118 const saveRequestSpy = jest.fn();
120 addApiMock('core/v4/keys/all', () => {
121 return { Address: { Keys: [] } };
123 addApiMock('contacts/v4/contacts', (args) => {
124 saveRequestSpy(args.data);
125 return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
127 addApiMock('contacts/v4/contacts/ContactID', (args) => {
128 saveRequestSpy(args.data);
129 return { Code: API_CODES.SINGLE_SUCCESS };
133 <ContactEmailSettingsModal
136 vCardContact={vCardContact}
137 emailProperty={vCardContact.email[0]}
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
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
169 expect(signedCardContent).toBe(expectedCard);
172 it('should update the stored pinned key order when a different uploaded key is selected for sending', async () => {
173 CryptoProxy.setEndpoint({
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' }),
181 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
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),
189 const dummyKey1Base64 = btoa('dummy-pinned-key-1');
190 const dummyKey2Base64 = btoa('dummy-pinned-key-2');
191 const vcard = `BEGIN:VCARD
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
202 const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
204 const saveRequestSpy = jest.fn();
206 addApiMock('core/v4/keys/all', () => {
207 return { Address: { Keys: [] } };
209 addApiMock('contacts/v4/contacts', (args) => {
210 saveRequestSpy(args.data);
211 return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
213 addApiMock('contacts/v4/contacts/ContactID', (args) => {
214 saveRequestSpy(args.data);
215 return { Code: API_CODES.SINGLE_SUCCESS };
219 <ContactEmailSettingsModal
222 vCardContact={vCardContact}
223 emailProperty={vCardContact.email[0]}
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
260 // confirm that pinned key order has been changed
261 expect(signedCardContent.includes(`ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,${dummyKey2Base64}`)).toBe(
264 expect(signedCardContent.includes(`ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,${dummyKey1Base64}`)).toBe(
267 expect(signedCardContent.includes('ITEM1.X-PM-ENCRYPT')).toBe(true);
268 expect(signedCardContent.includes('ITEM1.X-PM-SIGN')).toBe(true);
271 it('should warn if encryption is enabled and uploaded keys are not valid for sending', async () => {
272 CryptoProxy.setEndpoint({
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' }),
280 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
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),
288 const vcard = `BEGIN:VCARD
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
302 ITEM1.X-PM-ENCRYPT:true
306 const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
308 const saveRequestSpy = jest.fn();
310 addApiMock('core/v4/keys/all', () => {
311 return { Address: { Keys: [] } };
313 addApiMock('contacts/v4/contacts', (args) => {
314 saveRequestSpy(args.data);
315 return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
317 addApiMock('contacts/v4/contacts/ContactID', (args) => {
318 saveRequestSpy(args.data);
319 return { Code: API_CODES.SINGLE_SUCCESS };
323 <ContactEmailSettingsModal
326 vCardContact={vCardContact}
327 emailProperty={vCardContact.email[0]}
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();
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
361 expect(signedCardContent.includes('ITEM1.X-PM-ENCRYPT:false')).toBe(true);
364 it('should enable encryption by default if WKD keys are found', async () => {
365 CryptoProxy.setEndpoint({
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' }),
373 getUserIDs: jest.fn().mockImplementation(() => ['<jdoe@example.com>']),
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),
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
392 -----END PGP PUBLIC KEY BLOCK-----`;
394 const vcard = `BEGIN:VCARD
397 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
398 ITEM1.EMAIL;PREF=1:jdoe@example.com
399 ITEM1.X-PM-ENCRYPT:true
402 const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
404 const saveRequestSpy = jest.fn();
406 addApiMock('core/v4/keys/all', () => {
408 Address: { Keys: [] },
410 Keys: [{ Flags: 3, PublicKey: armoredPublicKey }],
414 addApiMock('contacts/v4/contacts', (args) => {
415 saveRequestSpy(args.data);
416 return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
418 addApiMock('contacts/v4/contacts/ContactID', (args) => {
419 saveRequestSpy(args.data);
420 return { Code: API_CODES.SINGLE_SUCCESS };
424 <ContactEmailSettingsModal
427 vCardContact={vCardContact}
428 emailProperty={vCardContact.email[0]}
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
455 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
456 ITEM1.EMAIL;PREF=1:jdoe@example.com
457 ITEM1.X-PM-ENCRYPT-UNTRUSTED:true
459 END:VCARD`.replaceAll('\n', '\r\n');
461 const signedCardContent = cards.find(
462 ({ Type }: { Type: CONTACT_CARD_TYPE }) => Type === CONTACT_CARD_TYPE.SIGNED
465 expect(signedCardContent).toBe(expectedEncryptedCard);
468 it('should warn if encryption is enabled and WKD keys are not valid for sending', async () => {
469 CryptoProxy.setEndpoint({
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' }),
477 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
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),
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
498 -----END PGP PUBLIC KEY BLOCK-----`;
500 const vcard = `BEGIN:VCARD
503 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
504 ITEM1.EMAIL;PREF=1:jdoe@example.com
507 const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
509 const saveRequestSpy = jest.fn();
511 addApiMock('core/v4/keys/all', () => {
513 Address: { Keys: [] },
514 Unverified: { Keys: [{ Flags: 3, PublicKey: armoredPublicKey }] },
517 addApiMock('contacts/v4/contacts', (args) => {
518 saveRequestSpy(args.data);
519 return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
521 addApiMock('contacts/v4/contacts/ContactID', (args) => {
522 saveRequestSpy(args.data);
523 return { Code: API_CODES.SINGLE_SUCCESS };
527 <ContactEmailSettingsModal
530 vCardContact={vCardContact}
531 emailProperty={vCardContact.email[0]}
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();
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
565 expect(signedCardContent.includes('ITEM1.X-PM-ENCRYPT-UNTRUSTED:false')).toBe(true);
568 it('should indicate that end-to-end encryption is disabled for internal addresses whose keys have e2ee-disabled flags', async () => {
569 CryptoProxy.setEndpoint({
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' }),
577 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
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),
585 const vcard = `BEGIN:VCARD
588 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
589 ITEM1.EMAIL;PREF=1:jdoe@proton.me
592 const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
594 const saveRequestSpy = jest.fn();
596 addApiMock('core/v4/keys/all', () => {
601 PublicKey: 'mocked armored key',
602 Flags: KEY_FLAG.FLAG_EMAIL_NO_ENCRYPT | KEY_FLAG.FLAG_NOT_COMPROMISED,
606 ProtonMX: true, // internal address
609 addApiMock('contacts/v4/contacts', (args) => {
610 saveRequestSpy(args.data);
611 return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
613 addApiMock('contacts/v4/contacts/ContactID', (args) => {
614 saveRequestSpy(args.data);
615 return { Code: API_CODES.SINGLE_SUCCESS };
619 <ContactEmailSettingsModal
622 vCardContact={vCardContact}
623 emailProperty={vCardContact.email[0]}
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();
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
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);
665 it('should display WKD keys but not internal address keys for external account with internal address keys', async () => {
666 CryptoProxy.setEndpoint({
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' }),
674 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
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),
682 const vcard = `BEGIN:VCARD
685 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
686 ITEM1.EMAIL;PREF=1:jdoe@example.com
689 const vCardContact = parseToVCard(vcard) as RequireSome<VCardContact, 'email'>;
691 const saveRequestSpy = jest.fn();
693 addApiMock('core/v4/keys/all', () => {
698 PublicKey: 'internal mocked armored key',
699 Flags: KEY_FLAG.FLAG_EMAIL_NO_ENCRYPT | KEY_FLAG.FLAG_NOT_COMPROMISED,
704 Keys: [{ PublicKey: 'wkd mocked armored key', Flags: KEY_FLAG.FLAG_NOT_COMPROMISED }],
706 ProtonMX: false, // external account
709 addApiMock('contacts/v4/contacts', (args) => {
710 saveRequestSpy(args.data);
711 return { Responses: [{ Response: { Code: API_CODES.SINGLE_SUCCESS } }] };
713 addApiMock('contacts/v4/contacts/ContactID', (args) => {
714 saveRequestSpy(args.data);
715 return { Code: API_CODES.SINGLE_SUCCESS };
719 <ContactEmailSettingsModal
722 vCardContact={vCardContact}
723 emailProperty={vCardContact.email[0]}
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();
737 await waitFor(() => {
738 const wkdKeyFingerprint = screen.getByText('wkd mocked armored key');
739 return expect(wkdKeyFingerprint).toBeVisible();
742 const infoEncryptionDisabled = screen.queryByText(
743 /The owner of this address has disabled end-to-end encryption/
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
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);
783 describe('contact key origin label', () => {
784 const vcard = `BEGIN:VCARD
787 UID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1
788 ITEM1.EMAIL;PREF=1:jdoe@example.com
792 CryptoProxy.setEndpoint({
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' }),
800 getUserIDs: jest.fn().mockImplementation(() => ['<userid@userid.com>']),
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),
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', () => ({
816 PublicKey: 'internally mocked armored key',
817 Flags: KEY_FLAG.FLAG_NOT_COMPROMISED,
821 ProtonMX: true, // internal address
825 <ContactEmailSettingsModal
828 vCardContact={vCardContact}
829 emailProperty={vCardContact.email[0]}
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();
842 expect(screen.queryByTestId('wkd-origin-label')).toBeNull();
843 expect(screen.queryByTestId('koo-origin-label')).toBeNull();
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', () => ({
855 PublicKey: 'externally fetched mocked armored key',
856 Flags: KEY_FLAG.FLAG_NOT_COMPROMISED,
857 Source: API_KEY_SOURCE.WKD,
861 ProtonMX: false, // external account
865 <ContactEmailSettingsModal
868 vCardContact={vCardContact}
869 emailProperty={vCardContact.email[0]}
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();
883 expect(screen.queryByTestId('koo-origin-label')).toBeNull();
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', () => ({
896 PublicKey: 'externally fetched mocked armored key',
897 Flags: KEY_FLAG.FLAG_NOT_COMPROMISED,
898 Source: API_KEY_SOURCE.KOO,
902 ProtonMX: false, // external account
906 <ContactEmailSettingsModal
909 vCardContact={vCardContact}
910 emailProperty={vCardContact.email[0]}
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();
923 expect(screen.queryByTestId('wkd-origin-label')).toBeNull();