test: Make oclif commands testable (#3571)

*  Add `@oclif/core`

* 📦 Update `package-lock.json`

* 📘 Export `Logger` for use as type

*  Create `BaseCommand`

* 🐛 Prevent DB re-init

* ♻️ Refactor `reset` command

* 🧪 Fix `reset` test

* 👕 Add lint exception

Co-authored-by: Jan Oberhauser <janober@users.noreply.github.com>
This commit is contained in:
Iván Ovejero 2022-06-26 06:03:46 +02:00 committed by GitHub
parent 848fcfde5d
commit 7879239e03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 396 additions and 91 deletions

275
package-lock.json generated
View file

@ -18,6 +18,7 @@
"@kafkajs/confluent-schema-registry": "1.0.6",
"@n8n_io/riot-tmpl": "^1.0.1",
"@oclif/command": "^1.5.18",
"@oclif/core": "^1.9.3",
"@oclif/dev-cli": "^1.22.2",
"@oclif/errors": "^1.2.2",
"@rudderstack/rudder-sdk-node": "1.0.6",
@ -11404,6 +11405,160 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@oclif/core": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@oclif/core/-/core-1.9.3.tgz",
"integrity": "sha512-npxWULRu+iW9AuUNoCH118MNI8cxYIkcWkknz3mCDumTo11FC+h3OY1cMtlclqZHfZcDHh4iaSkNMX/7se9GUQ==",
"dependencies": {
"@oclif/linewrap": "^1.0.0",
"@oclif/screen": "^3.0.2",
"ansi-escapes": "^4.3.2",
"ansi-styles": "^4.3.0",
"cardinal": "^2.1.1",
"chalk": "^4.1.2",
"clean-stack": "^3.0.1",
"cli-progress": "^3.10.0",
"debug": "^4.3.4",
"ejs": "^3.1.6",
"fs-extra": "^9.1.0",
"get-package-type": "^0.1.0",
"globby": "^11.1.0",
"hyperlinker": "^1.0.0",
"indent-string": "^4.0.0",
"is-wsl": "^2.2.0",
"js-yaml": "^3.14.1",
"natural-orderby": "^2.0.3",
"object-treeify": "^1.1.33",
"password-prompt": "^1.1.2",
"semver": "^7.3.7",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"supports-color": "^8.1.1",
"supports-hyperlinks": "^2.2.0",
"tslib": "^2.3.1",
"widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@oclif/core/node_modules/@oclif/screen": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-3.0.2.tgz",
"integrity": "sha512-S/SF/XYJeevwIgHFmVDAFRUvM3m+OjhvCAYMk78ZJQCYCQ5wS7j+LTt1ZEv2jpEEGg2tx/F6TYYWxddNAYHrFQ==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@oclif/core/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@oclif/core/node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@oclif/core/node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@oclif/core/node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/@oclif/core/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@oclif/core/node_modules/semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@oclif/core/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/@oclif/core/node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/@oclif/core/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@oclif/core/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@oclif/dev-cli": {
"version": "1.26.10",
"resolved": "https://registry.npmjs.org/@oclif/dev-cli/-/dev-cli-1.26.10.tgz",
@ -69729,6 +69884,126 @@
}
}
},
"@oclif/core": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@oclif/core/-/core-1.9.3.tgz",
"integrity": "sha512-npxWULRu+iW9AuUNoCH118MNI8cxYIkcWkknz3mCDumTo11FC+h3OY1cMtlclqZHfZcDHh4iaSkNMX/7se9GUQ==",
"requires": {
"@oclif/linewrap": "^1.0.0",
"@oclif/screen": "^3.0.2",
"ansi-escapes": "^4.3.2",
"ansi-styles": "^4.3.0",
"cardinal": "^2.1.1",
"chalk": "^4.1.2",
"clean-stack": "^3.0.1",
"cli-progress": "^3.10.0",
"debug": "^4.3.4",
"ejs": "^3.1.6",
"fs-extra": "^9.1.0",
"get-package-type": "^0.1.0",
"globby": "^11.1.0",
"hyperlinker": "^1.0.0",
"indent-string": "^4.0.0",
"is-wsl": "^2.2.0",
"js-yaml": "^3.14.1",
"natural-orderby": "^2.0.3",
"object-treeify": "^1.1.33",
"password-prompt": "^1.1.2",
"semver": "^7.3.7",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"supports-color": "^8.1.1",
"supports-hyperlinks": "^2.2.0",
"tslib": "^2.3.1",
"widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0"
},
"dependencies": {
"@oclif/screen": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-3.0.2.tgz",
"integrity": "sha512-S/SF/XYJeevwIgHFmVDAFRUvM3m+OjhvCAYMk78ZJQCYCQ5wS7j+LTt1ZEv2jpEEGg2tx/F6TYYWxddNAYHrFQ=="
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"dependencies": {
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"requires": {
"has-flag": "^4.0.0"
}
},
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
},
"@oclif/dev-cli": {
"version": "1.26.10",
"resolved": "https://registry.npmjs.org/@oclif/dev-cli/-/dev-cli-1.26.10.tgz",

View file

@ -0,0 +1,57 @@
import { Command } from '@oclif/core';
import { LoggerProxy } from 'n8n-workflow';
import { getLogger, Logger } from '../src/Logger';
import { User } from '../src/databases/entities/User';
import { Db } from '../src';
export abstract class BaseCommand extends Command {
logger: Logger;
/**
* Lifecycle methods
*/
async init(): Promise<void> {
this.logger = getLogger();
LoggerProxy.init(this.logger);
await Db.init();
}
async finally(): Promise<void> {
if (process.env.NODE_ENV === 'test') return;
this.exit();
}
/**
* User Management utils
*/
defaultUserProps = {
firstName: null,
lastName: null,
email: null,
password: null,
resetPasswordToken: null,
};
async getInstanceOwner(): Promise<User> {
const globalRole = await Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'global',
});
const owner = await Db.collections.User.findOne({ globalRole });
if (owner) return owner;
const user = new User();
Object.assign(user, { ...this.defaultUserProps, globalRole });
await Db.collections.User.save(user);
return Db.collections.User.findOneOrFail({ globalRole });
}
}

View file

@ -1,85 +1,51 @@
/* eslint-disable no-console */
import Command from '@oclif/command';
import { Not } from 'typeorm';
import { LoggerProxy } from 'n8n-workflow';
import { Db } from '../../src';
import { User } from '../../src/databases/entities/User';
import { getLogger } from '../../src/Logger';
import { BaseCommand } from '../BaseCommand';
export class Reset extends Command {
export class Reset extends BaseCommand {
static description = '\nResets the database to the default user state';
private defaultUserProps = {
firstName: null,
lastName: null,
email: null,
password: null,
resetPasswordToken: null,
};
async run(): Promise<void> {
const logger = getLogger();
LoggerProxy.init(logger);
await Db.init();
const owner = await this.getInstanceOwner();
try {
const owner = await this.getInstanceOwner();
const ownerWorkflowRole = await Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'workflow',
});
const ownerCredentialRole = await Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'credential',
});
await Db.collections.SharedWorkflow.update(
{ user: { id: Not(owner.id) }, role: ownerWorkflowRole },
{ user: owner },
);
await Db.collections.SharedCredentials.update(
{ user: { id: Not(owner.id) }, role: ownerCredentialRole },
{ user: owner },
);
await Db.collections.User.delete({ id: Not(owner.id) });
await Db.collections.User.save(Object.assign(owner, this.defaultUserProps));
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: 'false' },
);
await Db.collections.Settings.update(
{ key: 'userManagement.skipInstanceOwnerSetup' },
{ value: 'false' },
);
} catch (error) {
console.error('Error resetting database. See log messages for details.');
if (error instanceof Error) logger.error(error.message);
this.exit(1);
}
console.info('Successfully reset the database to default user state.');
this.exit();
}
private async getInstanceOwner(): Promise<User> {
const globalRole = await Db.collections.Role.findOneOrFail({
const ownerWorkflowRole = await Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'global',
scope: 'workflow',
});
const owner = await Db.collections.User.findOne({ globalRole });
const ownerCredentialRole = await Db.collections.Role.findOneOrFail({
name: 'owner',
scope: 'credential',
});
if (owner) return owner;
await Db.collections.SharedWorkflow.update(
{ user: { id: Not(owner.id) }, role: ownerWorkflowRole },
{ user: owner },
);
const user = new User();
await Db.collections.SharedCredentials.update(
{ user: { id: Not(owner.id) }, role: ownerCredentialRole },
{ user: owner },
);
await Db.collections.User.save(Object.assign(user, { ...this.defaultUserProps, globalRole }));
await Db.collections.User.delete({ id: Not(owner.id) });
await Db.collections.User.save(Object.assign(owner, this.defaultUserProps));
return Db.collections.User.findOneOrFail({ globalRole });
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: 'false' },
);
await Db.collections.Settings.update(
{ key: 'userManagement.skipInstanceOwnerSetup' },
{ value: 'false' },
);
this.logger.info('Successfully reset the database to default user state.');
}
async catch(error: Error): Promise<void> {
this.logger.error('Error resetting database. See log messages for details.');
this.logger.error(error.message);
this.exit(1);
}
}

View file

@ -92,6 +92,7 @@
"typescript": "~4.6.0"
},
"dependencies": {
"@oclif/core": "^1.9.3",
"@apidevtools/swagger-cli": "4.0.0",
"@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2",

View file

@ -45,6 +45,8 @@ export function linkRepository<Entity>(entityClass: EntityTarget<Entity>): Repos
export async function init(
testConnectionOptions?: ConnectionOptions,
): Promise<IDatabaseCollections> {
if (isInitialized) return collections;
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
const n8nFolder = UserSettings.getUserN8nFolderPath();

View file

@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { inspect } from 'util';
import winston from 'winston';
@ -11,7 +12,7 @@ import callsites from 'callsites';
import { basename } from 'path';
import config from '../config';
class Logger implements ILogger {
export class Logger implements ILogger {
private logger: winston.Logger;
constructor() {
@ -71,7 +72,7 @@ class Logger implements ILogger {
}
}
log(type: LogTypes, message: string, meta: object = {}) {
log(type: LogTypes, message: string, meta: object = {}): void {
const callsite = callsites();
// We are using the third array element as the structure is as follows:
// [0]: this file
@ -93,23 +94,23 @@ class Logger implements ILogger {
// Convenience methods below
debug(message: string, meta: object = {}) {
debug(message: string, meta: object = {}): void {
this.log('debug', message, meta);
}
info(message: string, meta: object = {}) {
info(message: string, meta: object = {}): void {
this.log('info', message, meta);
}
error(message: string, meta: object = {}) {
error(message: string, meta: object = {}): void {
this.log('error', message, meta);
}
verbose(message: string, meta: object = {}) {
verbose(message: string, meta: object = {}): void {
this.log('verbose', message, meta);
}
warn(message: string, meta: object = {}) {
warn(message: string, meta: object = {}): void {
this.log('warn', message, meta);
}
}

View file

@ -1,13 +1,10 @@
import { execSync } from 'child_process';
import express from 'express';
import path from 'path';
import { Db } from '../../../src';
import { Reset } from '../../../commands/user-management/reset';
import * as utils from '../shared/utils';
import type { Role } from '../../../src/databases/entities/Role';
import * as testDb from '../shared/testDb';
import { randomEmail, randomName, randomValidPassword } from '../shared/random';
import type { Role } from '../../../src/databases/entities/Role';
let app: express.Application;
let testDbName = '';
@ -21,6 +18,10 @@ beforeAll(async () => {
globalOwnerRole = await testDb.getGlobalOwnerRole();
});
beforeEach(async () => {
await testDb.truncate(['User'], testDbName);
});
afterAll(async () => {
await testDb.terminate(testDbName);
});
@ -28,17 +29,19 @@ afterAll(async () => {
test('user-management:reset should reset DB to default user state', async () => {
await testDb.createUser({ globalRole: globalOwnerRole });
const command = [path.join('bin', 'n8n'), 'user-management:reset'].join(' ');
await Reset.run();
execSync(command);
const user = await Db.collections.User.findOne({ globalRole: globalOwnerRole });
const user = await Db.collections.User.findOne();
if (!user) {
fail('No owner found after DB reset to default user state');
}
expect(user?.email).toBeNull();
expect(user?.firstName).toBeNull();
expect(user?.lastName).toBeNull();
expect(user?.password).toBeNull();
expect(user?.resetPasswordToken).toBeNull();
expect(user?.resetPasswordTokenExpiration).toBeNull();
expect(user?.personalizationAnswers).toBeNull();
expect(user.email).toBeNull();
expect(user.firstName).toBeNull();
expect(user.lastName).toBeNull();
expect(user.password).toBeNull();
expect(user.resetPasswordToken).toBeNull();
expect(user.resetPasswordTokenExpiration).toBeNull();
expect(user.personalizationAnswers).toBeNull();
});