feat: setup nightly tests for postgres and mysql schemas (#4441)

* feat: unify Jest config

* feat: simplify DB setup for tests

* feat: setup nightly tests for postgres and mysql schemas
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2022-10-25 22:06:03 +02:00 committed by GitHub
parent 5c9b40117a
commit 99157cf581
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 352 additions and 9984 deletions

19
.github/docker-compose.yml vendored Normal file
View file

@ -0,0 +1,19 @@
version: '3.9'
services:
mysql:
image: mysql:5.7-debian
environment:
- MYSQL_DATABASE=n8n
- MYSQL_ROOT_PASSWORD=password
ports:
- 3306:3306
postgres:
image: postgres:11
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=root
- POSTGRES_PASSWORD=password
ports:
- 5432:5432

47
.github/workflows/ci-postgres-mysql.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Test Postgres and MySQL schemas
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
DB_MYSQLDB_PASSWORD: password
DB_POSTGRESDB_PASSWORD: password
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- name: Install npm and dependencies
run: npm install -g npm@latest && npm install
- name: Start MySQL & Postgres
uses: isbang/compose-action@v1.3.2
with:
compose-file: ./.github/docker-compose.yml
- name: Build Core & Workflow
run: npm run -w packages/workflow -w packages/core build
- name: Test MySQL
working-directory: packages/cli
run: npm run test:mysql
- name: Test Postgres
working-directory: packages/cli
run: npm run test:postgres
- name: Test Postgres (alternate schema)
working-directory: packages/cli
run: npm run test:postgres:alt-schema

21
jest.config.js Normal file
View file

@ -0,0 +1,21 @@
const { compilerOptions } = require('./tsconfig.json');
/** @type {import('jest').Config} */
module.exports = {
verbose: true,
preset: 'ts-jest',
testEnvironment: 'node',
testRegex: '\\.(test|spec)\\.(js|ts)$',
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
globals: {
'ts-jest': {
isolatedModules: true,
tsconfig: {
...compilerOptions,
declaration: false,
sourceMap: false,
skipLibCheck: true,
},
},
},
};

9851
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -22,9 +22,14 @@
"worker": "./packages/cli/bin/n8n worker"
},
"devDependencies": {
"@types/jest": "^28.1.8",
"jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.3",
"jest-mock": "^28.1.3",
"patch-package": "^6.4.7",
"rimraf": "^3.0.2",
"run-script-os": "^1.0.7",
"ts-jest": "^28.0.8",
"turbo": "1.2.15"
},
"postcss": {},

View file

@ -13,6 +13,7 @@ const config = (module.exports = {
'node_modules/**',
'dist/**',
'test/**', // TODO: remove this
'jest.config.js', // TODO: remove this
],
plugins: [

View file

@ -0,0 +1,2 @@
/** @type {import('jest').Config} */
module.exports = require('../../../jest.config');

View file

@ -13,8 +13,7 @@
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n8n-local-rules": "^1.0.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.4.0",
"jest": "^28.1.3"
"eslint-plugin-vue": "^9.4.0"
},
"scripts": {
"test": "jest"

View file

@ -1,17 +1,8 @@
/** @type {import('jest').Config} */
module.exports = {
verbose: true,
transform: {
'^.+\\.ts$': 'ts-jest',
},
testURL: 'http://localhost/',
testRegex: '(/__tests__/.*|(\\.|/)(test))\\.ts$',
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
moduleFileExtensions: ['ts', 'js', 'json'],
globals: {
'ts-jest': {
isolatedModules: true,
},
...require('../../jest.config'),
testEnvironmentOptions: {
url: 'http://localhost/',
},
globalTeardown: '<rootDir>/test/teardown.ts',
setupFiles: ['<rootDir>/test/setup.ts'],
};

View file

@ -69,7 +69,6 @@
"@types/convict": "^4.2.1",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.6",
"@types/jest": "^27.4.0",
"@types/json-diff": "^0.5.1",
"@types/jsonwebtoken": "^8.5.2",
"@types/localtunnel": "^1.9.0",
@ -94,12 +93,9 @@
"@types/validator": "^13.7.0",
"@types/yamljs": "^0.2.31",
"concurrently": "^5.1.0",
"jest": "^27.4.7",
"jest-mock": "^28.1.3",
"nodemon": "^2.0.2",
"run-script-os": "^1.0.7",
"supertest": "^6.2.2",
"ts-jest": "^27.1.3",
"ts-node": "^8.9.1",
"typescript": "~4.8.0"
},

View file

@ -57,18 +57,6 @@ export const MAPPING_TABLES_TO_CLEAR: Record<string, string[] | undefined> = {
Tag: ['workflows_tags'],
};
/**
* Name of the connection used for creating and dropping a Postgres DB
* for each suite test run.
*/
export const BOOTSTRAP_POSTGRES_CONNECTION_NAME: Readonly<string> = 'n8n_bs_postgres';
/**
* Name of the connection (and database) used for creating and dropping a MySQL DB
* for each suite test run.
*/
export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly<string> = 'n8n_bs_mysql';
export const COMMUNITY_PACKAGE_VERSION = {
CURRENT: '0.1.0',
UPDATED: '0.2.0',

View file

@ -1,6 +1,3 @@
import { exec as callbackExec } from 'child_process';
import { promisify } from 'util';
import { UserSettings } from 'n8n-core';
import { Connection, ConnectionOptions, createConnection, getConnection } from 'typeorm';
@ -13,13 +10,7 @@ import { mysqlMigrations } from '../../../src/databases/migrations/mysqldb';
import { postgresMigrations } from '../../../src/databases/migrations/postgresdb';
import { sqliteMigrations } from '../../../src/databases/migrations/sqlite';
import { hashPassword } from '../../../src/UserManagement/UserManagementHelper';
import {
BOOTSTRAP_MYSQL_CONNECTION_NAME,
BOOTSTRAP_POSTGRES_CONNECTION_NAME,
DB_INITIALIZATION_TIMEOUT,
MAPPING_TABLES,
MAPPING_TABLES_TO_CLEAR,
} from './constants';
import { DB_INITIALIZATION_TIMEOUT, MAPPING_TABLES, MAPPING_TABLES_TO_CLEAR } from './constants';
import {
randomApiKey,
randomCredentialPayload,
@ -45,17 +36,16 @@ import type {
MappingName,
} from './types';
const exec = promisify(callbackExec);
export type TestDBType = 'postgres' | 'mysql';
/**
* Initialize one test DB per suite run, with bootstrap connection if needed.
*/
export async function init() {
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
const dbType = config.getEnv('database.type');
if (dbType === 'sqlite') {
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
// no bootstrap connection required
const testDbName = `n8n_test_sqlite_${randomString(6, 10)}_${Date.now()}`;
await Db.init(getSqliteOptions({ name: testDbName }));
@ -65,10 +55,8 @@ export async function init() {
}
if (dbType === 'postgresdb') {
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
let bootstrapPostgres;
const pgOptions = getBootstrapPostgresOptions();
const pgOptions = getBootstrapDBOptions('postgres');
try {
bootstrapPostgres = await createConnection(pgOptions);
@ -91,40 +79,32 @@ export async function init() {
process.exit(1);
}
const testDbName = `pg_${randomString(6, 10)}_${Date.now()}_n8n_test`;
await bootstrapPostgres.query(`CREATE DATABASE ${testDbName};`);
const testDbName = `postgres_${randomString(6, 10)}_${Date.now()}_n8n_test`;
await bootstrapPostgres.query(`CREATE DATABASE ${testDbName}`);
await bootstrapPostgres.close();
try {
const schema = config.getEnv('database.postgresdb.schema');
const exportPasswordCli = pgOptions.password
? `export PGPASSWORD=${pgOptions.password} && `
: '';
await exec(
`${exportPasswordCli} psql -h ${pgOptions.host} -U ${pgOptions.username} -d ${testDbName} -c "CREATE SCHEMA IF NOT EXISTS ${schema}";`,
);
} catch (error) {
if (error instanceof Error && error.message.includes('command not found')) {
console.error(
'psql command not found. Make sure psql is installed and added to your PATH.',
);
}
process.exit(1);
const dbOptions = getDBOptions('postgres', testDbName);
if (dbOptions.schema !== 'public') {
const { schema, migrations, ...options } = dbOptions;
const connection = await createConnection(options);
await connection.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
await connection.close();
}
await Db.init(getPostgresOptions({ name: testDbName }));
await Db.init(dbOptions);
return { testDbName };
}
if (dbType === 'mysqldb') {
// initialization timeout in test/setup.ts
const bootstrapMysql = await createConnection(getBootstrapMySqlOptions());
const bootstrapMysql = await createConnection(getBootstrapDBOptions('mysql'));
const testDbName = `mysql_${randomString(6, 10)}_${Date.now()}_n8n_test`;
await bootstrapMysql.query(`CREATE DATABASE ${testDbName};`);
await bootstrapMysql.query(`CREATE DATABASE ${testDbName}`);
await bootstrapMysql.close();
await Db.init(getMySqlOptions({ name: testDbName }));
await Db.init(getDBOptions('mysql', testDbName));
return { testDbName };
}
@ -136,27 +116,7 @@ export async function init() {
* Drop test DB, closing bootstrap connection if existing.
*/
export async function terminate(testDbName: string) {
const dbType = config.getEnv('database.type');
if (dbType === 'sqlite') {
await getConnection(testDbName).close();
}
if (dbType === 'postgresdb') {
await getConnection(testDbName).close();
const bootstrapPostgres = getConnection(BOOTSTRAP_POSTGRES_CONNECTION_NAME);
await bootstrapPostgres.query(`DROP DATABASE ${testDbName}`);
await bootstrapPostgres.close();
}
if (dbType === 'mysqldb') {
await getConnection(testDbName).close();
const bootstrapMySql = getConnection(BOOTSTRAP_MYSQL_CONNECTION_NAME);
await bootstrapMySql.query(`DROP DATABASE ${testDbName}`);
await bootstrapMySql.close();
}
await getConnection(testDbName).close();
}
async function truncateMappingTables(
@ -277,6 +237,7 @@ function toTableName(sourceName: CollectionName | MappingName) {
return {
Credentials: 'credentials_entity',
CredentialUsage: 'credential_usage',
Workflow: 'workflow_entity',
Execution: 'execution_entity',
Tag: 'tag_entity',
@ -714,100 +675,38 @@ export const getSqliteOptions = ({ name }: { name: string }): ConnectionOptions
};
};
/**
* Generate options for a bootstrap Postgres connection,
* to create and drop test Postgres databases.
*/
export const getBootstrapPostgresOptions = () => {
const username = config.getEnv('database.postgresdb.user');
const password = config.getEnv('database.postgresdb.password');
const host = config.getEnv('database.postgresdb.host');
const port = config.getEnv('database.postgresdb.port');
const schema = config.getEnv('database.postgresdb.schema');
return {
name: BOOTSTRAP_POSTGRES_CONNECTION_NAME,
type: 'postgres',
database: 'postgres', // pre-existing default database
host,
port,
username,
password,
schema,
} as const;
};
export const getPostgresOptions = ({ name }: { name: string }): ConnectionOptions => {
const username = config.getEnv('database.postgresdb.user');
const password = config.getEnv('database.postgresdb.password');
const host = config.getEnv('database.postgresdb.host');
const port = config.getEnv('database.postgresdb.port');
const schema = config.getEnv('database.postgresdb.schema');
return {
name,
type: 'postgres',
database: name,
host,
port,
username,
password,
entityPrefix: '',
schema,
dropSchema: true,
migrations: postgresMigrations,
migrationsRun: true,
migrationsTableName: 'migrations',
entities: Object.values(entities),
synchronize: false,
logging: false,
};
};
const baseOptions = (type: TestDBType) => ({
host: config.getEnv(`database.${type}db.host`),
port: config.getEnv(`database.${type}db.port`),
username: config.getEnv(`database.${type}db.user`),
password: config.getEnv(`database.${type}db.password`),
schema: type === 'postgres' ? config.getEnv(`database.postgresdb.schema`) : undefined,
});
/**
* Generate options for a bootstrap MySQL connection,
* to create and drop test MySQL databases.
* Generate options for a bootstrap DB connection, to create and drop test databases.
*/
export const getBootstrapMySqlOptions = (): ConnectionOptions => {
const username = config.getEnv('database.mysqldb.user');
const password = config.getEnv('database.mysqldb.password');
const host = config.getEnv('database.mysqldb.host');
const port = config.getEnv('database.mysqldb.port');
export const getBootstrapDBOptions = (type: TestDBType) =>
({
type,
name: type,
database: type,
...baseOptions(type),
} as const);
return {
name: BOOTSTRAP_MYSQL_CONNECTION_NAME,
database: BOOTSTRAP_MYSQL_CONNECTION_NAME,
type: 'mysql',
host,
port,
username,
password,
};
};
/**
* Generate options for a MySQL database connection,
* one per test suite run.
*/
export const getMySqlOptions = ({ name }: { name: string }): ConnectionOptions => {
const username = config.getEnv('database.mysqldb.user');
const password = config.getEnv('database.mysqldb.password');
const host = config.getEnv('database.mysqldb.host');
const port = config.getEnv('database.mysqldb.port');
return {
name,
database: name,
type: 'mysql',
host,
port,
username,
password,
migrations: mysqlMigrations,
migrationsTableName: 'migrations',
migrationsRun: true,
};
};
const getDBOptions = (type: TestDBType, name: string) => ({
type,
name,
database: name,
...baseOptions(type),
dropSchema: true,
migrations: type === 'postgres' ? postgresMigrations : mysqlMigrations,
migrationsRun: true,
migrationsTableName: 'migrations',
entities: Object.values(entities),
synchronize: false,
logging: false,
});
// ----------------------------------
// encryption

View file

@ -1,36 +0,0 @@
import { exec as callbackExec } from 'child_process';
import { promisify } from 'util';
import config from '../config';
import {
BOOTSTRAP_MYSQL_CONNECTION_NAME,
DB_INITIALIZATION_TIMEOUT,
} from './integration/shared/constants';
const exec = promisify(callbackExec);
const dbType = config.getEnv('database.type');
if (dbType === 'mysqldb') {
const username = config.getEnv('database.mysqldb.user');
const password = config.getEnv('database.mysqldb.password');
const host = config.getEnv('database.mysqldb.host');
const passwordSegment = password ? `-p${password}` : '';
(async () => {
try {
jest.setTimeout(DB_INITIALIZATION_TIMEOUT);
await exec(
`echo "CREATE DATABASE IF NOT EXISTS ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${username} ${passwordSegment}; USE ${BOOTSTRAP_MYSQL_CONNECTION_NAME};`,
);
} catch (error) {
if (error.stderr.includes('Access denied')) {
console.error(
`ERROR: Failed to log into MySQL to create bootstrap DB.\nPlease review your MySQL connection options:\n\thost: "${host}"\n\tusername: "${username}"\n\tpassword: "${password}"\nFix by setting correct values via environment variables.\n\texport DB_MYSQLDB_HOST=value\n\texport DB_MYSQLDB_USERNAME=value\n\texport DB_MYSQLDB_PASSWORD=value`,
);
process.exit(1);
}
}
})();
}

View file

@ -1,47 +1,23 @@
import { createConnection } from 'typeorm';
import config from '../config';
import { exec } from 'child_process';
import { getBootstrapMySqlOptions, getBootstrapPostgresOptions } from './integration/shared/testDb';
import { BOOTSTRAP_MYSQL_CONNECTION_NAME } from './integration/shared/constants';
import { getBootstrapDBOptions } from './integration/shared/testDb';
export default async () => {
const dbType = config.getEnv('database.type');
const dbType = config.getEnv('database.type').replace(/db$/, '');
if (dbType !== 'postgres' && dbType !== 'mysql') return;
if (dbType === 'postgresdb') {
const bootstrapPostgres = await createConnection(getBootstrapPostgresOptions());
const connection = await createConnection(getBootstrapDBOptions(dbType));
const results: { db_name: string }[] = await bootstrapPostgres.query(
'SELECT datname as db_name FROM pg_database;',
);
const query =
dbType === 'postgres' ? 'SELECT datname as "Database" FROM pg_database' : 'SHOW DATABASES';
const results: { Database: string }[] = await connection.query(query);
const databases = results
.filter(
({ Database: dbName }) => dbName.startsWith(`${dbType}_`) && dbName.endsWith('_n8n_test'),
)
.map(({ Database: dbName }) => dbName);
const promises = results
.filter(({ db_name: dbName }) => dbName.startsWith('pg_') && dbName.endsWith('_n8n_test'))
.map(({ db_name: dbName }) => bootstrapPostgres.query(`DROP DATABASE ${dbName};`));
await Promise.all(promises);
bootstrapPostgres.close();
}
if (dbType === 'mysqldb') {
const user = config.getEnv('database.mysqldb.user');
const password = config.getEnv('database.mysqldb.password');
const host = config.getEnv('database.mysqldb.host');
const bootstrapMySql = await createConnection(getBootstrapMySqlOptions());
const results: { Database: string }[] = await bootstrapMySql.query('SHOW DATABASES;');
const promises = results
.filter(({ Database: dbName }) => dbName.startsWith('mysql_') && dbName.endsWith('_n8n_test'))
.map(({ Database: dbName }) => bootstrapMySql.query(`DROP DATABASE ${dbName};`));
await Promise.all(promises);
await bootstrapMySql.close();
exec(
`echo "DROP DATABASE ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${user} -p${password}`,
);
}
const promises = databases.map((dbName) => connection.query(`DROP DATABASE ${dbName};`));
await Promise.all(promises);
await connection.close();
};

View file

@ -0,0 +1,2 @@
/** @type {import('jest').Config} */
module.exports = require('../../jest.config');

View file

@ -31,15 +31,12 @@
"@types/cron": "~1.7.1",
"@types/crypto-js": "^4.0.1",
"@types/express": "^4.17.6",
"@types/jest": "^27.4.0",
"@types/lodash.get": "^4.4.6",
"@types/mime-types": "^2.1.0",
"@types/node": "^16.11.22",
"@types/request-promise-native": "~1.0.15",
"@types/uuid": "^8.3.2",
"jest": "^27.4.7",
"source-map-support": "^0.5.9",
"ts-jest": "^27.1.3",
"typescript": "~4.8.0"
},
"dependencies": {
@ -59,22 +56,5 @@
"request": "^2.88.2",
"request-promise-native": "^1.0.7",
"uuid": "^8.3.2"
},
"jest": {
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testURL": "http://localhost/",
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$",
"testPathIgnorePatterns": [
"/dist/",
"/node_modules/"
],
"moduleFileExtensions": [
"ts",
"js",
"json",
"node"
]
}
}

View file

@ -0,0 +1,2 @@
/** @type {import('jest').Config} */
module.exports = require('../../jest.config');

View file

@ -732,7 +732,6 @@
"@types/formidable": "^1.0.31",
"@types/gm": "^1.18.2",
"@types/imap-simple": "^4.2.0",
"@types/jest": "^27.4.0",
"@types/jsonwebtoken": "^8.5.2",
"@types/lodash.set": "^4.3.6",
"@types/lossless-json": "^1.0.0",
@ -752,9 +751,7 @@
"@types/xml2js": "^0.4.3",
"eslint-plugin-n8n-nodes-base": "^1.10.0",
"gulp": "^4.0.0",
"jest": "^27.4.7",
"n8n-workflow": "~0.121.0",
"ts-jest": "^27.1.3",
"tslint": "^6.1.2",
"typescript": "~4.8.0"
},
@ -816,21 +813,5 @@
"vm2": "~3.9.5",
"xlsx": "^0.17.0",
"xml2js": "^0.4.23"
},
"jest": {
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testURL": "http://localhost/",
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$",
"testPathIgnorePatterns": [
"/dist/",
"/node_modules/"
],
"moduleFileExtensions": [
"ts",
"js",
"json"
]
}
}

View file

@ -0,0 +1,2 @@
/** @type {import('jest').Config} */
module.exports = require('../../jest.config');

View file

@ -39,7 +39,6 @@
"devDependencies": {
"@n8n_io/eslint-config": "",
"@types/express": "^4.17.6",
"@types/jest": "^27.4.0",
"@types/jmespath": "^0.15.0",
"@types/lodash.get": "^4.4.6",
"@types/lodash.merge": "^4.6.6",
@ -47,10 +46,7 @@
"@types/luxon": "^2.0.9",
"@types/node": "^16.11.22",
"@types/xml2js": "^0.4.3",
"jest": "^27.4.7",
"jest-environment-jsdom": "^27.5.1",
"prettier": "^2.3.2",
"ts-jest": "^27.1.3",
"typescript": "~4.8.0"
},
"dependencies": {
@ -62,21 +58,5 @@
"lodash.set": "^4.3.2",
"luxon": "~2.3.0",
"xml2js": "^0.4.23"
},
"jest": {
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testURL": "http://localhost/",
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$",
"testPathIgnorePatterns": [
"/dist/",
"/node_modules/"
],
"moduleFileExtensions": [
"ts",
"js",
"json"
]
}
}