Skip to content

Commit a8c1667

Browse files
authored
Merge pull request n8n-io#3622 from n8n-io/n8n-3990
Stabilize tests
2 parents 86b3cc6 + 70a1aa5 commit a8c1667

File tree

7 files changed

+137
-37
lines changed

7 files changed

+137
-37
lines changed

packages/cli/config/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ config.getEnv = config.get;
1414
// optional configuration files
1515
if (process.env.N8N_CONFIG_FILES !== undefined) {
1616
const configFiles = process.env.N8N_CONFIG_FILES.split(',');
17-
console.log(`\nLoading configuration overwrites from:\n - ${configFiles.join('\n - ')}\n`);
17+
if (process.env.NODE_ENV !== 'test') {
18+
console.log(`\nLoading configuration overwrites from:\n - ${configFiles.join('\n - ')}\n`);
19+
}
1820

1921
config.loadFile(configFiles);
2022
}

packages/cli/test/integration/passwordReset.api.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ beforeAll(async () => {
3535
utils.initTestLogger();
3636

3737
isSmtpAvailable = await utils.isTestSmtpServiceAvailable();
38-
});
38+
}, SMTP_TEST_TIMEOUT);
3939

4040
beforeEach(async () => {
4141
await testDb.truncate(['User'], testDbName);

packages/cli/test/integration/publicApi/workflows.test.ts

+9-15
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,22 @@ beforeAll(async () => {
2323
const initResult = await testDb.init();
2424
testDbName = initResult.testDbName;
2525

26-
const [
27-
fetchedGlobalOwnerRole,
28-
fetchedGlobalMemberRole,
29-
fetchedWorkflowOwnerRole,
30-
] = await testDb.getAllRoles();
26+
const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole, fetchedWorkflowOwnerRole] =
27+
await testDb.getAllRoles();
3128

3229
globalOwnerRole = fetchedGlobalOwnerRole;
3330
globalMemberRole = fetchedGlobalMemberRole;
3431
workflowOwnerRole = fetchedWorkflowOwnerRole;
3532

3633
utils.initTestTelemetry();
3734
utils.initTestLogger();
35+
utils.initConfigFile();
3836
await utils.initNodeTypes();
39-
await utils.initConfigFile();
4037
workflowRunner = await utils.initActiveWorkflowRunner();
4138
});
4239

4340
beforeEach(async () => {
44-
await testDb.truncate(
45-
['SharedCredentials', 'SharedWorkflow', 'User', 'Workflow', 'Credentials'],
46-
testDbName,
47-
);
41+
await testDb.truncate(['SharedWorkflow', 'User', 'Workflow'], testDbName);
4842

4943
config.set('userManagement.disabled', false);
5044
config.set('userManagement.isInstanceOwnerSetUp', true);
@@ -384,7 +378,7 @@ test('GET /workflows/:id should fail due to invalid API Key', async () => {
384378
expect(response.statusCode).toBe(401);
385379
});
386380

387-
test('GET /workflows/:id should fail due to non existing workflow', async () => {
381+
test('GET /workflows/:id should fail due to non-existing workflow', async () => {
388382
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
389383

390384
const authOwnerAgent = utils.createAgent(app, {
@@ -495,7 +489,7 @@ test('DELETE /workflows/:id should fail due to invalid API Key', async () => {
495489
expect(response.statusCode).toBe(401);
496490
});
497491

498-
test('DELETE /workflows/:id should fail due to non existing workflow', async () => {
492+
test('DELETE /workflows/:id should fail due to non-existing workflow', async () => {
499493
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
500494

501495
const authOwnerAgent = utils.createAgent(app, {
@@ -619,7 +613,7 @@ test('POST /workflows/:id/activate should fail due to invalid API Key', async ()
619613
expect(response.statusCode).toBe(401);
620614
});
621615

622-
test('POST /workflows/:id/activate should fail due to non existing workflow', async () => {
616+
test('POST /workflows/:id/activate should fail due to non-existing workflow', async () => {
623617
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
624618

625619
const authOwnerAgent = utils.createAgent(app, {
@@ -781,7 +775,7 @@ test('POST /workflows/:id/deactivate should fail due to invalid API Key', async
781775
expect(response.statusCode).toBe(401);
782776
});
783777

784-
test('POST /workflows/:id/deactivate should fail due to non existing workflow', async () => {
778+
test('POST /workflows/:id/deactivate should fail due to non-existing workflow', async () => {
785779
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
786780

787781
const authOwnerAgent = utils.createAgent(app, {
@@ -1036,7 +1030,7 @@ test('PUT /workflows/:id should fail due to invalid API Key', async () => {
10361030
expect(response.statusCode).toBe(401);
10371031
});
10381032

1039-
test('PUT /workflows/:id should fail due to non existing workflow', async () => {
1033+
test('PUT /workflows/:id should fail due to non-existing workflow', async () => {
10401034
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
10411035

10421036
const authOwnerAgent = utils.createAgent(app, {

packages/cli/test/integration/shared/constants.ts

+23
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly<string[]> = [
4848
'POST /owner/skip-setup',
4949
];
5050

51+
/**
52+
* Mapping tables link entities but, unlike `SharedWorkflow` and `SharedCredentials`,
53+
* have no entity representation. Therefore, mapping tables must be cleared
54+
* on truncation of any of the collections they link.
55+
*/
56+
export const MAPPING_TABLES_TO_CLEAR: Record<string, string[] | undefined> = {
57+
Workflow: ['workflows_tags'],
58+
Tag: ['workflows_tags'],
59+
};
60+
61+
5162
/**
5263
* Name of the connection used for creating and dropping a Postgres DB
5364
* for each suite test run.
@@ -64,3 +75,15 @@ export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly<string> = 'n8n_bs_mysql';
6475
* Timeout (in milliseconds) to account for fake SMTP service being slow to respond.
6576
*/
6677
export const SMTP_TEST_TIMEOUT = 30_000;
78+
79+
/**
80+
* Timeout (in milliseconds) to account for DB being slow to initialize.
81+
*/
82+
export const DB_INITIALIZATION_TIMEOUT = 30_000;
83+
84+
/**
85+
* Mapping tables having no entity representation.
86+
*/
87+
export const MAPPING_TABLES = {
88+
WorkflowsTags: 'workflows_tags',
89+
} as const;

packages/cli/test/integration/shared/testDb.ts

+93-18
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import { exec as callbackExec } from 'child_process';
22
import { promisify } from 'util';
33

44
import { createConnection, getConnection, ConnectionOptions, Connection } from 'typeorm';
5-
import { Credentials, UserSettings } from 'n8n-core';
5+
import { UserSettings } from 'n8n-core';
66

77
import config from '../../../config';
8-
import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } from './constants';
9-
import { Db, ICredentialsDb, IDatabaseCollections } from '../../../src';
8+
import {
9+
BOOTSTRAP_MYSQL_CONNECTION_NAME,
10+
BOOTSTRAP_POSTGRES_CONNECTION_NAME,
11+
DB_INITIALIZATION_TIMEOUT,
12+
MAPPING_TABLES,
13+
MAPPING_TABLES_TO_CLEAR,
14+
} from './constants';
15+
import { DatabaseType, Db, ICredentialsDb } from '../../../src';
1016
import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random';
1117
import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
1218
import { hashPassword } from '../../../src/UserManagement/UserManagementHelper';
@@ -19,7 +25,7 @@ import { createCredentiasFromCredentialsEntity } from '../../../src/CredentialsH
1925

2026
import type { Role } from '../../../src/databases/entities/Role';
2127
import { User } from '../../../src/databases/entities/User';
22-
import type { CollectionName, CredentialPayload } from './types';
28+
import type { CollectionName, CredentialPayload, MappingName } from './types';
2329
import { WorkflowEntity } from '../../../src/databases/entities/WorkflowEntity';
2430
import { ExecutionEntity } from '../../../src/databases/entities/ExecutionEntity';
2531
import { TagEntity } from '../../../src/databases/entities/TagEntity';
@@ -33,6 +39,8 @@ export async function init() {
3339
const dbType = config.getEnv('database.type');
3440

3541
if (dbType === 'sqlite') {
42+
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
43+
3644
// no bootstrap connection required
3745
const testDbName = `n8n_test_sqlite_${randomString(6, 10)}_${Date.now()}`;
3846
await Db.init(getSqliteOptions({ name: testDbName }));
@@ -42,6 +50,8 @@ export async function init() {
4250
}
4351

4452
if (dbType === 'postgresdb') {
53+
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
54+
4555
let bootstrapPostgres;
4656
const pgOptions = getBootstrapPostgresOptions();
4757

@@ -87,6 +97,8 @@ export async function init() {
8797
}
8898

8999
if (dbType === 'mysqldb') {
100+
// initialization timeout in test/setup.ts
101+
90102
const bootstrapMysql = await createConnection(getBootstrapMySqlOptions());
91103

92104
const testDbName = `mysql_${randomString(6, 10)}_${Date.now()}_n8n_test`;
@@ -127,32 +139,89 @@ export async function terminate(testDbName: string) {
127139
}
128140
}
129141

142+
async function truncateMappingTables(
143+
dbType: DatabaseType,
144+
collections: Array<CollectionName>,
145+
testDb: Connection,
146+
) {
147+
const mappingTables = collections.reduce<string[]>((acc, collection) => {
148+
const found = MAPPING_TABLES_TO_CLEAR[collection];
149+
150+
if (found) acc.push(...found);
151+
152+
return acc;
153+
}, []);
154+
155+
if (dbType === 'sqlite') {
156+
const promises = mappingTables.map((tableName) =>
157+
testDb.query(
158+
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
159+
),
160+
);
161+
162+
return Promise.all(promises);
163+
}
164+
165+
if (dbType === 'postgresdb') {
166+
const schema = config.getEnv('database.postgresdb.schema');
167+
168+
// `TRUNCATE` in postgres cannot be parallelized
169+
for (const tableName of mappingTables) {
170+
const fullTableName = `${schema}.${tableName}`;
171+
await testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
172+
}
173+
174+
return Promise.resolve([]);
175+
}
176+
177+
// mysqldb, mariadb
178+
179+
const promises = mappingTables.flatMap((tableName) => [
180+
testDb.query(`DELETE FROM ${tableName};`),
181+
testDb.query(`ALTER TABLE ${tableName} AUTO_INCREMENT = 1;`),
182+
]);
183+
184+
return Promise.all(promises);
185+
}
186+
130187
/**
131-
* Truncate DB tables for collections.
188+
* Truncate specific DB tables in a test DB.
132189
*
133190
* @param collections Array of entity names whose tables to truncate.
134191
* @param testDbName Name of the test DB to truncate tables in.
135192
*/
136-
export async function truncate(collections: CollectionName[], testDbName: string) {
193+
export async function truncate(collections: Array<CollectionName>, testDbName: string) {
137194
const dbType = config.getEnv('database.type');
138-
139195
const testDb = getConnection(testDbName);
140196

141197
if (dbType === 'sqlite') {
142198
await testDb.query('PRAGMA foreign_keys=OFF');
143-
await Promise.all(collections.map((collection) => Db.collections[collection].clear()));
199+
200+
const truncationPromises = collections.map((collection) => {
201+
const tableName = toTableName(collection);
202+
return testDb.query(
203+
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
204+
);
205+
});
206+
207+
truncationPromises.push(truncateMappingTables(dbType, collections, testDb));
208+
209+
await Promise.all(truncationPromises);
210+
144211
return testDb.query('PRAGMA foreign_keys=ON');
145212
}
146213

147214
if (dbType === 'postgresdb') {
148-
return Promise.all(
149-
collections.map((collection) => {
150-
const schema = config.getEnv('database.postgresdb.schema');
151-
const fullTableName = `${schema}.${toTableName(collection)}`;
215+
const schema = config.getEnv('database.postgresdb.schema');
152216

153-
testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
154-
}),
155-
);
217+
// `TRUNCATE` in postgres cannot be parallelized
218+
for (const collection of collections) {
219+
const fullTableName = `${schema}.${toTableName(collection)}`;
220+
await testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
221+
}
222+
223+
return await truncateMappingTables(dbType, collections, testDb);
224+
// return Promise.resolve([])
156225
}
157226

158227
/**
@@ -167,11 +236,17 @@ export async function truncate(collections: CollectionName[], testDbName: string
167236
);
168237

169238
await truncateMySql(testDb, isShared);
239+
await truncateMappingTables(dbType, collections, testDb);
170240
await truncateMySql(testDb, isNotShared);
171241
}
172242
}
173243

174-
function toTableName(collectionName: CollectionName) {
244+
const isMapping = (collection: string): collection is MappingName =>
245+
Object.keys(MAPPING_TABLES).includes(collection);
246+
247+
function toTableName(sourceName: CollectionName | MappingName) {
248+
if (isMapping(sourceName)) return MAPPING_TABLES[sourceName];
249+
175250
return {
176251
Credentials: 'credentials_entity',
177252
Workflow: 'workflow_entity',
@@ -183,10 +258,10 @@ function toTableName(collectionName: CollectionName) {
183258
SharedCredentials: 'shared_credentials',
184259
SharedWorkflow: 'shared_workflow',
185260
Settings: 'settings',
186-
}[collectionName];
261+
}[sourceName];
187262
}
188263

189-
function truncateMySql(connection: Connection, collections: Array<keyof IDatabaseCollections>) {
264+
function truncateMySql(connection: Connection, collections: CollectionName[]) {
190265
return Promise.all(
191266
collections.map(async (collection) => {
192267
const tableName = toTableName(collection);

packages/cli/test/integration/shared/types.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-
22
import type { ICredentialsDb, IDatabaseCollections } from '../../../src';
33
import type { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
44
import type { User } from '../../../src/databases/entities/User';
5+
import { MAPPING_TABLES } from './constants';
56

67
export type CollectionName = keyof IDatabaseCollections;
78

9+
export type MappingName = keyof typeof MAPPING_TABLES;
10+
811
export type SmtpTestAccount = {
912
user: string;
1013
pass: string;

packages/cli/test/setup.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { exec as callbackExec } from 'child_process';
22
import { promisify } from 'util';
33

44
import config from '../config';
5-
import { BOOTSTRAP_MYSQL_CONNECTION_NAME } from './integration/shared/constants';
5+
import {
6+
BOOTSTRAP_MYSQL_CONNECTION_NAME,
7+
DB_INITIALIZATION_TIMEOUT,
8+
} from './integration/shared/constants';
69

710
const exec = promisify(callbackExec);
811

@@ -17,7 +20,7 @@ if (dbType === 'mysqldb') {
1720

1821
(async () => {
1922
try {
20-
jest.setTimeout(30000); // 30 seconds for DB initialization
23+
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
2124
await exec(
2225
`echo "CREATE DATABASE IF NOT EXISTS ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${username} ${passwordSegment}; USE ${BOOTSTRAP_MYSQL_CONNECTION_NAME};`,
2326
);

0 commit comments

Comments
 (0)