import { MIME_TYPES } from '@proton/shared/lib/constants' ; import { Address , Key } from '@proton/shared/lib/interfaces' ; import { fireEvent , getByTitle } from '@testing-library/dom' ; import loudRejection from 'loud-rejection' ; import { wait } from '@proton/shared/lib/helpers/promise' ; import { act } from '@testing-library/react' ; import { clearAll , render , tick , GeneratedKey , generateKeys , addKeysToAddressKeysCache , addApiResolver , addApiMock , addApiKeys , getDropdown , addAddressToCache , minimalCache , createAttachment , decryptSessionKey , waitForNotification , parseFormData , waitForNoNotification , } from '../../../helpers/test/helper' ; import Composer from '../Composer' ; import { ID , props , prepareMessage , toAddress } from './Composer.test.helpers' ; import { arrayToBase64 } from '../../../helpers/base64' ; loudRejection ( ) ; jest . setTimeout ( 20000 ) ; describe ( 'Composer attachments' , ( ) => { let address1Keys ; let address2Keys ; const address1 = { ID : 'AddressID1' , Email : 'email1@home.net' , Keys : [ { ID : 'KeyID1' } as Key ] } as Address ; const address2 = { ID : 'AddressID2' , Email : 'email2@home.net' , Keys : [ { ID : 'KeyID2' } as Key ] } as Address ; const fileName = 'file.png' ; const fileType = 'image/png' ; const file = new File ( [ ] , fileName , { type : fileType } ) ; const attachmentData = { ID : 'AttachmentID' , Name : fileName , MIMEType : fileType } ; const setup = async ( MIMEType = MIME_TYPES . PLAINTEXT ) => { const { attachment , sessionKey : generatedSessionKey } = await createAttachment ( attachmentData , address1Keys . publicKeys ) ; const resolve = addApiResolver ( 'mail/v4/attachments' ) ; const updateSpy = jest . fn ( ( { data : { Message , AttachmentKeyPackets } } ) => { Message . Attachments . forEach ( ( Attachment ) => { if ( AttachmentKeyPackets ?. [ Attachment . ID ] ) { Attachment . KeyPackets = AttachmentKeyPackets [ Attachment . ID ] ; } } ) ; return Promise . resolve ( { Message } ) ; } ) ; addApiMock ( `mail/v4/messages/${ ID }` , updateSpy , 'put' ) ; const sendSpy = jest . fn ( ( ) => Promise . resolve ( { Sent : { } } ) ) ; addApiMock ( `mail/v4/messages/${ ID }` , sendSpy , 'post' ) ; minimalCache ( ) ; const message = prepareMessage ( { localID : ID , data : { AddressID : address1 . ID , MIMEType } , messageDocument : { plainText : 'test' } , } ) ; addApiKeys ( false , toAddress , [ ] ) ; addAddressToCache ( address1 ) ; addAddressToCache ( address2 ) ; const result = await render ( < Composer { ... props } messageID = { ID } / > , false ) ; props . onClose . mockImplementation ( result . unmount ) ; const inputAttachment = result . getByTestId ( 'composer-attachments-button' ) as HTMLInputElement ; fireEvent . change ( inputAttachment , { target : { files : [ file ] } } ) ; await tick ( ) ; return { ... result , resolve , message , attachment , generatedSessionKey , updateSpy , sendSpy } ; } ; const saveNow = async ( container ) => { fireEvent . keyDown ( container , { key : 's' , ctrlKey : true } ) ; await tick ( ) ; await act ( async ( ) => { await wait ( 3000 ) ; } ) ; } ; const switchAddress = async ( getByTestId ) => { const fromButton = getByTestId ( 'composer:from' ) as HTMLButtonElement ; fireEvent . click ( fromButton ) ; const fromDropdown = await getDropdown ( ) ; const email2Dropdown = getByTitle ( fromDropdown , address2 . Email ) ; fireEvent . click ( email2Dropdown ) ; } ; beforeAll ( async ( ) => { address1Keys = await generateKeys ( '1' , address1 . Email ) ; address2Keys = await generateKeys ( '2' , address2 . Email ) ; } ) ; beforeEach ( ( ) => { addKeysToAddressKeysCache ( address1 . ID , address1Keys ) ; addKeysToAddressKeysCache ( address2 . ID , address2Keys ) ; } ) ; afterEach ( clearAll ) ; it ( 'should not show embedded modal when plaintext mode' , async ( ) => { const { queryAllByText , findByText , resolve , container } = await setup ( ) ; const embeddedModalMatch = queryAllByText ( 'Insert image' ) ; expect ( embeddedModalMatch . length ) . toBe ( 0 ) ; const attachmentName = container . querySelector ( `[aria-label="${ fileName }"]` ) ; expect ( attachmentName ) . not . toBe ( null ) ; resolve ( { Attachment : { ID : 'AttachmentID' } } ) ; await findByText ( 'file attached' ) ; await saveNow ( container ) ; } ) ; it ( 'should show embedded modal when html mode' , async ( ) => { const { queryAllByText , getByTestId , findByText , resolve , container } = await setup ( MIME_TYPES . DEFAULT ) ; const embeddedModalMatch = queryAllByText ( 'Insert image' ) ; expect ( embeddedModalMatch . length ) . toBeGreaterThanOrEqual ( 1 ) ; const attachmentButton = getByTestId ( 'composer:insert-image-attachment' ) ; fireEvent . click ( attachmentButton ) ; await tick ( ) ; const attachmentName = container . querySelector ( `[aria-label="${ fileName }"]` ) ; expect ( attachmentName ) . not . toBe ( null ) ; resolve ( { Attachment : { ID : 'AttachmentID' } } ) ; await findByText ( 'file attached' ) ; await saveNow ( container ) ; } ) ; it ( 'should re-encrypt attachment key packets on sender address change' , async ( ) => { const { getByTestId , findByText , resolve , attachment , generatedSessionKey , container , updateSpy } = await setup ( ) ; resolve ( { Attachment : attachment } ) ; await switchAddress ( getByTestId ) ; await findByText ( 'file attached' ) ; await saveNow ( container ) ; expect ( updateSpy ) . toHaveBeenCalled ( ) ; const requestData = ( updateSpy . mock . calls [ 0 ] as any [ ] ) [ 0 ] . data ; const keyPackets = requestData . AttachmentKeyPackets [ attachment . ID as string ] ; expect ( keyPackets ) . toBeDefined ( ) ; const decryptedSessionKey = await decryptSessionKey ( keyPackets , address2Keys . privateKeys ) ; expect ( generatedSessionKey ) . toEqual ( decryptedSessionKey ) ; } ) ; it ( 'should re-encrypt attachment key packets on sender address change and send' , async ( ) => { const { message , resolve , attachment , generatedSessionKey , updateSpy , sendSpy , ... renderResult } = await setup ( ) ; await switchAddress ( renderResult . getByTestId ) ; const sendButton = await renderResult . findByTestId ( 'composer:send-button' ) ; fireEvent . click ( sendButton ) ; await tick ( ) ; await waitForNotification ( 'Sending message...' ) ; resolve ( { Attachment : attachment } ) ; await waitForNotification ( 'Message sent' ) ; await waitForNoNotification ( ) ; const requestData = ( updateSpy . mock . calls [ 0 ] as any [ ] ) [ 0 ] . data ; const keyPackets = requestData . AttachmentKeyPackets [ attachment . ID as string ] ; expect ( keyPackets ) . toBeDefined ( ) ; const sendRequest = ( sendSpy . mock . calls [ 0 ] as any [ ] ) [ 0 ] . data ; const sendData = parseFormData ( sendRequest ) ; const attachmentKey = sendData . Packages [ 'text/plain' ] . AttachmentKeys [ attachment . ID as string ] . Key ; expect ( attachmentKey ) . toBe ( arrayToBase64 ( generatedSessionKey . data ) ) ; } ) ; } ) ;