mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -08:00
commit
a8c16675d0
|
@ -14,7 +14,9 @@ config.getEnv = config.get;
|
|||
// optional configuration files
|
||||
if (process.env.N8N_CONFIG_FILES !== undefined) {
|
||||
const configFiles = process.env.N8N_CONFIG_FILES.split(',');
|
||||
console.log(`\nLoading configuration overwrites from:\n - ${configFiles.join('\n - ')}\n`);
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.log(`\nLoading configuration overwrites from:\n - ${configFiles.join('\n - ')}\n`);
|
||||
}
|
||||
|
||||
config.loadFile(configFiles);
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ beforeAll(async () => {
|
|||
utils.initTestLogger();
|
||||
|
||||
isSmtpAvailable = await utils.isTestSmtpServiceAvailable();
|
||||
});
|
||||
}, SMTP_TEST_TIMEOUT);
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['User'], testDbName);
|
||||
|
|
|
@ -23,11 +23,8 @@ beforeAll(async () => {
|
|||
const initResult = await testDb.init();
|
||||
testDbName = initResult.testDbName;
|
||||
|
||||
const [
|
||||
fetchedGlobalOwnerRole,
|
||||
fetchedGlobalMemberRole,
|
||||
fetchedWorkflowOwnerRole,
|
||||
] = await testDb.getAllRoles();
|
||||
const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole, fetchedWorkflowOwnerRole] =
|
||||
await testDb.getAllRoles();
|
||||
|
||||
globalOwnerRole = fetchedGlobalOwnerRole;
|
||||
globalMemberRole = fetchedGlobalMemberRole;
|
||||
|
@ -35,16 +32,13 @@ beforeAll(async () => {
|
|||
|
||||
utils.initTestTelemetry();
|
||||
utils.initTestLogger();
|
||||
utils.initConfigFile();
|
||||
await utils.initNodeTypes();
|
||||
await utils.initConfigFile();
|
||||
workflowRunner = await utils.initActiveWorkflowRunner();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(
|
||||
['SharedCredentials', 'SharedWorkflow', 'User', 'Workflow', 'Credentials'],
|
||||
testDbName,
|
||||
);
|
||||
await testDb.truncate(['SharedWorkflow', 'User', 'Workflow'], testDbName);
|
||||
|
||||
config.set('userManagement.disabled', false);
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
|
@ -384,7 +378,7 @@ test('GET /workflows/:id should fail due to invalid API Key', async () => {
|
|||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('GET /workflows/:id should fail due to non existing workflow', async () => {
|
||||
test('GET /workflows/:id should fail due to non-existing workflow', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
|
||||
|
||||
const authOwnerAgent = utils.createAgent(app, {
|
||||
|
@ -495,7 +489,7 @@ test('DELETE /workflows/:id should fail due to invalid API Key', async () => {
|
|||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('DELETE /workflows/:id should fail due to non existing workflow', async () => {
|
||||
test('DELETE /workflows/:id should fail due to non-existing workflow', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
|
||||
|
||||
const authOwnerAgent = utils.createAgent(app, {
|
||||
|
@ -619,7 +613,7 @@ test('POST /workflows/:id/activate should fail due to invalid API Key', async ()
|
|||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('POST /workflows/:id/activate should fail due to non existing workflow', async () => {
|
||||
test('POST /workflows/:id/activate should fail due to non-existing workflow', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
|
||||
|
||||
const authOwnerAgent = utils.createAgent(app, {
|
||||
|
@ -781,7 +775,7 @@ test('POST /workflows/:id/deactivate should fail due to invalid API Key', async
|
|||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('POST /workflows/:id/deactivate should fail due to non existing workflow', async () => {
|
||||
test('POST /workflows/:id/deactivate should fail due to non-existing workflow', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
|
||||
|
||||
const authOwnerAgent = utils.createAgent(app, {
|
||||
|
@ -1036,7 +1030,7 @@ test('PUT /workflows/:id should fail due to invalid API Key', async () => {
|
|||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('PUT /workflows/:id should fail due to non existing workflow', async () => {
|
||||
test('PUT /workflows/:id should fail due to non-existing workflow', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
|
||||
|
||||
const authOwnerAgent = utils.createAgent(app, {
|
||||
|
|
|
@ -48,6 +48,17 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly<string[]> = [
|
|||
'POST /owner/skip-setup',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapping tables link entities but, unlike `SharedWorkflow` and `SharedCredentials`,
|
||||
* have no entity representation. Therefore, mapping tables must be cleared
|
||||
* on truncation of any of the collections they link.
|
||||
*/
|
||||
export const MAPPING_TABLES_TO_CLEAR: Record<string, string[] | undefined> = {
|
||||
Workflow: ['workflows_tags'],
|
||||
Tag: ['workflows_tags'],
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Name of the connection used for creating and dropping a Postgres DB
|
||||
* for each suite test run.
|
||||
|
@ -64,3 +75,15 @@ export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly<string> = 'n8n_bs_mysql';
|
|||
* Timeout (in milliseconds) to account for fake SMTP service being slow to respond.
|
||||
*/
|
||||
export const SMTP_TEST_TIMEOUT = 30_000;
|
||||
|
||||
/**
|
||||
* Timeout (in milliseconds) to account for DB being slow to initialize.
|
||||
*/
|
||||
export const DB_INITIALIZATION_TIMEOUT = 30_000;
|
||||
|
||||
/**
|
||||
* Mapping tables having no entity representation.
|
||||
*/
|
||||
export const MAPPING_TABLES = {
|
||||
WorkflowsTags: 'workflows_tags',
|
||||
} as const;
|
||||
|
|
|
@ -2,11 +2,17 @@ import { exec as callbackExec } from 'child_process';
|
|||
import { promisify } from 'util';
|
||||
|
||||
import { createConnection, getConnection, ConnectionOptions, Connection } from 'typeorm';
|
||||
import { Credentials, UserSettings } from 'n8n-core';
|
||||
import { UserSettings } from 'n8n-core';
|
||||
|
||||
import config from '../../../config';
|
||||
import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } from './constants';
|
||||
import { Db, ICredentialsDb, IDatabaseCollections } from '../../../src';
|
||||
import {
|
||||
BOOTSTRAP_MYSQL_CONNECTION_NAME,
|
||||
BOOTSTRAP_POSTGRES_CONNECTION_NAME,
|
||||
DB_INITIALIZATION_TIMEOUT,
|
||||
MAPPING_TABLES,
|
||||
MAPPING_TABLES_TO_CLEAR,
|
||||
} from './constants';
|
||||
import { DatabaseType, Db, ICredentialsDb } from '../../../src';
|
||||
import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random';
|
||||
import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
|
||||
import { hashPassword } from '../../../src/UserManagement/UserManagementHelper';
|
||||
|
@ -19,7 +25,7 @@ import { createCredentiasFromCredentialsEntity } from '../../../src/CredentialsH
|
|||
|
||||
import type { Role } from '../../../src/databases/entities/Role';
|
||||
import { User } from '../../../src/databases/entities/User';
|
||||
import type { CollectionName, CredentialPayload } from './types';
|
||||
import type { CollectionName, CredentialPayload, MappingName } from './types';
|
||||
import { WorkflowEntity } from '../../../src/databases/entities/WorkflowEntity';
|
||||
import { ExecutionEntity } from '../../../src/databases/entities/ExecutionEntity';
|
||||
import { TagEntity } from '../../../src/databases/entities/TagEntity';
|
||||
|
@ -33,6 +39,8 @@ export async function init() {
|
|||
const dbType = config.getEnv('database.type');
|
||||
|
||||
if (dbType === 'sqlite') {
|
||||
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
|
||||
|
||||
// no bootstrap connection required
|
||||
const testDbName = `n8n_test_sqlite_${randomString(6, 10)}_${Date.now()}`;
|
||||
await Db.init(getSqliteOptions({ name: testDbName }));
|
||||
|
@ -42,6 +50,8 @@ export async function init() {
|
|||
}
|
||||
|
||||
if (dbType === 'postgresdb') {
|
||||
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
|
||||
|
||||
let bootstrapPostgres;
|
||||
const pgOptions = getBootstrapPostgresOptions();
|
||||
|
||||
|
@ -87,6 +97,8 @@ export async function init() {
|
|||
}
|
||||
|
||||
if (dbType === 'mysqldb') {
|
||||
// initialization timeout in test/setup.ts
|
||||
|
||||
const bootstrapMysql = await createConnection(getBootstrapMySqlOptions());
|
||||
|
||||
const testDbName = `mysql_${randomString(6, 10)}_${Date.now()}_n8n_test`;
|
||||
|
@ -127,32 +139,89 @@ export async function terminate(testDbName: string) {
|
|||
}
|
||||
}
|
||||
|
||||
async function truncateMappingTables(
|
||||
dbType: DatabaseType,
|
||||
collections: Array<CollectionName>,
|
||||
testDb: Connection,
|
||||
) {
|
||||
const mappingTables = collections.reduce<string[]>((acc, collection) => {
|
||||
const found = MAPPING_TABLES_TO_CLEAR[collection];
|
||||
|
||||
if (found) acc.push(...found);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (dbType === 'sqlite') {
|
||||
const promises = mappingTables.map((tableName) =>
|
||||
testDb.query(
|
||||
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
|
||||
),
|
||||
);
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
if (dbType === 'postgresdb') {
|
||||
const schema = config.getEnv('database.postgresdb.schema');
|
||||
|
||||
// `TRUNCATE` in postgres cannot be parallelized
|
||||
for (const tableName of mappingTables) {
|
||||
const fullTableName = `${schema}.${tableName}`;
|
||||
await testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
// mysqldb, mariadb
|
||||
|
||||
const promises = mappingTables.flatMap((tableName) => [
|
||||
testDb.query(`DELETE FROM ${tableName};`),
|
||||
testDb.query(`ALTER TABLE ${tableName} AUTO_INCREMENT = 1;`),
|
||||
]);
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate DB tables for collections.
|
||||
* Truncate specific DB tables in a test DB.
|
||||
*
|
||||
* @param collections Array of entity names whose tables to truncate.
|
||||
* @param testDbName Name of the test DB to truncate tables in.
|
||||
*/
|
||||
export async function truncate(collections: CollectionName[], testDbName: string) {
|
||||
export async function truncate(collections: Array<CollectionName>, testDbName: string) {
|
||||
const dbType = config.getEnv('database.type');
|
||||
|
||||
const testDb = getConnection(testDbName);
|
||||
|
||||
if (dbType === 'sqlite') {
|
||||
await testDb.query('PRAGMA foreign_keys=OFF');
|
||||
await Promise.all(collections.map((collection) => Db.collections[collection].clear()));
|
||||
|
||||
const truncationPromises = collections.map((collection) => {
|
||||
const tableName = toTableName(collection);
|
||||
return testDb.query(
|
||||
`DELETE FROM ${tableName}; DELETE FROM sqlite_sequence WHERE name=${tableName};`,
|
||||
);
|
||||
});
|
||||
|
||||
truncationPromises.push(truncateMappingTables(dbType, collections, testDb));
|
||||
|
||||
await Promise.all(truncationPromises);
|
||||
|
||||
return testDb.query('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
if (dbType === 'postgresdb') {
|
||||
return Promise.all(
|
||||
collections.map((collection) => {
|
||||
const schema = config.getEnv('database.postgresdb.schema');
|
||||
const fullTableName = `${schema}.${toTableName(collection)}`;
|
||||
const schema = config.getEnv('database.postgresdb.schema');
|
||||
|
||||
testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
|
||||
}),
|
||||
);
|
||||
// `TRUNCATE` in postgres cannot be parallelized
|
||||
for (const collection of collections) {
|
||||
const fullTableName = `${schema}.${toTableName(collection)}`;
|
||||
await testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
|
||||
}
|
||||
|
||||
return await truncateMappingTables(dbType, collections, testDb);
|
||||
// return Promise.resolve([])
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -167,11 +236,17 @@ export async function truncate(collections: CollectionName[], testDbName: string
|
|||
);
|
||||
|
||||
await truncateMySql(testDb, isShared);
|
||||
await truncateMappingTables(dbType, collections, testDb);
|
||||
await truncateMySql(testDb, isNotShared);
|
||||
}
|
||||
}
|
||||
|
||||
function toTableName(collectionName: CollectionName) {
|
||||
const isMapping = (collection: string): collection is MappingName =>
|
||||
Object.keys(MAPPING_TABLES).includes(collection);
|
||||
|
||||
function toTableName(sourceName: CollectionName | MappingName) {
|
||||
if (isMapping(sourceName)) return MAPPING_TABLES[sourceName];
|
||||
|
||||
return {
|
||||
Credentials: 'credentials_entity',
|
||||
Workflow: 'workflow_entity',
|
||||
|
@ -183,10 +258,10 @@ function toTableName(collectionName: CollectionName) {
|
|||
SharedCredentials: 'shared_credentials',
|
||||
SharedWorkflow: 'shared_workflow',
|
||||
Settings: 'settings',
|
||||
}[collectionName];
|
||||
}[sourceName];
|
||||
}
|
||||
|
||||
function truncateMySql(connection: Connection, collections: Array<keyof IDatabaseCollections>) {
|
||||
function truncateMySql(connection: Connection, collections: CollectionName[]) {
|
||||
return Promise.all(
|
||||
collections.map(async (collection) => {
|
||||
const tableName = toTableName(collection);
|
||||
|
|
|
@ -2,9 +2,12 @@ import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-
|
|||
import type { ICredentialsDb, IDatabaseCollections } from '../../../src';
|
||||
import type { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
|
||||
import type { User } from '../../../src/databases/entities/User';
|
||||
import { MAPPING_TABLES } from './constants';
|
||||
|
||||
export type CollectionName = keyof IDatabaseCollections;
|
||||
|
||||
export type MappingName = keyof typeof MAPPING_TABLES;
|
||||
|
||||
export type SmtpTestAccount = {
|
||||
user: string;
|
||||
pass: string;
|
||||
|
|
|
@ -2,7 +2,10 @@ import { exec as callbackExec } from 'child_process';
|
|||
import { promisify } from 'util';
|
||||
|
||||
import config from '../config';
|
||||
import { BOOTSTRAP_MYSQL_CONNECTION_NAME } from './integration/shared/constants';
|
||||
import {
|
||||
BOOTSTRAP_MYSQL_CONNECTION_NAME,
|
||||
DB_INITIALIZATION_TIMEOUT,
|
||||
} from './integration/shared/constants';
|
||||
|
||||
const exec = promisify(callbackExec);
|
||||
|
||||
|
@ -17,7 +20,7 @@ if (dbType === 'mysqldb') {
|
|||
|
||||
(async () => {
|
||||
try {
|
||||
jest.setTimeout(30000); // 30 seconds for DB initialization
|
||||
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
|
||||
await exec(
|
||||
`echo "CREATE DATABASE IF NOT EXISTS ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${username} ${passwordSegment}; USE ${BOOTSTRAP_MYSQL_CONNECTION_NAME};`,
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue