🧪 Truncate mapping tables

This commit is contained in:
Iván Ovejero 2022-06-30 13:43:50 +02:00
parent 86b3cc6e15
commit de55fdb625
4 changed files with 103 additions and 19 deletions

View file

@ -35,14 +35,14 @@ beforeAll(async () => {
utils.initTestTelemetry(); utils.initTestTelemetry();
utils.initTestLogger(); utils.initTestLogger();
utils.initConfigFile();
await utils.initNodeTypes(); await utils.initNodeTypes();
await utils.initConfigFile();
workflowRunner = await utils.initActiveWorkflowRunner(); workflowRunner = await utils.initActiveWorkflowRunner();
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate( await testDb.truncate(
['SharedCredentials', 'SharedWorkflow', 'User', 'Workflow', 'Credentials'], ['SharedCredentials', 'SharedWorkflow', 'User', 'Workflow', 'Credentials', 'Tag'],
testDbName, testDbName,
); );

View file

@ -48,6 +48,17 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly<string[]> = [
'POST /owner/skip-setup', '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 * Name of the connection used for creating and dropping a Postgres DB
* for each suite test run. * for each suite test run.
@ -64,3 +75,10 @@ export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly<string> = 'n8n_bs_mysql';
* Timeout (in milliseconds) to account for fake SMTP service being slow to respond. * Timeout (in milliseconds) to account for fake SMTP service being slow to respond.
*/ */
export const SMTP_TEST_TIMEOUT = 30_000; export const SMTP_TEST_TIMEOUT = 30_000;
/**
* Mapping tables having no entity representation.
*/
export const MAPPING_TABLES = {
WorkflowsTags: 'workflows_tags',
} as const;

View file

@ -5,8 +5,13 @@ import { createConnection, getConnection, ConnectionOptions, Connection } from '
import { Credentials, UserSettings } from 'n8n-core'; import { Credentials, UserSettings } from 'n8n-core';
import config from '../../../config'; import config from '../../../config';
import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } from './constants'; import {
import { Db, ICredentialsDb, IDatabaseCollections } from '../../../src'; BOOTSTRAP_MYSQL_CONNECTION_NAME,
BOOTSTRAP_POSTGRES_CONNECTION_NAME,
MAPPING_TABLES,
MAPPING_TABLES_TO_CLEAR,
} from './constants';
import { DatabaseType, Db, ICredentialsDb } from '../../../src';
import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random'; import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random';
import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity'; import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
import { hashPassword } from '../../../src/UserManagement/UserManagementHelper'; import { hashPassword } from '../../../src/UserManagement/UserManagementHelper';
@ -19,7 +24,7 @@ import { createCredentiasFromCredentialsEntity } from '../../../src/CredentialsH
import type { Role } from '../../../src/databases/entities/Role'; import type { Role } from '../../../src/databases/entities/Role';
import { User } from '../../../src/databases/entities/User'; 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 { WorkflowEntity } from '../../../src/databases/entities/WorkflowEntity';
import { ExecutionEntity } from '../../../src/databases/entities/ExecutionEntity'; import { ExecutionEntity } from '../../../src/databases/entities/ExecutionEntity';
import { TagEntity } from '../../../src/databases/entities/TagEntity'; import { TagEntity } from '../../../src/databases/entities/TagEntity';
@ -127,32 +132,84 @@ 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 promises = mappingTables.map((tableName) => {
const schema = config.getEnv('database.postgresdb.schema');
const fullTableName = `${schema}.${tableName}`;
testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
});
return Promise.all(promises);
}
// mysqldb, mariadb
const promises = mappingTables.map((tableName) =>
testDb.query(`DELETE FROM ${tableName}; 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 collections Array of entity names whose tables to truncate.
* @param testDbName Name of the test DB to truncate tables in. * @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 dbType = config.getEnv('database.type');
const testDb = getConnection(testDbName); const testDb = getConnection(testDbName);
if (dbType === 'sqlite') { if (dbType === 'sqlite') {
await testDb.query('PRAGMA foreign_keys=OFF'); 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'); return testDb.query('PRAGMA foreign_keys=ON');
} }
if (dbType === 'postgresdb') { if (dbType === 'postgresdb') {
return Promise.all( const truncationPromises = collections.map((collection) => {
collections.map((collection) => { const schema = config.getEnv('database.postgresdb.schema');
const schema = config.getEnv('database.postgresdb.schema'); const fullTableName = `${schema}.${toTableName(collection)}`;
const fullTableName = `${schema}.${toTableName(collection)}`;
testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`); return testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
}), });
);
truncationPromises.push(truncateMappingTables(dbType, collections, testDb));
return Promise.all(truncationPromises);
} }
/** /**
@ -167,11 +224,17 @@ export async function truncate(collections: CollectionName[], testDbName: string
); );
await truncateMySql(testDb, isShared); await truncateMySql(testDb, isShared);
await truncateMappingTables(dbType, collections, testDb);
await truncateMySql(testDb, isNotShared); 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 { return {
Credentials: 'credentials_entity', Credentials: 'credentials_entity',
Workflow: 'workflow_entity', Workflow: 'workflow_entity',
@ -183,10 +246,10 @@ function toTableName(collectionName: CollectionName) {
SharedCredentials: 'shared_credentials', SharedCredentials: 'shared_credentials',
SharedWorkflow: 'shared_workflow', SharedWorkflow: 'shared_workflow',
Settings: 'settings', Settings: 'settings',
}[collectionName]; }[sourceName];
} }
function truncateMySql(connection: Connection, collections: Array<keyof IDatabaseCollections>) { function truncateMySql(connection: Connection, collections: CollectionName[]) {
return Promise.all( return Promise.all(
collections.map(async (collection) => { collections.map(async (collection) => {
const tableName = toTableName(collection); const tableName = toTableName(collection);

View file

@ -2,9 +2,12 @@ import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-
import type { ICredentialsDb, IDatabaseCollections } from '../../../src'; import type { ICredentialsDb, IDatabaseCollections } from '../../../src';
import type { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity'; import type { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
import type { User } from '../../../src/databases/entities/User'; import type { User } from '../../../src/databases/entities/User';
import { MAPPING_TABLES } from './constants';
export type CollectionName = keyof IDatabaseCollections; export type CollectionName = keyof IDatabaseCollections;
export type MappingName = keyof typeof MAPPING_TABLES;
export type SmtpTestAccount = { export type SmtpTestAccount = {
user: string; user: string;
pass: string; pass: string;