ci: Simplify DB truncate in tests (no-changelog) (#5243)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-01-25 10:02:28 +01:00 committed by GitHub
parent 0c70a40317
commit ac460aa841
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 22 additions and 188 deletions

View file

@ -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 });

View file

@ -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;

View file

@ -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');
}
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;`);
const repository: Repository<any> = Db.collections[collection];
await repository.delete({});
}
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];
}
// ----------------------------------

View file

@ -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;

View file

@ -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 {

View file

@ -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 () => {