mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
ci: Simplify DB truncate in tests (no-changelog) (#5243)
This commit is contained in:
parent
0c70a40317
commit
ac460aa841
|
@ -35,15 +35,15 @@ beforeAll(async () => {
|
||||||
utils.initTestTelemetry();
|
utils.initTestTelemetry();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testDb.truncate(['User']);
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await testDb.terminate();
|
await testDb.terminate();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Owner shell', () => {
|
describe('Owner shell', () => {
|
||||||
beforeEach(async () => {
|
|
||||||
await testDb.truncate(['User']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GET /me should return sanitized owner shell', async () => {
|
test('GET /me should return sanitized owner shell', async () => {
|
||||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
|
|
||||||
|
@ -238,10 +238,6 @@ describe('Member', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await testDb.truncate(['User']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GET /me should return sanitized member', async () => {
|
test('GET /me should return sanitized member', async () => {
|
||||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||||
|
|
||||||
|
@ -441,10 +437,6 @@ describe('Owner', () => {
|
||||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await testDb.truncate(['User']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GET /me should return sanitized owner', async () => {
|
test('GET /me should return sanitized owner', async () => {
|
||||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
|
|
||||||
|
|
|
@ -47,16 +47,6 @@ 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'],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const COMMUNITY_PACKAGE_VERSION = {
|
export const COMMUNITY_PACKAGE_VERSION = {
|
||||||
CURRENT: '0.1.0',
|
CURRENT: '0.1.0',
|
||||||
UPDATED: '0.2.0',
|
UPDATED: '0.2.0',
|
||||||
|
@ -71,10 +61,3 @@ export const COMMUNITY_NODE_VERSION = {
|
||||||
* Timeout (in milliseconds) to account for DB being slow to initialize.
|
* Timeout (in milliseconds) to account for DB being slow to initialize.
|
||||||
*/
|
*/
|
||||||
export const DB_INITIALIZATION_TIMEOUT = 30_000;
|
export const DB_INITIALIZATION_TIMEOUT = 30_000;
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping tables having no entity representation.
|
|
||||||
*/
|
|
||||||
export const MAPPING_TABLES = {
|
|
||||||
WorkflowsTags: 'workflows_tags',
|
|
||||||
} as const;
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { UserSettings } from 'n8n-core';
|
import { UserSettings } from 'n8n-core';
|
||||||
import { DataSource as Connection, DataSourceOptions as ConnectionOptions } from 'typeorm';
|
import {
|
||||||
|
DataSource as Connection,
|
||||||
|
DataSourceOptions as ConnectionOptions,
|
||||||
|
Repository,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
|
@ -11,25 +15,24 @@ import { postgresMigrations } from '@db/migrations/postgresdb';
|
||||||
import { sqliteMigrations } from '@db/migrations/sqlite';
|
import { sqliteMigrations } from '@db/migrations/sqlite';
|
||||||
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
||||||
import { AuthIdentity } from '@/databases/entities/AuthIdentity';
|
import { AuthIdentity } from '@/databases/entities/AuthIdentity';
|
||||||
import { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
||||||
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
import { TagEntity } from '@db/entities/TagEntity';
|
import type { TagEntity } from '@db/entities/TagEntity';
|
||||||
import { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
|
import { ICredentialsDb } from '@/Interfaces';
|
||||||
|
|
||||||
|
import { DB_INITIALIZATION_TIMEOUT } from './constants';
|
||||||
|
import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random';
|
||||||
|
import { getPostgresSchemaSection } from './utils';
|
||||||
import type {
|
import type {
|
||||||
CollectionName,
|
CollectionName,
|
||||||
CredentialPayload,
|
CredentialPayload,
|
||||||
InstalledNodePayload,
|
InstalledNodePayload,
|
||||||
InstalledPackagePayload,
|
InstalledPackagePayload,
|
||||||
MappingName,
|
|
||||||
} from './types';
|
} from './types';
|
||||||
import { DB_INITIALIZATION_TIMEOUT, MAPPING_TABLES, MAPPING_TABLES_TO_CLEAR } from './constants';
|
|
||||||
import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random';
|
|
||||||
import { categorize, getPostgresSchemaSection } from './utils';
|
|
||||||
|
|
||||||
import type { DatabaseType, ICredentialsDb } from '@/Interfaces';
|
|
||||||
|
|
||||||
export type TestDBType = 'postgres' | 'mysql';
|
export type TestDBType = 'postgres' | 'mysql';
|
||||||
|
|
||||||
|
@ -95,140 +98,14 @@ export async function terminate() {
|
||||||
await Db.getConnection().destroy();
|
await Db.getConnection().destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function truncateMappingTables(
|
|
||||||
dbType: DatabaseType,
|
|
||||||
collections: 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(async (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');
|
|
||||||
|
|
||||||
// sequential TRUNCATEs to prevent race conditions
|
|
||||||
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 specific DB tables in a test DB.
|
* 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[]) {
|
export async function truncate(collections: CollectionName[]) {
|
||||||
const dbType = config.getEnv('database.type');
|
for (const collection of collections) {
|
||||||
const testDb = Db.getConnection();
|
const repository: Repository<any> = Db.collections[collection];
|
||||||
|
await repository.delete({});
|
||||||
if (dbType === 'sqlite') {
|
|
||||||
await testDb.query('PRAGMA foreign_keys=OFF');
|
|
||||||
|
|
||||||
const truncationPromises = collections.map(async (collection) => {
|
|
||||||
const tableName = toTableName(collection);
|
|
||||||
// Db.collections[collection].clear();
|
|
||||||
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') {
|
|
||||||
const schema = config.getEnv('database.postgresdb.schema');
|
|
||||||
|
|
||||||
// sequential TRUNCATEs to prevent race conditions
|
|
||||||
for (const collection of collections) {
|
|
||||||
const fullTableName = `${schema}.${toTableName(collection)}`;
|
|
||||||
await testDb.query(`TRUNCATE TABLE ${fullTableName} RESTART IDENTITY CASCADE;`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return truncateMappingTables(dbType, collections, testDb);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dbType === 'mysqldb') {
|
|
||||||
const { pass: sharedTables, fail: rest } = categorize(collections, (c: CollectionName) =>
|
|
||||||
c.toLowerCase().startsWith('shared'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// sequential DELETEs to prevent race conditions
|
|
||||||
// clear foreign-key tables first to avoid deadlocks on MySQL: https://stackoverflow.com/a/41174997
|
|
||||||
for (const collection of [...sharedTables, ...rest]) {
|
|
||||||
const tableName = toTableName(collection);
|
|
||||||
|
|
||||||
await testDb.query(`DELETE FROM ${tableName};`);
|
|
||||||
|
|
||||||
const hasIdColumn = await testDb
|
|
||||||
.query(`SHOW COLUMNS FROM ${tableName}`)
|
|
||||||
.then((columns: Array<{ Field: string }>) => columns.find((c) => c.Field === 'id'));
|
|
||||||
|
|
||||||
if (!hasIdColumn) continue;
|
|
||||||
|
|
||||||
await testDb.query(`ALTER TABLE ${tableName} AUTO_INCREMENT = 1;`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return truncateMappingTables(dbType, collections, testDb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
AuthIdentity: 'auth_identity',
|
|
||||||
AuthProviderSyncHistory: 'auth_provider_sync_history',
|
|
||||||
Credentials: 'credentials_entity',
|
|
||||||
Execution: 'execution_entity',
|
|
||||||
InstalledNodes: 'installed_nodes',
|
|
||||||
InstalledPackages: 'installed_packages',
|
|
||||||
Role: 'role',
|
|
||||||
Settings: 'settings',
|
|
||||||
SharedCredentials: 'shared_credentials',
|
|
||||||
SharedWorkflow: 'shared_workflow',
|
|
||||||
Tag: 'tag_entity',
|
|
||||||
User: 'user',
|
|
||||||
Webhook: 'webhook_entity',
|
|
||||||
Workflow: 'workflow_entity',
|
|
||||||
WorkflowStatistics: 'workflow_statistics',
|
|
||||||
EventDestinations: 'event_destinations',
|
|
||||||
}[sourceName];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
|
@ -4,12 +4,9 @@ import type { SuperAgentTest } from 'supertest';
|
||||||
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
import type { ICredentialsDb, IDatabaseCollections } from '@/Interfaces';
|
import type { ICredentialsDb, IDatabaseCollections } from '@/Interfaces';
|
||||||
import { MAPPING_TABLES } from './constants';
|
|
||||||
|
|
||||||
export type CollectionName = keyof IDatabaseCollections;
|
export type CollectionName = keyof IDatabaseCollections;
|
||||||
|
|
||||||
export type MappingName = keyof typeof MAPPING_TABLES;
|
|
||||||
|
|
||||||
export type ApiPath = 'internal' | 'public';
|
export type ApiPath = 'internal' | 'public';
|
||||||
|
|
||||||
export type AuthAgent = (user: User) => SuperAgentTest;
|
export type AuthAgent = (user: User) => SuperAgentTest;
|
||||||
|
|
|
@ -675,20 +675,6 @@ export async function isInstanceOwnerSetUp() {
|
||||||
// misc
|
// misc
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
||||||
/**
|
|
||||||
* Categorize array items into two groups based on whether they pass a test.
|
|
||||||
*/
|
|
||||||
export const categorize = <T>(arr: T[], test: (str: T) => boolean) => {
|
|
||||||
return arr.reduce<{ pass: T[]; fail: T[] }>(
|
|
||||||
(acc, cur) => {
|
|
||||||
test(cur) ? acc.pass.push(cur) : acc.fail.push(cur);
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ pass: [], fail: [] },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getPostgresSchemaSection(
|
export function getPostgresSchemaSection(
|
||||||
schema = config.getSchema(),
|
schema = config.getSchema(),
|
||||||
): PostgresSchemaSection | null {
|
): PostgresSchemaSection | null {
|
||||||
|
|
|
@ -49,8 +49,7 @@ beforeAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await testDb.truncate(['SharedWorkflow', 'SharedCredentials']);
|
await testDb.truncate(['SharedWorkflow', 'SharedCredentials', 'Workflow', 'Credentials', 'User']);
|
||||||
await testDb.truncate(['User', 'Workflow', 'Credentials']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|
Loading…
Reference in a new issue