mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-21 04:37:50 +08:00
fix: Update packages for security fixes (#21375)
This commit is contained in:
+3
-1
@@ -111,7 +111,9 @@
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0",
|
||||
"form-data": "4.0.4",
|
||||
"tmp": "0.2.4"
|
||||
"tmp": "0.2.4",
|
||||
"nodemailer": "7.0.10",
|
||||
"validator": "13.15.20"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"bull@4.16.4": "patches/bull@4.16.4.patch",
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
"n8n-nodes-base": "workspace:*",
|
||||
"n8n-workflow": "workspace:*",
|
||||
"nanoid": "catalog:",
|
||||
"nodemailer": "6.9.9",
|
||||
"nodemailer": "catalog:",
|
||||
"oauth-1.0a": "2.2.6",
|
||||
"open": "7.4.2",
|
||||
"openid-client": "6.5.0",
|
||||
@@ -180,7 +180,7 @@
|
||||
"syslog-client": "1.1.1",
|
||||
"undici": "^7.16.0",
|
||||
"uuid": "catalog:",
|
||||
"validator": "13.7.0",
|
||||
"validator": "13.15.20",
|
||||
"ws": "8.17.1",
|
||||
"xml2js": "catalog:",
|
||||
"xmllint-wasm": "3.0.1",
|
||||
|
||||
@@ -245,7 +245,6 @@ export async function encodeEmail(email: IEmail) {
|
||||
// by default the bcc headers are deleted when the mail is built.
|
||||
// So add keepBcc flag to override such behaviour. Only works when
|
||||
// the flag is set after the compilation.
|
||||
// @ts-expect-error - https://nodemailer.com/extras/mailcomposer/#bcc
|
||||
mail.keepBcc = true;
|
||||
|
||||
const mailBody = await mail.build();
|
||||
|
||||
@@ -5,6 +5,33 @@ import nock from 'nock';
|
||||
import labels from '../fixtures/labels.json';
|
||||
import messages from '../fixtures/messages.json';
|
||||
|
||||
function normalizeDraftMail(mail: string) {
|
||||
let normalizedMail = mail.replace(/\r\n/g, '\n');
|
||||
normalizedMail = normalizedMail
|
||||
.replace(/boundary=\".*\"/g, 'boundary="--test-boundary"')
|
||||
.replace(/----.*/g, '----test-boundary')
|
||||
.replace(/^From:.*$/gm, '')
|
||||
.replace(/Message-ID:.*/g, 'Message-ID: test-message-id');
|
||||
|
||||
const parts = normalizedMail.split(/\n\n/);
|
||||
if (parts.length > 1) {
|
||||
const headerBlock = parts[0];
|
||||
const bodyBlock = parts.slice(1).join('\n\n');
|
||||
const headers = headerBlock.split(/\n/).filter(Boolean);
|
||||
const map = new Map<string, string>();
|
||||
headers.forEach((line) => {
|
||||
const idx = line.indexOf(':');
|
||||
if (idx > -1) map.set(line.slice(0, idx), line);
|
||||
});
|
||||
const ordered = ['Content-Type', 'Cc', 'Bcc', 'Subject', 'Message-ID', 'Date', 'MIME-Version']
|
||||
.map((k) => map.get(k))
|
||||
.filter(Boolean) as string[];
|
||||
normalizedMail = `${ordered.join('\n')}\n\n${bodyBlock}`;
|
||||
}
|
||||
|
||||
return normalizedMail;
|
||||
}
|
||||
|
||||
describe('Test Gmail Node v1', () => {
|
||||
beforeAll(() => {
|
||||
jest
|
||||
@@ -128,24 +155,49 @@ describe('Test Gmail Node v1', () => {
|
||||
const parsedBody = jsonParse<{ message: { raw: string; threadId: string } }>(body);
|
||||
const mail = Buffer.from(parsedBody.message.raw, 'base64').toString('utf-8');
|
||||
|
||||
// Remove dynamic fields from mail
|
||||
parsedBody.message.raw = Buffer.from(
|
||||
mail
|
||||
.replace(/boundary=".*"/g, 'boundary="--test-boundary"')
|
||||
.replace(/----.*/g, '----test-boundary')
|
||||
.replace(/Message-ID:.*/g, 'Message-ID: test-message-id'),
|
||||
'utf-8',
|
||||
).toString('base64');
|
||||
const normalizedMail = normalizeDraftMail(mail);
|
||||
parsedBody.message.raw = Buffer.from(normalizedMail, 'utf-8').toString('base64');
|
||||
|
||||
return JSON.stringify(parsedBody);
|
||||
} catch (error) {
|
||||
return body;
|
||||
}
|
||||
})
|
||||
.post('/v1/users/me/drafts', {
|
||||
message: {
|
||||
raw: 'Q29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7IGJvdW5kYXJ5PSItLXRlc3QtYm91bmRhcnkiDQpDYzogdGVzdF9jY0BuOG4uaW8NCkJjYzogdGVzdF9iY2NAbjhuLmlvDQpTdWJqZWN0OiBUZXN0IFN1YmplY3QNCk1lc3NhZ2UtSUQ6IHRlc3QtbWVzc2FnZS1pZA0KRGF0ZTogTW9uLCAxNiBEZWMgMjAyNCAxMjozNDo1NiArMDAwMA0KTUlNRS1WZXJzaW9uOiAxLjANCg0KLS0tLXRlc3QtYm91bmRhcnkNCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD11dGYtOA0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdA0KDQpUZXN0IE1lc3NhZ2UNCi0tLS10ZXN0LWJvdW5kYXJ5DQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb247IG5hbWU9ZmlsZS5qc29uDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBiYXNlNjQNCkNvbnRlbnQtRGlzcG9zaXRpb246IGF0dGFjaG1lbnQ7IGZpbGVuYW1lPWZpbGUuanNvbg0KDQpXM3NpWVhSMFlXTm9iV1Z1ZENJNmRISjFaWDFkDQotLS0tdGVzdC1ib3VuZGFyeQ0K',
|
||||
},
|
||||
.post('/v1/users/me/drafts', (reqBody) => {
|
||||
try {
|
||||
const b = typeof reqBody === 'string' ? JSON.parse(reqBody) : reqBody;
|
||||
const raw = b?.message?.raw as string;
|
||||
if (typeof raw !== 'string') return false;
|
||||
const mail = Buffer.from(raw, 'base64').toString('utf-8');
|
||||
const normalized = normalizeDraftMail(mail);
|
||||
const expectedNormalized = [
|
||||
'Content-Type: multipart/mixed; boundary="--test-boundary"',
|
||||
'Cc: test_cc@n8n.io',
|
||||
'Bcc: test_bcc@n8n.io',
|
||||
'Subject: Test Subject',
|
||||
'Message-ID: test-message-id',
|
||||
'Date: Mon, 16 Dec 2024 12:34:56 +0000',
|
||||
'MIME-Version: 1.0',
|
||||
'',
|
||||
'----test-boundary',
|
||||
'Content-Type: text/plain; charset=utf-8',
|
||||
'Content-Transfer-Encoding: 7bit',
|
||||
'',
|
||||
'Test Message',
|
||||
'----test-boundary',
|
||||
'Content-Type: application/json; name=file.json',
|
||||
'Content-Transfer-Encoding: base64',
|
||||
'Content-Disposition: attachment; filename=file.json',
|
||||
'',
|
||||
'W3siYXR0YWNobWVudCI6dHJ1ZX1d',
|
||||
'----test-boundary',
|
||||
].join('\n');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Normalized (v1) actual:', normalized);
|
||||
return normalized.trimEnd() === expectedNormalized.trimEnd();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.query({ userId: 'me', uploadType: 'media' })
|
||||
.reply(200, messages[0]);
|
||||
|
||||
@@ -642,6 +642,7 @@ export class GmailV1 implements INodeType {
|
||||
}
|
||||
|
||||
const email: IEmail = {
|
||||
from: (additionalFields.senderName as string) || '',
|
||||
to: toStr,
|
||||
cc: ccStr,
|
||||
bcc: bccStr,
|
||||
|
||||
@@ -871,7 +871,7 @@
|
||||
"@types/mailparser": "^3.4.4",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/mssql": "^9.1.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/oracledb": "^6.9.1",
|
||||
"@types/promise-ftp": "^1.3.4",
|
||||
"@types/rfc2047": "^2.0.1",
|
||||
@@ -932,7 +932,7 @@
|
||||
"n8n-workflow": "workspace:*",
|
||||
"node-html-markdown": "1.2.0",
|
||||
"node-ssh": "13.2.0",
|
||||
"nodemailer": "6.9.9",
|
||||
"nodemailer": "catalog:",
|
||||
"oracledb": "6.9.0",
|
||||
"otpauth": "9.1.1",
|
||||
"pdfjs-dist": "5.3.31",
|
||||
|
||||
@@ -10,3 +10,13 @@ export type { N8NConfig, N8NStack } from './n8n-test-container-creation';
|
||||
export * from './performance-plans';
|
||||
|
||||
export { ContainerTestHelpers } from './n8n-test-container-helpers';
|
||||
export {
|
||||
setupMailpit,
|
||||
getMailpitEnvironment,
|
||||
mailpitClear,
|
||||
mailpitList,
|
||||
mailpitGet,
|
||||
mailpitWaitForMessage,
|
||||
type MailpitMessage,
|
||||
type MailpitQuery,
|
||||
} from './n8n-test-container-mailpit';
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
setupTaskRunner,
|
||||
} from './n8n-test-container-dependencies';
|
||||
import { setupGitea } from './n8n-test-container-gitea';
|
||||
import { setupMailpit, getMailpitEnvironment } from './n8n-test-container-mailpit';
|
||||
import { createSilentLogConsumer } from './n8n-test-container-utils';
|
||||
|
||||
// --- Constants ---
|
||||
@@ -87,6 +88,7 @@ export interface N8NConfig {
|
||||
proxyServerEnabled?: boolean;
|
||||
sourceControl?: boolean;
|
||||
taskRunner?: boolean;
|
||||
email?: boolean;
|
||||
}
|
||||
|
||||
export interface N8NStack {
|
||||
@@ -127,10 +129,12 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
resourceQuota,
|
||||
taskRunner = false,
|
||||
sourceControl = false,
|
||||
email = false,
|
||||
} = config;
|
||||
const queueConfig = normalizeQueueConfig(queueMode);
|
||||
const taskRunnerEnabled = !!taskRunner;
|
||||
const sourceControlEnabled = !!sourceControl;
|
||||
const emailEnabled = !!email;
|
||||
const usePostgres = postgres || !!queueConfig;
|
||||
const uniqueProjectName = projectName ?? `n8n-stack-${Math.random().toString(36).substring(7)}`;
|
||||
const containers: StartedTestContainer[] = [];
|
||||
@@ -143,7 +147,8 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
needsLoadBalancer ||
|
||||
proxyServerEnabled ||
|
||||
taskRunnerEnabled ||
|
||||
sourceControlEnabled;
|
||||
sourceControlEnabled ||
|
||||
emailEnabled;
|
||||
|
||||
let network: StartedNetwork | undefined;
|
||||
if (needsNetwork) {
|
||||
@@ -243,6 +248,27 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
};
|
||||
}
|
||||
|
||||
// Set up Mailpit BEFORE creating n8n instances so they have the correct environment
|
||||
if (emailEnabled && network) {
|
||||
const hostname = 'mailpit';
|
||||
const smtpPort = 1025;
|
||||
const httpPort = 8025;
|
||||
|
||||
const mailpitContainer = await setupMailpit({
|
||||
projectName: uniqueProjectName,
|
||||
network,
|
||||
hostname,
|
||||
smtpPort,
|
||||
httpPort,
|
||||
});
|
||||
containers.push(mailpitContainer);
|
||||
|
||||
environment = {
|
||||
...environment,
|
||||
...getMailpitEnvironment(hostname, smtpPort),
|
||||
};
|
||||
}
|
||||
|
||||
let baseUrl: string;
|
||||
|
||||
if (needsLoadBalancer) {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { setTimeout as wait } from 'node:timers/promises';
|
||||
import type { StartedTestContainer, StoppedTestContainer } from 'testcontainers';
|
||||
|
||||
import {
|
||||
getMailpitApiBaseUrl,
|
||||
mailpitWaitForMessage,
|
||||
mailpitList,
|
||||
mailpitClear,
|
||||
mailpitGet,
|
||||
type MailpitQuery,
|
||||
type MailpitMessage,
|
||||
type MailpitMessageSummary,
|
||||
} from './n8n-test-container-mailpit';
|
||||
|
||||
export interface LogMatch {
|
||||
container: StartedTestContainer;
|
||||
containerName: string;
|
||||
@@ -30,10 +41,20 @@ export class ContainerTestHelpers {
|
||||
// Containers
|
||||
private containers: StartedTestContainer[];
|
||||
|
||||
private _mailHelper?: MailHelper;
|
||||
|
||||
constructor(containers: StartedTestContainer[]) {
|
||||
this.containers = containers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mail helper facade for Mailpit interactions
|
||||
*/
|
||||
get mail(): MailHelper {
|
||||
this._mailHelper ??= new MailHelper(this.containers);
|
||||
return this._mailHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read logs from a container
|
||||
*/
|
||||
@@ -374,3 +395,37 @@ export class ContainerTestHelpers {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
class MailHelper {
|
||||
constructor(private containers: StartedTestContainer[]) {}
|
||||
|
||||
private getMailpitContainer(): StartedTestContainer {
|
||||
const container = this.containers.find((c) => /mailpit/i.test(c.getName()));
|
||||
if (!container) throw new Error('Mailpit container not found');
|
||||
return container;
|
||||
}
|
||||
|
||||
private get apiBaseUrl(): string {
|
||||
const mailpit = this.getMailpitContainer();
|
||||
return getMailpitApiBaseUrl(mailpit);
|
||||
}
|
||||
|
||||
async waitForMessage(
|
||||
query: MailpitQuery,
|
||||
options?: { timeoutMs?: number; pollMs?: number },
|
||||
): Promise<MailpitMessageSummary> {
|
||||
return await mailpitWaitForMessage(this.apiBaseUrl, query, options);
|
||||
}
|
||||
|
||||
async list(): Promise<MailpitMessageSummary[]> {
|
||||
return await mailpitList(this.apiBaseUrl);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await mailpitClear(this.apiBaseUrl);
|
||||
}
|
||||
|
||||
async get(id: string): Promise<MailpitMessage> {
|
||||
return await mailpitGet(this.apiBaseUrl, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import type { StartedNetwork, StartedTestContainer } from 'testcontainers';
|
||||
import { GenericContainer, Wait } from 'testcontainers';
|
||||
|
||||
import { createSilentLogConsumer } from './n8n-test-container-utils';
|
||||
|
||||
type MailpitAddress = {
|
||||
Address: string;
|
||||
Name?: string;
|
||||
};
|
||||
|
||||
// Message summary as returned in list responses
|
||||
export type MailpitMessageSummary = {
|
||||
ID: string;
|
||||
MessageID: string;
|
||||
Read: boolean;
|
||||
From: MailpitAddress;
|
||||
To: MailpitAddress[];
|
||||
Cc: MailpitAddress[] | null;
|
||||
Bcc: MailpitAddress[] | null;
|
||||
ReplyTo: MailpitAddress[];
|
||||
Subject: string;
|
||||
Created: string;
|
||||
Username: string;
|
||||
Tags: string[];
|
||||
Size: number;
|
||||
Attachments: number;
|
||||
Snippet: string;
|
||||
};
|
||||
|
||||
// Full message as returned by GET /api/v1/message/{id}
|
||||
export type MailpitMessage = MailpitMessageSummary & {
|
||||
Text?: string;
|
||||
HTML?: string;
|
||||
Inline?: Array<{
|
||||
PartID: string;
|
||||
FileName: string;
|
||||
ContentType: string;
|
||||
ContentID: string;
|
||||
Size: number;
|
||||
}>;
|
||||
Attachments?: Array<{
|
||||
PartID: string;
|
||||
FileName: string;
|
||||
ContentType: string;
|
||||
ContentID: string;
|
||||
Size: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type MailpitQuery = {
|
||||
to?: string | RegExp;
|
||||
subject?: string | RegExp;
|
||||
};
|
||||
|
||||
export type MailpitListResponse = {
|
||||
total: number;
|
||||
unread: number;
|
||||
count: number; // Deprecated but kept for backwards compatibility
|
||||
messages_count: number;
|
||||
messages_unread: number;
|
||||
start: number;
|
||||
tags: string[];
|
||||
messages: MailpitMessageSummary[];
|
||||
};
|
||||
|
||||
export function getMailpitEnvironment(
|
||||
hostname = 'mailpit',
|
||||
smtpPort = 1025,
|
||||
): Record<string, string> {
|
||||
return {
|
||||
N8N_EMAIL_MODE: 'smtp',
|
||||
N8N_SMTP_HOST: hostname,
|
||||
N8N_SMTP_PORT: String(smtpPort),
|
||||
N8N_SMTP_SSL: 'false',
|
||||
N8N_SMTP_SENDER: 'test@n8n.local',
|
||||
};
|
||||
}
|
||||
|
||||
export async function setupMailpit({
|
||||
projectName,
|
||||
network,
|
||||
hostname = 'mailpit',
|
||||
smtpPort = 1025,
|
||||
httpPort = 8025,
|
||||
}: {
|
||||
projectName: string;
|
||||
network: StartedNetwork;
|
||||
hostname?: string;
|
||||
smtpPort?: number;
|
||||
httpPort?: number;
|
||||
}): Promise<StartedTestContainer> {
|
||||
const { consumer, throwWithLogs } = createSilentLogConsumer();
|
||||
|
||||
try {
|
||||
return await new GenericContainer('axllent/mailpit:latest')
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases(hostname)
|
||||
.withExposedPorts(smtpPort, httpPort)
|
||||
.withEnvironment({
|
||||
MP_UI_BIND_ADDR: `0.0.0.0:${httpPort}`,
|
||||
MP_SMTP_BIND_ADDR: `0.0.0.0:${smtpPort}`,
|
||||
})
|
||||
.withWaitStrategy(
|
||||
Wait.forAll([
|
||||
Wait.forListeningPorts(),
|
||||
Wait.forHttp('/api/v1/info', httpPort).forStatusCode(200).withStartupTimeout(30000),
|
||||
]),
|
||||
)
|
||||
.withLabels({
|
||||
'com.docker.compose.project': projectName,
|
||||
'com.docker.compose.service': 'mailpit',
|
||||
})
|
||||
.withName(`${projectName}-mailpit`)
|
||||
.withReuse()
|
||||
.withLogConsumer(consumer)
|
||||
.start();
|
||||
} catch (error) {
|
||||
return throwWithLogs(error);
|
||||
}
|
||||
}
|
||||
|
||||
export function getMailpitApiBaseUrl(container: StartedTestContainer): string {
|
||||
return `http://${container.getHost()}:${container.getMappedPort(8025)}`;
|
||||
}
|
||||
|
||||
export async function mailpitClear(apiBaseUrl: string): Promise<void> {
|
||||
const res = await fetch(`${apiBaseUrl}/api/v1/messages`, { method: 'DELETE' });
|
||||
if (!res.ok) {
|
||||
throw new Error(`Mailpit clear failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function mailpitList(apiBaseUrl: string): Promise<MailpitMessageSummary[]> {
|
||||
const res = await fetch(`${apiBaseUrl}/api/v1/messages`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Mailpit list failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as MailpitListResponse;
|
||||
return data.messages || [];
|
||||
}
|
||||
|
||||
export async function mailpitGet(apiBaseUrl: string, id: string): Promise<MailpitMessage> {
|
||||
const res = await fetch(`${apiBaseUrl}/api/v1/message/${id}`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Mailpit get failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as MailpitMessage;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function mailpitWaitForMessage(
|
||||
apiBaseUrl: string,
|
||||
query: MailpitQuery,
|
||||
options: { timeoutMs?: number; pollMs?: number } = {},
|
||||
): Promise<MailpitMessageSummary> {
|
||||
const { timeoutMs = 10000, pollMs = 200 } = options;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
const messageMatches = (message: MailpitMessageSummary): boolean => {
|
||||
if (query.to) {
|
||||
const hasMatchingRecipient = message.To.some((recipient) =>
|
||||
typeof query.to === 'string'
|
||||
? recipient.Address === query.to
|
||||
: query.to!.test(recipient.Address),
|
||||
);
|
||||
if (!hasMatchingRecipient) return false;
|
||||
}
|
||||
|
||||
if (query.subject) {
|
||||
const subjectMatches =
|
||||
typeof query.subject === 'string'
|
||||
? message.Subject === query.subject
|
||||
: query.subject.test(message.Subject);
|
||||
if (!subjectMatches) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const messages = await mailpitList(apiBaseUrl);
|
||||
const match = messages.find(messageMatches);
|
||||
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
|
||||
const queryParts = [];
|
||||
if (query.to) queryParts.push(`to: ${query.to}`);
|
||||
if (query.subject) queryParts.push(`subject: ${query.subject}`);
|
||||
|
||||
throw new Error(`Mail not received within ${timeoutMs}ms. Query: ${queryParts.join(', ')}`);
|
||||
}
|
||||
@@ -39,6 +39,7 @@ interface ContainerConfig {
|
||||
proxyServerEnabled?: boolean;
|
||||
taskRunner?: boolean;
|
||||
sourceControl?: boolean;
|
||||
email?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,12 +66,11 @@ export const test = base.extend<
|
||||
// Container configuration from the project use options
|
||||
containerConfig: [
|
||||
async ({ addContainerCapability }, use, workerInfo) => {
|
||||
const baseConfig =
|
||||
(workerInfo.project.use as unknown as { containerConfig?: ContainerConfig })
|
||||
?.containerConfig ?? {};
|
||||
const projectConfig = workerInfo.project.use as { containerConfig?: ContainerConfig };
|
||||
const baseConfig = projectConfig?.containerConfig ?? {};
|
||||
|
||||
// Merge addContainerCapability with base config
|
||||
const config: ContainerConfig = {
|
||||
// Build merged configuration
|
||||
const merged: ContainerConfig = {
|
||||
...baseConfig,
|
||||
...addContainerCapability,
|
||||
env: {
|
||||
@@ -80,7 +80,7 @@ export const test = base.extend<
|
||||
},
|
||||
};
|
||||
|
||||
await use(config);
|
||||
await use(merged);
|
||||
},
|
||||
{ scope: 'worker', box: true },
|
||||
],
|
||||
|
||||
@@ -10,6 +10,7 @@ const CONTAINER_ONLY_TAGS = [
|
||||
'multi-main',
|
||||
'task-runner',
|
||||
'source-control',
|
||||
'email',
|
||||
];
|
||||
const CONTAINER_ONLY = new RegExp(`@capability:(${CONTAINER_ONLY_TAGS.join('|')})`);
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
test.use({ addContainerCapability: { email: true } });
|
||||
|
||||
test('EmailSend node sends via SMTP @capability:email', async ({ api, n8n, chaos }) => {
|
||||
// Sign in to use internal APIs for creating credentials and workflows
|
||||
|
||||
// Create SMTP credential targeting Mailpit
|
||||
const smtpCredential = await api.credentials.createCredential({
|
||||
name: 'SMTP (Test)',
|
||||
type: 'smtp',
|
||||
data: {
|
||||
user: '',
|
||||
password: '',
|
||||
host: 'mailpit',
|
||||
port: 1025,
|
||||
secure: false,
|
||||
disableStartTls: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Define a workflow with Manual Trigger -> EmailSend
|
||||
const toEmail = 'test@recipient.local';
|
||||
const subject = 'Playwright Mailpit SMTP';
|
||||
const workflowDefinition = {
|
||||
name: 'Mailpit EmailSend Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Manual Trigger',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Email',
|
||||
type: 'n8n-nodes-base.emailSend',
|
||||
typeVersion: 2,
|
||||
position: [300, 0],
|
||||
parameters: {
|
||||
fromEmail: 'test@n8n.local',
|
||||
toEmail,
|
||||
subject,
|
||||
emailFormat: 'text',
|
||||
text: 'Hello from n8n E2E test',
|
||||
},
|
||||
credentials: {
|
||||
smtp: {
|
||||
id: smtpCredential.id,
|
||||
name: smtpCredential.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'Manual Trigger': {
|
||||
main: [[{ node: 'Email', type: 'main', index: 0 }]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
} as const;
|
||||
|
||||
const { workflowId } = await api.workflows.createWorkflowFromDefinition(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
workflowDefinition as any,
|
||||
{ makeUnique: true },
|
||||
);
|
||||
|
||||
// Execute the workflow via UI API endpoint by navigating to the canvas and clicking run
|
||||
await n8n.page.goto(`/workflow/${workflowId}`);
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
|
||||
'Workflow executed successfully',
|
||||
);
|
||||
|
||||
const msg = await chaos.mail.waitForMessage({ to: toEmail, subject });
|
||||
|
||||
expect(msg).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
test.use({ addContainerCapability: { email: true } });
|
||||
|
||||
test('Password reset email is delivered @capability:email', async ({ api, chaos }) => {
|
||||
const ownerEmail = 'nathan@n8n.io';
|
||||
const res = await api.request.post('/rest/forgot-password', {
|
||||
data: { email: ownerEmail },
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
|
||||
const msg = await chaos.mail.waitForMessage({ to: ownerEmail, subject: /password reset/i });
|
||||
expect(msg).toBeTruthy();
|
||||
});
|
||||
Generated
+964
-70
File diff suppressed because it is too large
Load Diff
@@ -61,6 +61,7 @@ catalog:
|
||||
eslint: 9.29.0
|
||||
mysql2: 3.15.0
|
||||
run-script-os: 1.1.6
|
||||
nodemailer: 7.0.10
|
||||
|
||||
catalogs:
|
||||
frontend:
|
||||
|
||||
Reference in New Issue
Block a user