mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -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();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
describe('Owner shell', () => {
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
});
|
||||
|
||||
test('GET /me should return sanitized owner shell', async () => {
|
||||
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 () => {
|
||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
|
||||
|
@ -441,10 +437,6 @@ describe('Owner', () => {
|
|||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
});
|
||||
|
||||
test('GET /me should return sanitized owner', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
|
||||
|
|
|
@ -47,16 +47,6 @@ 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'],
|
||||
};
|
||||
|
||||
export const COMMUNITY_PACKAGE_VERSION = {
|
||||
CURRENT: '0.1.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.
|
||||
*/
|
||||
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 { DataSource as Connection, DataSourceOptions as ConnectionOptions } from 'typeorm';
|
||||
import {
|
||||
DataSource as Connection,
|
||||
DataSourceOptions as ConnectionOptions,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
import config from '@/config';
|
||||
import * as Db from '@/Db';
|
||||
|
@ -11,25 +15,24 @@ import { postgresMigrations } from '@db/migrations/postgresdb';
|
|||
import { sqliteMigrations } from '@db/migrations/sqlite';
|
||||
import { hashPassword } from '@/UserManagement/UserManagementHelper';
|
||||
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 { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||
import type { Role } from '@db/entities/Role';
|
||||
import { TagEntity } from '@db/entities/TagEntity';
|
||||
import { User } from '@db/entities/User';
|
||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import type { TagEntity } from '@db/entities/TagEntity';
|
||||
import type { User } from '@db/entities/User';
|
||||
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 {
|
||||
CollectionName,
|
||||
CredentialPayload,
|
||||
InstalledNodePayload,
|
||||
InstalledPackagePayload,
|
||||
MappingName,
|
||||
} 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';
|
||||
|
||||
|
@ -95,140 +98,14 @@ export async function terminate() {
|
|||
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.
|
||||
*
|
||||
* @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[]) {
|
||||
const dbType = config.getEnv('database.type');
|
||||
const testDb = Db.getConnection();
|
||||
|
||||
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');
|
||||
for (const collection of collections) {
|
||||
const repository: Repository<any> = Db.collections[collection];
|
||||
await repository.delete({});
|
||||
}
|
||||
|
||||
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 { User } from '@db/entities/User';
|
||||
import type { ICredentialsDb, IDatabaseCollections } from '@/Interfaces';
|
||||
import { MAPPING_TABLES } from './constants';
|
||||
|
||||
export type CollectionName = keyof IDatabaseCollections;
|
||||
|
||||
export type MappingName = keyof typeof MAPPING_TABLES;
|
||||
|
||||
export type ApiPath = 'internal' | 'public';
|
||||
|
||||
export type AuthAgent = (user: User) => SuperAgentTest;
|
||||
|
|
|
@ -675,20 +675,6 @@ export async function isInstanceOwnerSetUp() {
|
|||
// 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(
|
||||
schema = config.getSchema(),
|
||||
): PostgresSchemaSection | null {
|
||||
|
|
|
@ -49,8 +49,7 @@ beforeAll(async () => {
|
|||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['SharedWorkflow', 'SharedCredentials']);
|
||||
await testDb.truncate(['User', 'Workflow', 'Credentials']);
|
||||
await testDb.truncate(['SharedWorkflow', 'SharedCredentials', 'Workflow', 'Credentials', 'User']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
Loading…
Reference in a new issue