diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index 01cb5e784..a994cd742 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -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); } diff --git a/packages/cli/test/integration/passwordReset.api.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts index deaf2f04f..4b6a4256c 100644 --- a/packages/cli/test/integration/passwordReset.api.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -35,7 +35,7 @@ beforeAll(async () => { utils.initTestLogger(); isSmtpAvailable = await utils.isTestSmtpServiceAvailable(); -}); +}, SMTP_TEST_TIMEOUT); beforeEach(async () => { await testDb.truncate(['User'], testDbName); diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 17736af83..e676b9c16 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -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, { diff --git a/packages/cli/test/integration/shared/constants.ts b/packages/cli/test/integration/shared/constants.ts index d7e165ab4..d68ef00e1 100644 --- a/packages/cli/test/integration/shared/constants.ts +++ b/packages/cli/test/integration/shared/constants.ts @@ -48,6 +48,17 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly = [ '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 = { + 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 = '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; diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index e34496123..3d7cc6140 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -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, + testDb: Connection, +) { + const mappingTables = collections.reduce((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, 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) { +function truncateMySql(connection: Connection, collections: CollectionName[]) { return Promise.all( collections.map(async (collection) => { const tableName = toTableName(collection); diff --git a/packages/cli/test/integration/shared/types.d.ts b/packages/cli/test/integration/shared/types.d.ts index c16a44d29..a711c4b03 100644 --- a/packages/cli/test/integration/shared/types.d.ts +++ b/packages/cli/test/integration/shared/types.d.ts @@ -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; diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index 1180a082c..e0af9209d 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -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};`, );