Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions packages/cli/test/integration/me.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

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

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

Expand Down
17 changes: 0 additions & 17 deletions packages/cli/test/integration/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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;
157 changes: 17 additions & 140 deletions packages/cli/test/integration/shared/testDb.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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];
}

// ----------------------------------
Expand Down
3 changes: 0 additions & 3 deletions packages/cli/test/integration/shared/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 0 additions & 14 deletions packages/cli/test/integration/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/test/unit/PermissionChecker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down