diff --git a/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts b/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts index 1ca1028f35..ce0f1ad1a4 100644 --- a/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts +++ b/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts @@ -1,3 +1,4 @@ +import qs from 'node:querystring'; import type { IExecuteFunctions, IDataObject, @@ -13,7 +14,7 @@ import { awsApiRequestSOAP, awsApiRequestSOAPAllItems } from './GenericFunctions function setParameter(params: string[], base: string, values: string[]) { for (let i = 0; i < values.length; i++) { - params.push(`${base}.${i + 1}=${values[i]}`); + params.push(`${base}.${i + 1}=${encodeURIComponent(values[i])}`); } } @@ -843,22 +844,20 @@ export class AwsSes implements INodeType { const templateSubject = this.getNodeParameter('templateSubject', i) as string; - const params = [ - 'Action=CreateCustomVerificationEmailTemplate', - `FailureRedirectionURL=${failureRedirectionURL}`, - `FromEmailAddress=${email}`, - `SuccessRedirectionURL=${successRedirectionURL}`, - `TemplateContent=${templateContent}`, - `TemplateName=${templateName}`, - `TemplateSubject=${templateSubject}`, - ]; - responseData = await awsApiRequestSOAP.call( this, 'email', 'POST', '', - params.join('&'), + qs.stringify({ + Action: 'CreateCustomVerificationEmailTemplate', + FromEmailAddress: email, + SuccessRedirectionURL: successRedirectionURL, + FailureRedirectionURL: failureRedirectionURL, + TemplateName: templateName, + TemplateSubject: templateSubject, + TemplateContent: templateContent, + }), ); responseData = responseData.CreateCustomVerificationEmailTemplateResponse; @@ -1012,7 +1011,7 @@ export class AwsSes implements INodeType { const params = [ `Message.Subject.Data=${encodeURIComponent(subject)}`, - `Source=${fromEmail}`, + `Source=${encodeURIComponent(fromEmail)}`, ]; if (isBodyHtml) { diff --git a/packages/nodes-base/nodes/Aws/SES/test/AwsSes.node.test.ts b/packages/nodes-base/nodes/Aws/SES/test/AwsSes.node.test.ts new file mode 100644 index 0000000000..45ad851ca7 --- /dev/null +++ b/packages/nodes-base/nodes/Aws/SES/test/AwsSes.node.test.ts @@ -0,0 +1,104 @@ +import qs from 'node:querystring'; +import assert from 'node:assert'; +import { NodeConnectionType } from 'n8n-workflow'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; +import * as Helpers from '@test/nodes/Helpers'; + +describe('AwsSes Node', () => { + const tests: WorkflowTestData[] = [ + { + description: 'should create customVerificationEmail', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '61c910d6-9997-4bc0-b95d-2b2771c3110f', + name: 'When clicking ‘Test workflow’', + type: 'n8n-nodes-base.manualTrigger', + typeVersion: 1, + position: [720, 380], + }, + { + parameters: { + resource: 'customVerificationEmail', + fromEmailAddress: 'test+user@example.com', + templateName: 'testTemplate', + templateContent: 'testContent', + templateSubject: 'testSubject', + successRedirectionURL: 'http://success.url/', + failureRedirectionURL: 'http://failure.url/', + }, + id: '5780c7b2-7e7f-44d2-980d-a162d28bf152', + name: 'AWS SES', + type: 'n8n-nodes-base.awsSes', + typeVersion: 1, + position: [940, 380], + credentials: { + aws: { + id: '1', + name: 'AWS', + }, + }, + }, + ], + connections: { + 'When clicking ‘Test workflow’': { + main: [ + [ + { + node: 'AWS SES', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'AWS SES': [[{ json: { success: 'true' } }]], + }, + }, + nock: { + baseUrl: 'https://email.eu-central-1.amazonaws.com', + mocks: [ + { + method: 'post', + path: '/', + requestBody: (body: any) => { + assert.deepEqual(qs.parse(body), { + Action: 'CreateCustomVerificationEmailTemplate', + FromEmailAddress: 'test+user@example.com', + SuccessRedirectionURL: 'http://success.url/', + FailureRedirectionURL: 'http://failure.url/', + TemplateName: 'testTemplate', + TemplateSubject: 'testSubject', + TemplateContent: 'testContent', + }); + return true; + }, + statusCode: 200, + responseBody: + 'true', + }, + ], + }, + }, + ]; + + const nodeTypes = Helpers.setup(tests); + + test.each(tests)('$description', async (testData) => { + const { result } = await executeWorkflow(testData, nodeTypes); + const resultNodeData = Helpers.getResultNodeData(result, testData); + resultNodeData.forEach(({ nodeName, resultData }) => + expect(resultData).toEqual(testData.output.nodeData[nodeName]), + ); + expect(result.finished).toEqual(true); + }); +}); diff --git a/packages/nodes-base/test/nodes/ExecuteWorkflow.ts b/packages/nodes-base/test/nodes/ExecuteWorkflow.ts index 5975d14071..0f36360fcd 100644 --- a/packages/nodes-base/test/nodes/ExecuteWorkflow.ts +++ b/packages/nodes-base/test/nodes/ExecuteWorkflow.ts @@ -9,9 +9,8 @@ export async function executeWorkflow(testData: WorkflowTestData, nodeTypes: INo if (testData.nock) { const { baseUrl, mocks } = testData.nock; const agent = nock(baseUrl); - mocks.forEach(({ method, path, statusCode, responseBody }) => - // @ts-expect-error - agent[method](path).reply(statusCode, responseBody), + mocks.forEach(({ method, path, statusCode, requestBody, responseBody }) => + agent[method](path, requestBody).reply(statusCode, responseBody), ); } const executionMode = testData.trigger?.mode ?? 'manual'; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index f6b69a96e2..9639d90705 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -7,6 +7,7 @@ import type { IncomingHttpHeaders } from 'http'; import type { SecureContextOptions } from 'tls'; import type { Readable } from 'stream'; import type { URLSearchParams } from 'url'; +import type { RequestBodyMatcher } from 'nock'; import type { AuthenticationMethod } from './Authentication'; import type { CODE_EXECUTION_MODES, CODE_LANGUAGES, LOG_LEVELS } from './Constants'; @@ -2213,10 +2214,11 @@ export interface WorkflowTestData { nock?: { baseUrl: string; mocks: Array<{ - method: string; + method: 'get' | 'post'; path: string; + requestBody?: RequestBodyMatcher; statusCode: number; - responseBody: any; + responseBody: string | object; }>; }; trigger?: {