From fe4412a4d8a17c50c979931c2c15ef36e40dadde Mon Sep 17 00:00:00 2001 From: Tom J <6306961+tomholub@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:05:00 +0000 Subject: [PATCH] Revert "Issue 4870 revert individual fes msgs (#4871)" This reverts commit f043553799ad3bd54c5b5cf2c0e55aa11552de0d. --- .../compose-modules/compose-draft-module.ts | 2 +- .../encrypted-mail-msg-formatter.ts | 101 +++++++----- extension/js/common/api/account-server.ts | 7 +- .../api/account-servers/enterprise-server.ts | 5 +- test/source/mock/fes/fes-endpoints.ts | 109 ++++++++++--- test/source/mock/google/google-data.ts | 6 +- .../strategies/send-message-strategy.ts | 148 +++++++++++------- test/source/test.ts | 2 +- test/source/tests/compose.ts | 14 +- test/source/tests/flaky.ts | 55 ++++++- 10 files changed, 314 insertions(+), 135 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-draft-module.ts b/extension/chrome/elements/compose-modules/compose-draft-module.ts index 68c38132da9..09cc59769db 100644 --- a/extension/chrome/elements/compose-modules/compose-draft-module.ts +++ b/extension/chrome/elements/compose-modules/compose-draft-module.ts @@ -126,7 +126,7 @@ export class ComposeDraftModule extends ViewModule { const msgData = await this.view.inputModule.extractAll(); const { pubkeys } = await this.view.storageModule.collectSingleFamilyKeys([], msgData.from.email, true); msgData.pwd = undefined; // not needed for drafts - const sendable = await new EncryptedMsgMailFormatter(this.view, true).encryptSendableNonPwdMsg(msgData, pubkeys); + const sendable = await new EncryptedMsgMailFormatter(this.view, true).sendableNonPwdMsg(msgData, pubkeys); if (this.view.replyParams?.inReplyTo) { sendable.headers.References = this.view.replyParams.inReplyTo; sendable.headers['In-Reply-To'] = this.view.replyParams.inReplyTo; diff --git a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts index 1a326b31b65..ff506a15b08 100644 --- a/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts +++ b/extension/chrome/elements/compose-modules/formatters/encrypted-mail-msg-formatter.ts @@ -6,7 +6,7 @@ import { BaseMailFormatter } from './base-mail-formatter.js'; import { ComposerResetBtnTrigger } from '../compose-err-module.js'; import { Mime, SendableMsgBody } from '../../../../js/common/core/mime.js'; import { getUniqueRecipientEmails, NewMsgData } from '../compose-types.js'; -import { Str, Value } from '../../../../js/common/core/common.js'; +import { EmailParts, Str, Value } from '../../../../js/common/core/common.js'; import { ApiErr } from '../../../../js/common/api/shared/api-error.js'; import { Attachment } from '../../../../js/common/core/attachment.js'; import { Buf } from '../../../../js/common/core/buf.js'; @@ -21,9 +21,10 @@ import { Xss } from '../../../../js/common/platform/xss.js'; import { AcctStore } from '../../../../js/common/platform/store/acct-store.js'; import { SmimeKey } from '../../../../js/common/core/crypto/smime/smime-key.js'; import { PgpHash } from '../../../../js/common/core/crypto/pgp/pgp-hash.js'; -import { UploadedMessageResponse } from '../../../../js/common/api/account-server.js'; +import { UploadedMessageData } from '../../../../js/common/api/account-server.js'; import { ParsedKeyInfo } from '../../../../js/common/core/crypto/key-store-util.js'; import { MultipleMessages } from './general-mail-formatter.js'; +import { Api, RecipientType } from '../../../../js/common/api/shared/api.js'; /** * this type must be kept in sync with FES UI code, changes must be backwards compatible @@ -43,9 +44,9 @@ type ReplyInfoRaw = { export class EncryptedMsgMailFormatter extends BaseMailFormatter { public sendableMsgs = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingKey?: ParsedKeyInfo): Promise => { if (newMsg.pwd && !this.isDraft) { - return await this.formatSendablePwdMsg(newMsg, pubkeys, signingKey); + return await this.formatSendablePwdMsgs(newMsg, pubkeys, signingKey); } else { - const msg = await this.encryptSendableNonPwdMsg(newMsg, pubkeys, signingKey?.key); + const msg = await this.sendableNonPwdMsg(newMsg, pubkeys, signingKey?.key); return { senderKi: signingKey?.keyInfo, msgs: [msg], @@ -57,7 +58,7 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { } }; - public encryptSendableNonPwdMsg = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingPrv?: Key): Promise => { + public sendableNonPwdMsg = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingPrv?: Key): Promise => { if (!this.isDraft) { // S/MIME drafts are currently formatted with inline armored data const x509certs = pubkeys.map(entry => entry.pubkey).filter(pub => pub.family === 'x509'); @@ -91,42 +92,70 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { }); }; - private formatSendablePwdMsg = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingKey?: ParsedKeyInfo) => { + private formatSendablePwdMsgs = async (newMsg: NewMsgData, pubkeys: PubkeyResult[], signingKey?: ParsedKeyInfo) => { // password-protected message, temporarily uploaded (already encrypted) to: // - flowcrypt.com/api (consumers and customers without on-prem setup), or - // - FlowCrypt External Service at fes.example.com (enterprise customers with on-prem setup) + // - FlowCrypt Enterprise Server (enterprise customers with on-prem setup) // It will be served to recipient through web - const uploadedMessageResponse = await this.prepareEncryptAndUploadPwdEncryptedMsg(newMsg); // encrypted for pwd only, pubkeys ignored + const uploadedMessageData = await this.prepareAndUploadPwdEncryptedMsg(newMsg); // encrypted for pwd only, pubkeys ignored + // pwdRecipients that have their personal link + const individualPwdRecipients = Object.keys(uploadedMessageData.emailToExternalIdAndUrl ?? {}).filter(email => !pubkeys.some(p => p.email === email)); + const legacyPwdRecipients: { [type in RecipientType]?: EmailParts[] } = {}; newMsg.pwd = undefined; - // We used to support sending individual messages to each of the password recipients, each with unique link - // as per https://github.com/FlowCrypt/flowcrypt-browser/issues/4348. We later reverted that behavior in - // https://github.com/FlowCrypt/flowcrypt-browser/issues/4870. Therefore currently it's a single message - // again even though the code could support multiple messages. If by 2024 there is still support for multiple - // messages that is unused, then this can be refactored back to a single message - // (`SendableMsg[]` to `SendableMsg` and so on, including error handling which is much simpler when there is - // just one message to send) - const msg = await this.sendableCombinedPubkeyMsgWithoutAttachedFilesWithLinkToUploadedPwdMsg( - newMsg, - pubkeys, - { msgUrl: uploadedMessageResponse.url, externalId: uploadedMessageResponse.externalId }, - signingKey?.key - ); - // the above message has pgp/mime encrypted content that was attached as a set of pgp/mime attachments, - // but doesn't have the actual attachments the user has attached (they were uploaded to FES/backend but weren't - // attached to the message itself). We are adding the attachments here. - const pubkeyEncryptedAttachments = await this.view.attachmentsModule.attachment.collectEncryptAttachments(pubkeys); - msg.attachments = msg.attachments.concat(pubkeyEncryptedAttachments); + const encryptedAttachments = await this.view.attachmentsModule.attachment.collectEncryptAttachments(pubkeys); + const pubkeyRecipients: { [type in RecipientType]?: EmailParts[] } = {}; + for (const [sendingType, value] of Object.entries(newMsg.recipients)) { + if (Api.isRecipientHeaderNameType(sendingType)) { + pubkeyRecipients[sendingType] = value?.filter(emailPart => pubkeys.some(p => p.email === emailPart.email)); + legacyPwdRecipients[sendingType] = value?.filter( + emailPart => !pubkeys.some(p => p.email === emailPart.email) && !individualPwdRecipients.includes(emailPart.email) + ); + } + } + const msgs: SendableMsg[] = []; + // pubkey recipients get one combined message. If there are not pubkey recpients, only password - protected messages will be sent + if (pubkeyRecipients.to?.length || pubkeyRecipients.cc?.length || pubkeyRecipients.bcc?.length) { + const uniquePubkeyRecipientToAndCCs = Value.arr.unique( + (pubkeyRecipients.to || []).concat(pubkeyRecipients.cc || []).map(recipient => recipient.email.toLowerCase()) + ); + // pubkey recipients should be able to reply to "to" and "cc" pwd recipients + const replyToForMessageSentToPubkeyRecipients = (newMsg.recipients.to ?? []) + .concat(newMsg.recipients.cc ?? []) + .filter(recipient => !uniquePubkeyRecipientToAndCCs.includes(recipient.email.toLowerCase())); + const pubkeyMsgData = { + ...newMsg, + recipients: pubkeyRecipients, + // brackets are required for test emails like '@test:8001' + replyTo: replyToForMessageSentToPubkeyRecipients.length + ? `${Str.formatEmailList([newMsg.from, ...replyToForMessageSentToPubkeyRecipients], true)}` + : undefined, + }; + msgs.push(await this.sendableNonPwdMsg(pubkeyMsgData, pubkeys, signingKey?.key)); + } + // adding individual messages for each recipient that doesn't have a pubkey + for (const recipientEmail of individualPwdRecipients) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { url, externalId } = uploadedMessageData.emailToExternalIdAndUrl![recipientEmail]; + const foundParsedRecipient = (newMsg.recipients.to ?? []) + .concat(newMsg.recipients.cc ?? []) + .concat(newMsg.recipients.bcc ?? []) + .find(r => r.email.toLowerCase() === recipientEmail.toLowerCase()); + // todo: since a message is allowed to have only `cc` or `bcc` without `to`, should we preserve the original placement(s) of the recipient? + const individualMsgData = { ...newMsg, recipients: { to: [foundParsedRecipient ?? { email: recipientEmail }] } }; + msgs.push(await this.sendablePwdMsg(individualMsgData, pubkeys, { msgUrl: url, externalId }, signingKey?.key)); + } + if (legacyPwdRecipients.to?.length || legacyPwdRecipients.cc?.length || legacyPwdRecipients.bcc?.length) { + const legacyPwdMsgData = { ...newMsg, recipients: legacyPwdRecipients }; + msgs.push(await this.sendablePwdMsg(legacyPwdMsgData, pubkeys, { msgUrl: uploadedMessageData.url }, signingKey?.key)); + } return { senderKi: signingKey?.keyInfo, - msgs: [msg], - renderSentMessage: { - recipients: newMsg.recipients, - attachments: pubkeyEncryptedAttachments - }, + msgs, + renderSentMessage: { recipients: newMsg.recipients, attachments: encryptedAttachments }, }; }; - private prepareEncryptAndUploadPwdEncryptedMsg = async (newMsg: NewMsgData): Promise => { + private prepareAndUploadPwdEncryptedMsg = async (newMsg: NewMsgData): Promise => { // PGP/MIME + included attachments (encrypted for password only) if (!newMsg.pwd) { throw new Error('password unexpectedly missing'); @@ -163,19 +192,17 @@ export class EncryptedMsgMailFormatter extends BaseMailFormatter { { Subject: newMsg.subject }, // eslint-disable-line @typescript-eslint/naming-convention await this.view.attachmentsModule.attachment.collectAttachments() ); - // encrypted only for pwd, not signed - const { data: pwdEncryptedWithAttachments } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAttachments), newMsg.pwd, []); + const { data: pwdEncryptedWithAttachments } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeWithAttachments), newMsg.pwd, []); // encrypted only for pwd, not signed return await this.view.acctServer.messageUpload( pwdEncryptedWithAttachments, replyToken, newMsg.from.email, // todo: Str.formatEmailWithOptionalName? newMsg.recipients, - // still need to upload to Gmail later, this request represents first half of progress - p => this.view.sendBtnModule.renderUploadProgress(p, 'FIRST-HALF') + p => this.view.sendBtnModule.renderUploadProgress(p, 'FIRST-HALF') // still need to upload to Gmail later, this request represents first half of progress ); }; - private sendableCombinedPubkeyMsgWithoutAttachedFilesWithLinkToUploadedPwdMsg = async ( + private sendablePwdMsg = async ( newMsg: NewMsgData, pubs: PubkeyResult[], { msgUrl, externalId }: { msgUrl: string; externalId?: string }, diff --git a/extension/js/common/api/account-server.ts b/extension/js/common/api/account-server.ts index 6098cb36191..c575fe91e08 100644 --- a/extension/js/common/api/account-server.ts +++ b/extension/js/common/api/account-server.ts @@ -11,9 +11,10 @@ import { ParsedRecipients } from './email-provider/email-provider-api.js'; import { BackendAuthErr } from './shared/api-error.js'; import { Api, ProgressCb } from './shared/api.js'; -export type UploadedMessageResponse = { +export type UploadedMessageData = { url: string; // both FES and FlowCryptComApi - externalId?: string; // FES + externalId?: string; // legacy FES + emailToExternalIdAndUrl?: { [email: string]: { url: string; externalId: string } }; // FES only }; /** @@ -58,7 +59,7 @@ export class AccountServer extends Api { from: string, recipients: ParsedRecipients, progressCb: ProgressCb - ): Promise => { + ): Promise => { if (await this.isFesUsed()) { const fes = new EnterpriseServer(this.acctEmail); // Recipients are used to later cross-check replies from the web diff --git a/extension/js/common/api/account-servers/enterprise-server.ts b/extension/js/common/api/account-servers/enterprise-server.ts index 80b5eb4eaf8..841a3fa642d 100644 --- a/extension/js/common/api/account-servers/enterprise-server.ts +++ b/extension/js/common/api/account-servers/enterprise-server.ts @@ -20,8 +20,9 @@ type EventTag = 'compose' | 'decrypt' | 'setup' | 'settings' | 'import-pub' | 'i export namespace FesRes { export type ReplyToken = { replyToken: string }; export type MessageUpload = { - url: string; - externalId: string; + url: string; // LEGACY + externalId: string; // LEGACY + emailToExternalIdAndUrl?: { [email: string]: { url: string; externalId: string } }; }; export type ServiceInfo = { vendor: string; service: string; orgId: string; version: string; apiVersion: string }; export type ClientConfiguration = { clientConfiguration: ClientConfigurationJson }; diff --git a/test/source/mock/fes/fes-endpoints.ts b/test/source/mock/fes/fes-endpoints.ts index 501b7a9cdd8..40011a62abd 100644 --- a/test/source/mock/fes/fes-endpoints.ts +++ b/test/source/mock/fes/fes-endpoints.ts @@ -34,11 +34,21 @@ const processMessageFromUser = async (body: string) => { // small.txt expect(decryptedMimeMsg).to.contain('Content-Type: text/plain; name=small.txt\r\n' + 'Content-Disposition: attachment; filename=small.txt'); expect(decryptedMimeMsg).to.contain('Content-Transfer-Encoding: base64\r\n\r\n' + 'c21hbGwgdGV4dCBmaWxlCm5vdCBtdWNoIGhlcmUKdGhpcyB3b3JrZWQK'); - return { + const response = { // this url is required for pubkey encrypted message url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID`, externalId: 'FES-MOCK-EXTERNAL-ID', + emailToExternalIdAndUrl: {} as { [email: string]: { url: string; externalId: string } }, }; + response.emailToExternalIdAndUrl['to@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-TO@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-TO@EXAMPLE.COM-ID', + }; + response.emailToExternalIdAndUrl['bcc@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-BCC@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-BCC@EXAMPLE.COM-ID', + }; + return response; }; const processMessageFromUser2 = async (body: string) => { @@ -66,11 +76,29 @@ const processMessageFromUser2 = async (body: string) => { expect(decryptedMimeMsg).to.contain( 'Content-Transfer-Encoding: base64\r\n\r\n' + 'JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl' ); - return { + const response = { // this url is required for pubkey encrypted message url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID`, externalId: 'FES-MOCK-EXTERNAL-ID', + emailToExternalIdAndUrl: {} as { [email: string]: { url: string; externalId: string } }, + }; + response.emailToExternalIdAndUrl['to@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-TO@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-TO@EXAMPLE.COM-ID', + }; + response.emailToExternalIdAndUrl['sender@domain.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-SENDER@DOMAIN.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-SENDER@DOMAIN.COM-ID', + }; + response.emailToExternalIdAndUrl['flowcrypt.compatibility@gmail.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-FLOWCRYPT.COMPATIBILITY@GMAIL.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-FLOWCRYPT.COMPATIBILITY@GMAIL.COM-ID', + }; + response.emailToExternalIdAndUrl['mock.only.pubkey@flowcrypt.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-MOCK.ONLY.PUBKEY@FLOWCRYPT.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-MOCK.ONLY.PUBKEY@FLOWCRYPT.COM-ID', }; + return response; }; const processMessageFromUser3 = async (body: string) => { @@ -92,15 +120,69 @@ const processMessageFromUser3 = async (body: string) => { const decryptedMimeMsg = decrypted.content!.toUtfStr(); // small.txt expect(decryptedMimeMsg).to.contain( - 'Content-Type: text/plain\r\n' - + 'Content-Transfer-Encoding: quoted-printable\r\n\r\n' - + 'PWD encrypted message with FES - pubkey recipient in bcc' + 'Content-Type: text/plain\r\n' + 'Content-Transfer-Encoding: quoted-printable\r\n\r\n' + 'PWD encrypted message with FES - pubkey recipient in bcc' ); - return { + const response = { // this url is required for pubkey encrypted message url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID`, externalId: 'FES-MOCK-EXTERNAL-ID', + emailToExternalIdAndUrl: {} as { [email: string]: { url: string; externalId: string } }, }; + response.emailToExternalIdAndUrl['to@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-TO@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-TO@EXAMPLE.COM-ID', + }; + response.emailToExternalIdAndUrl['flowcrypt.compatibility@gmail.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-FLOWCRYPT.COMPATIBILITY@GMAIL.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-FLOWCRYPT.COMPATIBILITY@GMAIL.COM-ID', + }; + return response; +}; + +const processMessageFromUser4 = async (body: string) => { + const response = { + // this url is required for pubkey encrypted message + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID`, + externalId: 'FES-MOCK-EXTERNAL-ID', + emailToExternalIdAndUrl: {} as { [email: string]: { url: string; externalId: string } }, + }; + if (body.includes('to@example.com')) { + response.emailToExternalIdAndUrl['to@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-TO@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-TO@EXAMPLE.COM-ID', + }; + } + if (body.includes('invalid@example.com')) { + response.emailToExternalIdAndUrl['invalid@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-INVALID@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-INVALID@EXAMPLE.COM-ID', + }; + } + if (body.includes('timeout@example.com')) { + response.emailToExternalIdAndUrl['timeout@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-TIMEOUT@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-TIMEOUT@EXAMPLE.COM-ID', + }; + } + if (body.includes('Mr Cc ')) { + response.emailToExternalIdAndUrl['cc@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-CC@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-CC@EXAMPLE.COM-ID', + }; + } + if (body.includes('First Last ')) { + response.emailToExternalIdAndUrl['flowcrypt.compatibility@gmail.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-FLOWCRYPT.COMPATIBILITY@GMAIL.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-FLOWCRYPT.COMPATIBILITY@GMAIL.COM-ID', + }; + } + if (body.includes('gatewayfailure@example.com')) { + response.emailToExternalIdAndUrl['gatewayfailure@example.com'] = { + url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-FOR-GATEWAYFAILURE@EXAMPLE.COM-ID`, + externalId: 'FES-MOCK-EXTERNAL-FOR-GATEWAYFAILURE@EXAMPLE.COM-ID', + }; + } + return response; }; export const mockFesEndpoints: HandlersDefinition = { @@ -151,13 +233,6 @@ export const mockFesEndpoints: HandlersDefinition = { if (req.headers.host === standardFesUrl && req.method === 'POST' && typeof body === 'string') { // test: `compose - user@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal` authenticate(req, 'oidc'); - if (body.includes('gatewayfailure@example.com')) { // recipient - return { - // this url is required for pubkey encrypted message - url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID`, - externalId: 'FES-MOCK-EXTERNAL-FOR-GATEWAYFAILURE@EXAMPLE.COM-ID', - } - } if (body.includes('"from":"user@standardsubdomainfes.localhost:8001"')) { return await processMessageFromUser(body); } @@ -168,11 +243,7 @@ export const mockFesEndpoints: HandlersDefinition = { return await processMessageFromUser3(body); } if (body.includes('"from":"user4@standardsubdomainfes.localhost:8001"')) { - return { - // this url is required for pubkey encrypted message - url: `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID`, - externalId: 'FES-MOCK-EXTERNAL-ID', - } + return await processMessageFromUser4(body); } } throw new HttpClientErr('Not Found', 404); @@ -217,7 +288,7 @@ export const mockFesEndpoints: HandlersDefinition = { throw new HttpClientErr('Not Found', 404); }, '/api/v1/message/FES-MOCK-EXTERNAL-FOR-GATEWAYFAILURE@EXAMPLE.COM-ID/gateway': async () => { - // test: `user4@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal - send fails with gateway update error` + // test: `user4@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal - a send fails with gateway update error` throw new HttpClientErr(`Test error`, Status.BAD_REQUEST); }, }; diff --git a/test/source/mock/google/google-data.ts b/test/source/mock/google/google-data.ts index e761bb7501f..b40894c24b4 100644 --- a/test/source/mock/google/google-data.ts +++ b/test/source/mock/google/google-data.ts @@ -282,11 +282,7 @@ export class GoogleData { payload: { headers: [ { name: 'Subject', value: parsedMail.subject || '' }, - // todo - Gmail doesn't retain message ids we give it in the MIME message. - // therefore ideally, we would not retain them in mock either, to be consistent - // in a separate PR try to remove `parsedMail.messageId || ` below - // see if it breaks any tests - { name: 'Message-ID', value: parsedMail.messageId || `mock-gmail-message-id-${Util.lousyRandom()}` }, + { name: 'Message-ID', value: parsedMail.messageId || '' }, ], body, }, diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index 27dee6db6a8..272dcaf64c8 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -28,21 +28,27 @@ class PwdAndPubkeyEncryptedMessagesWithFlowCryptComApiTestStrategy implements IT public test = async (parseResult: ParseMsgResult, id: string) => { const mimeMsg = parseResult.mimeMsg; const senderEmail = Str.parseEmail(mimeMsg.from!.text).email; - expect(mimeMsg.text!).to.contain(`${senderEmail} has sent you a password-encrypted email`); - expect(mimeMsg.text!).to.contain('Follow this link to open it'); - if (!mimeMsg.text?.match(/https:\/\/flowcrypt.com\/[a-z0-9A-Z]{10}/)) { - throw new HttpClientErr(`Error: cannot find pwd encrypted flowcrypt.com/api link in:\n\n${mimeMsg.text}`); - } - const kisWithPp = await Config.getKeyInfo(['flowcrypt.compatibility.1pp1', 'flowcrypt.compatibility.2pp1']); - const encryptedData = mimeMsg.attachments.find(a => a.filename === 'encrypted.asc')!.content; - const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData, verificationPubs: [] }); - expect(decrypted.success).to.be.true; - expect(decrypted.content!.toUtfStr()).to.contain('PWD and pubkey encrypted messages with flowcrypt.com/api'); - expect((mimeMsg.to as AddressObject).text!).to.include('test@email.com'); - expect((mimeMsg.cc as AddressObject).text!).to.include('flowcrypt.compatibility@gmail.com'); - expect(mimeMsg.bcc).to.be.an.undefined; - expect(mimeMsg.headers.get('reply-to')).to.be.undefined; await new SaveMessageInStorageStrategy().test(parseResult, id); + if (mimeMsg.cc) { + // this is a message to the pubkey recipient + expect((mimeMsg.cc as AddressObject).text!).to.include('flowcrypt.compatibility@gmail.com'); + expect(mimeMsg.text!).to.not.include('has sent you a password-encrypted email'); + expect(mimeMsg.text!).to.not.include('Follow this link to open it'); + const kisWithPp = await Config.getKeyInfo(['flowcrypt.compatibility.1pp1', 'flowcrypt.compatibility.2pp1']); + const encryptedData = Buf.fromUtfStr(mimeMsg.text!); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData, verificationPubs: [] }); + expect(decrypted.success).to.be.true; + expect(decrypted.content!.toUtfStr()).to.contain('PWD and pubkey encrypted messages with flowcrypt.com/api'); + expect(mimeMsg.bcc).to.be.an.undefined; + expect(mimeMsg.to).to.be.an.undefined; + expect((mimeMsg.headers.get('reply-to') as AddressObject).text).to.equal('First Last , test@email.com'); + } else { + expect(mimeMsg.text!).to.contain(`${senderEmail} has sent you a password-encrypted email`); + expect(mimeMsg.text!).to.contain('Follow this link to open it'); + if (!mimeMsg.text?.match(/https:\/\/flowcrypt.com\/[a-z0-9A-Z]{10}/)) { + throw new HttpClientErr(`Error: cannot find pwd encrypted flowcrypt.com/api link in:\n\n${mimeMsg.text}`); + } + } }; } class PwdEncryptedMessageWithFlowCryptComApiTestStrategy implements ITestMsgStrategy { @@ -66,16 +72,20 @@ class PwdEncryptedMessageWithFesIdTokenTestStrategy implements ITestMsgStrategy const mimeMsg = parseResult.mimeMsg; const expectedSenderEmail = 'user@standardsubdomainfes.localhost:8001'; expect(mimeMsg.from!.text).to.equal(`First Last <${expectedSenderEmail}>`); - expect((mimeMsg.to as AddressObject).text).to.equal('Mr To '); - expect(mimeMsg.cc).to.be.an.undefined; - expect((mimeMsg.bcc as AddressObject).text).to.equal('Mr Bcc '); + if (mimeMsg.text?.includes('http://fes.standardsubdomainfes.localhost:8001/message/FES-MOCK-MESSAGE-FOR-TO@EXAMPLE.COM-ID')) { + expect((mimeMsg.to as AddressObject).text).to.equal('Mr To '); + expect(mimeMsg.cc).to.be.an.undefined; + expect(mimeMsg.bcc).to.be.an.undefined; + } else if (mimeMsg.text?.includes('http://fes.standardsubdomainfes.localhost:8001/message/FES-MOCK-MESSAGE-FOR-BCC@EXAMPLE.COM-ID')) { + expect((mimeMsg.to as AddressObject).text).to.equal('Mr Bcc '); + expect(mimeMsg.cc).to.be.an.undefined; + expect(mimeMsg.bcc).to.be.an.undefined; + } else { + // no pubkey recipients in this test + throw new HttpClientErr(`Error: cannot find pwd encrypted FES link in:\n\n${mimeMsg.text}`); + } expect(mimeMsg.text!).to.include(`${expectedSenderEmail} has sent you a password-encrypted email`); expect(mimeMsg.text!).to.include('Follow this link to open it'); - const kisWithPp = await Config.getKeyInfo(['flowcrypt.test.key.used.pgp']); - const encryptedData = mimeMsg.attachments.find(a => a.filename === 'encrypted.asc')!.content; - const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData, verificationPubs: [] }); - expect(decrypted.success).to.be.true; - expect(decrypted.content!.toUtfStr()).to.contain('PWD encrypted message with FES - ID TOKEN'); await new SaveMessageInStorageStrategy().test(parseResult, id); }; } @@ -85,38 +95,51 @@ class PwdEncryptedMessageWithFesPubkeyRecipientInBccTestStrategy implements ITes const mimeMsg = parseResult.mimeMsg; const expectedSenderEmail = 'user3@standardsubdomainfes.localhost:8001'; expect(mimeMsg.from!.text).to.equal(`First Last <${expectedSenderEmail}>`); - // originally fes - expect(mimeMsg.text!).to.include(`${expectedSenderEmail} has sent you a password-encrypted email`); - expect(mimeMsg.text!).to.include('Follow this link to open it'); - expect((mimeMsg.to as AddressObject).text).to.equal('to@example.com'); - expect(mimeMsg.cc).to.be.an.undefined; - expect((mimeMsg.bcc as AddressObject).text).to.equal('flowcrypt.compatibility@gmail.com'); - expect(mimeMsg.headers.get('reply-to')).to.be.an.undefined; - const kisWithPp = await Config.getKeyInfo(['flowcrypt.test.key.used.pgp']); - const encryptedData = mimeMsg.attachments.find(a => a.filename === 'encrypted.asc')!.content; - const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData, verificationPubs: [] }); - expect(decrypted.success).to.be.true; - expect(decrypted.content!.toUtfStr()).to.contain('PWD encrypted message with FES - pubkey recipient'); - expect(mimeMsg.headers.get('reply-to')).to.be.undefined; + if (mimeMsg.text?.includes('http://fes.standardsubdomainfes.localhost:8001/message/FES-MOCK-MESSAGE-FOR-TO@EXAMPLE.COM-ID')) { + expect(mimeMsg.text!).to.include(`${expectedSenderEmail} has sent you a password-encrypted email`); + expect(mimeMsg.text!).to.include('Follow this link to open it'); + expect((mimeMsg.to as AddressObject).text).to.equal('to@example.com'); + expect(mimeMsg.cc).to.be.an.undefined; + expect(mimeMsg.bcc).to.be.an.undefined; + expect(mimeMsg.headers.get('reply-to')).to.be.an.undefined; + } else { + // this is a message to pubkey recipients + expect(mimeMsg.text!).to.not.include('has sent you a password-encrypted email'); + expect(mimeMsg.text!).to.not.include('Follow this link to open it'); + const kisWithPp = await Config.getKeyInfo(['flowcrypt.test.key.used.pgp']); + const encryptedData = Buf.fromUtfStr(mimeMsg.text!); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData, verificationPubs: [] }); + expect(decrypted.success).to.be.true; + expect(decrypted.content!.toUtfStr()).to.equal('PWD encrypted message with FES - pubkey recipient in bcc'); + expect((mimeMsg.bcc as AddressObject).text).to.equal('flowcrypt.compatibility@gmail.com'); + expect(mimeMsg.cc).to.be.an.undefined; + expect(mimeMsg.to).to.be.an.undefined; + expect((mimeMsg.headers.get('reply-to') as AddressObject).text).to.equal('First Last , to@example.com'); + } await new SaveMessageInStorageStrategy().test(parseResult, id); }; } class PwdEncryptedMessageWithFesReplyBadRequestTestStrategy implements ITestMsgStrategy { - public test = async (parseResult: ParseMsgResult) => { + public test = async (parseResult: ParseMsgResult, id: string) => { const mimeMsg = parseResult.mimeMsg; const expectedSenderEmail = 'user4@standardsubdomainfes.localhost:8001'; expect(mimeMsg.from!.text).to.equal(`First Last <${expectedSenderEmail}>`); const to = parsedMailAddressObjectAsArray(mimeMsg.to) .concat(parsedMailAddressObjectAsArray(mimeMsg.cc)) .concat(parsedMailAddressObjectAsArray(mimeMsg.bcc)); - const recipientEmails = to.map(to => to.text) - if (recipientEmails.includes('invalid@example.com')) { + expect(to.length).to.equal(1); + const recipientEmail = to[0].text; + if (recipientEmail === 'to@example.com') { + // success + await new SaveMessageInStorageStrategy().test(parseResult, id); + return; + } else if (recipientEmail === 'invalid@example.com') { throw new HttpClientErr('Invalid to header', Status.BAD_REQUEST); - } else if (recipientEmails.includes('timeout@example.com')) { + } else if (recipientEmail === 'timeout@example.com') { throw new HttpClientErr('RequestTimeout', Status.BAD_REQUEST); } else { - throw new HttpClientErr(`Vague failure for ${recipientEmails.join(',')}`, Status.BAD_REQUEST); + throw new HttpClientErr(`Vague failure for ${recipientEmail}`, Status.BAD_REQUEST); } }; } @@ -126,17 +149,36 @@ class PwdEncryptedMessageWithFesReplyRenderingTestStrategy implements ITestMsgSt const mimeMsg = parseResult.mimeMsg; const expectedSenderEmail = 'user2@standardsubdomainfes.localhost:8001'; expect(mimeMsg.from!.text).to.equal(`First Last <${expectedSenderEmail}>`); - expect(mimeMsg.text).to.include(`${expectedSenderEmail} has sent you a password-encrypted email`); - expect(mimeMsg.text).to.include('Follow this link to open it'); - expect((mimeMsg.to as AddressObject).text).to.equal('sender@domain.com, flowcrypt.compatibility@gmail.com, to@example.com, mock.only.pubkey@flowcrypt.com'); - expect(mimeMsg.cc).to.be.an.undefined; - expect(mimeMsg.bcc).to.be.an.undefined; - expect(mimeMsg.headers.get('reply-to')).to.be.an.undefined; - const kisWithPp = await Config.getKeyInfo(['flowcrypt.test.key.used.pgp']); - const encryptedData = mimeMsg.attachments.find(a => a.filename === 'encrypted.asc')!.content; - const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData, verificationPubs: [] }); - expect(decrypted.success).to.be.true; - expect(decrypted.content!.toUtfStr()).to.include('> some dummy text'); + if (mimeMsg.text?.includes('http://fes.standardsubdomainfes.localhost:8001/message/FES-MOCK-MESSAGE-FOR-SENDER@DOMAIN.COM-ID')) { + expect(mimeMsg.text!).to.include(`${expectedSenderEmail} has sent you a password-encrypted email`); + expect(mimeMsg.text!).to.include('Follow this link to open it'); + expect((mimeMsg.to as AddressObject).text).to.equal('sender@domain.com'); + expect(mimeMsg.cc).to.be.an.undefined; + expect(mimeMsg.bcc).to.be.an.undefined; + expect(mimeMsg.headers.get('reply-to')).to.be.an.undefined; + } else if (mimeMsg.text?.includes('http://fes.standardsubdomainfes.localhost:8001/message/FES-MOCK-MESSAGE-FOR-TO@EXAMPLE.COM-ID')) { + expect(mimeMsg.text!).to.include(`${expectedSenderEmail} has sent you a password-encrypted email`); + expect(mimeMsg.text!).to.include('Follow this link to open it'); + expect((mimeMsg.to as AddressObject).text).to.equal('to@example.com'); + expect(mimeMsg.cc).to.be.an.undefined; + expect(mimeMsg.bcc).to.be.an.undefined; + expect(mimeMsg.headers.get('reply-to')).to.be.an.undefined; + } else { + // this is a message to pubkey recipients + expect(mimeMsg.text!).to.not.include('has sent you a password-encrypted email'); + expect(mimeMsg.text!).to.not.include('Follow this link to open it'); + const kisWithPp = await Config.getKeyInfo(['flowcrypt.test.key.used.pgp']); + const encryptedData = Buf.fromUtfStr(mimeMsg.text!); + const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData, verificationPubs: [] }); + expect(decrypted.success).to.be.true; + expect(decrypted.content!.toUtfStr()).to.include('> some dummy text'); + expect((mimeMsg.to as AddressObject).text).to.equal('flowcrypt.compatibility@gmail.com, mock.only.pubkey@flowcrypt.com'); + expect(mimeMsg.cc).to.be.an.undefined; + expect(mimeMsg.bcc).to.be.an.undefined; + expect((mimeMsg.headers.get('reply-to') as AddressObject).text).to.equal( + 'First Last , sender@domain.com, to@example.com' + ); + } await new SaveMessageInStorageStrategy().test(parseResult, id); }; } @@ -344,9 +386,9 @@ export class TestBySubjectStrategyContext { this.strategy = new PwdEncryptedMessageWithFesReplyRenderingTestStrategy(); } else if (subject.includes('PWD encrypted message with FES - pubkey recipient in bcc')) { this.strategy = new PwdEncryptedMessageWithFesPubkeyRecipientInBccTestStrategy(); - } else if (subject.includes('PWD encrypted message with FES web portal - send fails with BadRequest error')) { + } else if (subject.includes('PWD encrypted message with FES web portal - some sends fail with BadRequest error')) { this.strategy = new PwdEncryptedMessageWithFesReplyBadRequestTestStrategy(); - } else if (subject.includes('PWD encrypted message with FES web portal - send fails with gateway update error')) { + } else if (subject.includes('PWD encrypted message with FES web portal - a send fails with gateway update error')) { this.strategy = new SaveMessageInStorageStrategy(); } else if (subject.includes('Message With Image')) { this.strategy = new SaveMessageInStorageStrategy(); diff --git a/test/source/test.ts b/test/source/test.ts index 56e63cb39c9..4ddf208fe3a 100644 --- a/test/source/test.ts +++ b/test/source/test.ts @@ -146,7 +146,7 @@ ava.default.after.always('evaluate Catch.reportErr errors', async t => { 'BrowserMsg(ajax) Bad Request: 400 when GET-ing https://localhost:8001/flowcrypt-email-key-manager/v1/keys/private (no body): -> RequestTimeout', ].includes(e.message) ) - // below for test "user4@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal - send fails with gateway update error" + // below for test "user4@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal - a send fails with gateway update error" .filter(e => !e.message.includes('Test error')) // below for test "no.fes@example.com - skip FES on consumer, show friendly message on enterprise" .filter(e => !e.trace.includes('-1 when GET-ing https://fes.example.com')) diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 68a4842bdf0..7c33a3ea245 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -34,7 +34,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te const acct = 'flowcrypt.compatibility@gmail.com'; const msgPwd = 'super hard password for the message'; const subject = 'PWD and pubkey encrypted messages with flowcrypt.com/api'; - const expectedNumberOfPassedMessages = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length + 1; + const expectedNumberOfPassedMessages = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length + 2; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility'); await ComposePageRecipe.selectFromOption(composePage, acct); await ComposePageRecipe.fillMsg(composePage, { to: 'test@email.com', cc: 'flowcrypt.compatibility@gmail.com' }, subject); @@ -2434,7 +2434,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await composePage.waitAndClick('@action-send', { delay: 1 }); await ComposePageRecipe.closed(composePage); const sentMsgs = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject); - expect(sentMsgs.length).to.equal(1); + expect(sentMsgs.length).to.equal(2); // this test is using PwdEncryptedMessageWithFesIdTokenTestStrategy to check sent result based on subject "PWD encrypted message with FES - ID TOKEN" // also see '/api/v1/message' in fes-endpoints.ts mock }) @@ -2487,7 +2487,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te 'to: sender@domain.com, flowcrypt.compatibility@gmail.com, to@example.com, mock.only.pubkey@flowcrypt.com' ); const sentMsgs = (await GoogleData.withInitializedData(acct)).getMessagesByThread('1803be2e506153d2'); - expect(sentMsgs.length).to.equal(2); // 1 original message to reply to, 1 new sent + expect(sentMsgs.length).to.equal(4); // 1 original + 3 newly sent const attachmentFrames = (composePage.target as Page).frames(); expect(attachmentFrames.length).to.equal(3); // 1 pgp block + 2 attachments expect( @@ -2533,14 +2533,14 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await composePage.waitAndClick('@action-send', { delay: 1 }); await ComposePageRecipe.closed(composePage); const sentMsgs = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject); - expect(sentMsgs.length).to.equal(1); + expect(sentMsgs.length).to.equal(2); // this test is using PwdEncryptedMessageWithFesPubkeyRecipientInBccTestStrategy to check sent result based on subject "PWD encrypted message with FES - pubkey recipient in bcc" // also see '/api/v1/message' in fes-endpoints.ts mock }) ); ava.default( - 'user4@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal - send fails with gateway update error', + 'user4@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal - a send fails with gateway update error', testWithBrowser(undefined, async (t, browser) => { const acct = 'user4@standardsubdomainfes.localhost:8001'; // added port to trick extension into calling the mock const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); @@ -2550,7 +2550,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te { submitPubkey: false, usedPgpBefore: false }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false } ); - const subject = 'PWD encrypted message with FES web portal - send fails with gateway update error - ' + testVariant; + const subject = 'PWD encrypted message with FES web portal - a send fails with gateway update error - ' + testVariant; const expectedNumberOfPassedMessages = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'user4@standardsubdomainfes.localhost:8001'); await ComposePageRecipe.fillMsg(composePage, { to: 'gatewayfailure@example.com' }, subject); @@ -2559,7 +2559,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te await composePage.waitForContent('.ui-toast-title', 'Failed to bind Gateway ID of the message:'); await composePage.close(); expect((await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length).to.equal(expectedNumberOfPassedMessages + 1); - // this test is using SaveMessageInStorageStrategy to check sent result based on subject "PWD encrypted message with FES web portal - send fails with gateway update error" + // this test is using PwdEncryptedMessageWithFesReplyGatewayErrorTestStrategy to check sent result based on subject "PWD encrypted message with FES web portal - a send fails with gateway update error" // also see '/api/v1/message' in fes-endpoints.ts mock }) ); diff --git a/test/source/tests/flaky.ts b/test/source/tests/flaky.ts index 3b058c738df..fb55220b1f3 100644 --- a/test/source/tests/flaky.ts +++ b/test/source/tests/flaky.ts @@ -202,7 +202,7 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test ); ava.default( - 'user4@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal - send fails with BadRequest error', + 'user4@standardsubdomainfes.localhost:8001 - PWD encrypted message with FES web portal - some sends fail with BadRequest error', testWithBrowser(undefined, async (t, browser) => { const acct = 'user4@standardsubdomainfes.localhost:8001'; // added port to trick extension into calling the mock const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); @@ -221,10 +221,23 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test /* eslint-enable @typescript-eslint/no-explicit-any */ }); await dbPage.close(); - const subject = 'PWD encrypted message with FES web portal - send fails with BadRequest error - ' + testVariant; - const expectedNumberOfPassedMessages = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length; - // 1. vague Gmail error with all failures + const subject = 'PWD encrypted message with FES web portal - some sends fail with BadRequest error - ' + testVariant; + let expectedNumberOfPassedMessages = (await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length; + // 1. vague Gmail error with partial success let composePage = await ComposePageRecipe.openStandalone(t, browser, 'user4@standardsubdomainfes.localhost:8001'); + await ComposePageRecipe.fillMsg(composePage, { to: 'to@example.com', cc: 'cc@example.com', bcc: 'flowcrypt.compatibility@gmail.com' }, subject); + await composePage.waitAndType('@input-password', 'gO0d-pwd'); + await composePage.waitAndClick('@action-send', { delay: 1 }); + await composePage.waitAndRespondToModal( + 'confirm', + 'cancel', + 'Messages to some recipients were sent successfully, while messages to flowcrypt.compatibility@gmail.com, Mr Cc ' + + 'encountered error(s) from Gmail. Please help us improve FlowCrypt by reporting the error to us.' + ); + await composePage.close(); + expect((await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length).to.equal(++expectedNumberOfPassedMessages); + // 2. vague Gmail error with all failures + composePage = await ComposePageRecipe.openStandalone(t, browser, 'user4@standardsubdomainfes.localhost:8001'); await ComposePageRecipe.fillMsg(composePage, { cc: 'cc@example.com', bcc: 'flowcrypt.compatibility@gmail.com' }, subject); await composePage.waitAndType('@input-password', 'gO0d-pwd'); await composePage.waitAndClick('@action-send', { delay: 1 }); @@ -234,7 +247,21 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test 'Google returned an error when sending message. ' + 'Please help us improve FlowCrypt by reporting the error to us.' ); await composePage.close(); - // 2. "invalid To" Gmail error + expect((await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length).to.equal(expectedNumberOfPassedMessages); // + 0 messages + // 3. "invalid To" Gmail error with partial success + composePage = await ComposePageRecipe.openStandalone(t, browser, 'user4@standardsubdomainfes.localhost:8001'); + await ComposePageRecipe.fillMsg(composePage, { to: 'invalid@example.com', cc: 'to@example.com' }, subject); + await composePage.waitAndType('@input-password', 'gO0d-pwd'); + await composePage.waitAndClick('@action-send', { delay: 1 }); + await composePage.waitAndRespondToModal( + 'error', + 'confirm', + 'Messages to some recipients were sent successfully, while messages to invalid@example.com ' + + 'encountered error(s) from Gmail: Invalid recipients\n\nPlease remove recipients, add them back and re-send the message.' + ); + await composePage.close(); + expect((await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length).to.equal(++expectedNumberOfPassedMessages); + // 4. "invalid To" Gmail error with all failures composePage = await ComposePageRecipe.openStandalone(t, browser, 'user4@standardsubdomainfes.localhost:8001'); await ComposePageRecipe.fillMsg(composePage, { to: 'invalid@example.com', cc: 'cc@example.com' }, subject); await composePage.waitAndType('@input-password', 'gO0d-pwd'); @@ -245,7 +272,21 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test 'Error from google: Invalid recipients\n\nPlease remove recipients, add them back and re-send the message.' ); await composePage.close(); - // 3. "RequestTimeout" error with all failures + expect((await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length).to.equal(expectedNumberOfPassedMessages); // + 0 messages + // 5. "RequestTimeout" error with partial success + composePage = await ComposePageRecipe.openStandalone(t, browser, 'user4@standardsubdomainfes.localhost:8001'); + await ComposePageRecipe.fillMsg(composePage, { to: 'timeout@example.com', cc: 'to@example.com' }, subject); + await composePage.waitAndType('@input-password', 'gO0d-pwd'); + await composePage.waitAndClick('@action-send', { delay: 1 }); + await composePage.waitAndRespondToModal( + 'error', + 'confirm', + 'Messages to some recipients were sent successfully, while messages to timeout@example.com ' + + 'encountered network errors. Please check your internet connection and try again.' + ); + await composePage.close(); + expect((await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length).to.equal(++expectedNumberOfPassedMessages); + // 6. "RequestTimeout" error with all failures composePage = await ComposePageRecipe.openStandalone(t, browser, 'user4@standardsubdomainfes.localhost:8001'); await ComposePageRecipe.fillMsg(composePage, { to: 'timeout@example.com', cc: 'cc@example.com' }, subject); await composePage.waitAndType('@input-password', 'gO0d-pwd'); @@ -257,7 +298,7 @@ export const defineFlakyTests = (testVariant: TestVariant, testWithBrowser: Test '(This may also be caused by missing extension permissions).' ); await composePage.close(); - expect((await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length).to.equal(expectedNumberOfPassedMessages); + expect((await GoogleData.withInitializedData(acct)).searchMessagesBySubject(subject).length).to.equal(expectedNumberOfPassedMessages); // + 0 messages // this test is using PwdEncryptedMessageWithFesReplyBadRequestTestStrategy to check sent result based on subject // "PWD encrypted message with FES web portal - some sends fail with BadRequest error" // also see '/api/v1/message' in fes-endpoints.ts mock