Skip to content

Commit

Permalink
test: Fix randomly failing UM tests (#3061)
Browse files Browse the repository at this point in the history
* ⚡ Declutter test logs

* 🐛 Fix random passwords length

* 🐛 Fix password hashing in test user creation

* 🐛 Hash leftover password

* ⚡ Improve error message for `compare`

* ⚡ Restore `randomInvalidPassword` contant

* ⚡ Mock Telemetry module to prevent `--forceExit`

* ⚡ Silence logger

* ⚡ Simplify condition

* ⚡ Unhash password in payload
  • Loading branch information
ivov committed Apr 1, 2022
1 parent 0a75539 commit 7625421
Show file tree
Hide file tree
Showing 16 changed files with 63 additions and 30 deletions.
2 changes: 1 addition & 1 deletion packages/cli/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ const config = convict({
logs: {
level: {
doc: 'Log output level',
format: ['error', 'warn', 'info', 'verbose', 'debug'],
format: ['error', 'warn', 'info', 'verbose', 'debug', 'silent'],
default: 'info',
env: 'N8N_LOG_LEVEL',
},
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"start:default": "cd bin && ./n8n",
"start:windows": "cd bin && n8n",
"test": "npm run test:sqlite",
"test:sqlite": "export DB_TYPE=sqlite && jest --forceExit",
"test:sqlite": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=sqlite; jest",
"test:postgres": "export DB_TYPE=postgresdb && jest",
"test:mysql": "export DB_TYPE=mysqldb && jest",
"watch": "tsc --watch",
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ class Logger implements ILogger {
private logger: winston.Logger;

constructor() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const level = config.get('logs.level');
const level = config.get('logs.level') as string;

// eslint-disable-next-line @typescript-eslint/no-shadow
const output = (config.get('logs.output') as string).split(',').map((output) => output.trim());

this.logger = winston.createLogger({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
level,
silent: level === 'silent',
});

if (output.includes('console')) {
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/ResponseHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,13 @@ export function sendSuccessResponse(
}
}

export function sendErrorResponse(res: Response, error: ResponseError, shouldLog = true) {
export function sendErrorResponse(res: Response, error: ResponseError) {
let httpStatusCode = 500;
if (error.httpStatusCode) {
httpStatusCode = error.httpStatusCode;
}

shouldLog = !process.argv[1].split('/').includes('jest');

if (process.env.NODE_ENV !== 'production' && shouldLog) {
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
console.error('ERROR RESPONSE');
console.error(error);
}
Expand Down
21 changes: 20 additions & 1 deletion packages/cli/src/UserManagement/UserManagementHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import { Workflow } from 'n8n-workflow';
import { In, IsNull, Not } from 'typeorm';
import express = require('express');
import { compare } from 'bcryptjs';

import { PublicUser } from './Interfaces';
import { Db, GenericHelpers, ResponseHelper } from '..';
import { Db, ResponseHelper } from '..';
import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, User } from '../databases/entities/User';
import { Role } from '../databases/entities/Role';
import { AuthenticatedRequest } from '../requests';
Expand Down Expand Up @@ -216,3 +218,20 @@ export function isPostUsersId(req: express.Request, restEndpoint: string): boole
export function isAuthenticatedRequest(request: express.Request): request is AuthenticatedRequest {
return request.user !== undefined;
}

// ----------------------------------
// hashing
// ----------------------------------

export async function compareHash(str: string, hash: string): Promise<boolean | undefined> {
try {
return await compare(str, hash);
} catch (error) {
if (error instanceof Error && error.message.includes('Invalid salt version')) {
error.message +=
'. Comparison against unhashed string. Please check that the value compared against has been hashed.';
}

throw new Error(error);
}
}
6 changes: 3 additions & 3 deletions packages/cli/src/UserManagement/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Request, Response } from 'express';
import { compare } from 'bcryptjs';
import { IDataObject } from 'n8n-workflow';
import { Db, ResponseHelper } from '../..';
import { AUTH_COOKIE_NAME } from '../../constants';
import { issueCookie, resolveJwt } from '../auth/jwt';
import { N8nApp, PublicUser } from '../Interfaces';
import { isInstanceOwnerSetup, sanitizeUser } from '../UserManagementHelper';
import { compareHash, isInstanceOwnerSetup, sanitizeUser } from '../UserManagementHelper';
import { User } from '../../databases/entities/User';
import type { LoginRequest } from '../../requests';

Expand Down Expand Up @@ -43,7 +42,8 @@ export function authenticationMethods(this: N8nApp): void {
} catch (error) {
throw new Error('Unable to access database.');
}
if (!user || !user.password || !(await compare(req.body.password, user.password))) {

if (!user || !user.password || !(await compareHash(req.body.password, user.password))) {
// password is empty until user signs up
const error = new Error('Wrong username or password. Do you have caps lock on?');
// @ts-ignore
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/integration/auth.endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { randomEmail, randomValidPassword, randomName } from './shared/random';
import { getGlobalOwnerRole } from './shared/testDb';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let globalOwnerRole: Role;

let app: express.Application;
Expand All @@ -35,7 +37,7 @@ beforeEach(async () => {
email: TEST_USER.email,
firstName: TEST_USER.firstName,
lastName: TEST_USER.lastName,
password: hashSync(TEST_USER.password, genSaltSync(10)),
password: TEST_USER.password,
globalRole: globalOwnerRole,
});

Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/integration/auth.middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import express = require('express');

import * as request from 'supertest';
import {
REST_PATH_SEGMENT,
ROUTES_REQUIRING_AUTHORIZATION,
ROUTES_REQUIRING_AUTHENTICATION,
} from './shared/constants';

import * as utils from './shared/utils';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/integration/credentials.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Role } from '../../src/databases/entities/Role';
import { User } from '../../src/databases/entities/User';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';
let saveCredential: SaveCredentialFunction;
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/integration/me.endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Role } from '../../src/databases/entities/Role';
import { randomValidPassword, randomEmail, randomName, randomString } from './shared/random';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';
let globalOwnerRole: Role;
Expand Down Expand Up @@ -275,7 +277,7 @@ describe('Member', () => {
test('PATCH /me/password should succeed with valid inputs', async () => {
const memberPassword = randomValidPassword();
const member = await testDb.createUser({
password: hashSync(memberPassword, genSaltSync(10)),
password: memberPassword,
});
const authMemberAgent = utils.createAgent(app, { auth: true, user: member });

Expand Down
6 changes: 4 additions & 2 deletions packages/cli/test/integration/owner.endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
randomInvalidPassword,
} from './shared/random';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';

Expand Down Expand Up @@ -82,7 +84,7 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async ()

test('POST /owner should fail with invalid inputs', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) {
const response = await authOwnerAgent.post('/owner').send(invalidPayload);
Expand All @@ -92,7 +94,7 @@ test('POST /owner should fail with invalid inputs', async () => {

test('POST /owner/skip-setup should persist skipping setup to the DB', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

const response = await authOwnerAgent.post('/owner/skip-setup').send();

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/integration/passwordReset.endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
import { Role } from '../../src/databases/entities/Role';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let globalOwnerRole: Role;
let testDbName = '';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/integration/shared/random.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const randomDigit = () => Math.floor(Math.random() * 10);
const randomUppercaseLetter = () => chooseRandomly('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''));

export const randomValidPassword = () =>
randomString(MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH) + randomUppercaseLetter() + randomDigit();
randomString(MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH - 2) + randomUppercaseLetter() + randomDigit();

export const randomInvalidPassword = () =>
chooseRandomly([
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/test/integration/shared/testDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { sqliteMigrations } from '../../../src/databases/sqlite/migrations';
import type { Role } from '../../../src/databases/entities/Role';
import type { User } from '../../../src/databases/entities/User';
import type { CredentialPayload } from './types';
import { genSaltSync, hashSync } from 'bcryptjs';

/**
* Initialize one test DB per suite run, with bootstrap connection if needed.
Expand Down Expand Up @@ -188,7 +189,7 @@ export async function createUser(attributes: Partial<User> = {}): Promise<User>
const { email, password, firstName, lastName, globalRole, ...rest } = attributes;
const user = {
email: email ?? randomEmail(),
password: password ?? randomValidPassword(),
password: hashSync(password ?? randomValidPassword(), genSaltSync(10)),
firstName: firstName ?? randomName(),
lastName: lastName ?? randomName(),
globalRole: globalRole ?? (await getGlobalMemberRole()),
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/test/integration/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,13 @@ export function initTestServer({
return testServer.app;
}

/**
* Pre-requisite: Mock the telemetry module before calling.
*/
export function initTestTelemetry() {
const mockNodeTypes = { nodeTypes: {} } as INodeTypes;

void InternalHooksManager.init('test-instance-id', 'test-version', mockNodeTypes);

jest.spyOn(Telemetry.prototype, 'track').mockResolvedValue();
}

/**
Expand All @@ -117,10 +118,9 @@ const classifyEndpointGroups = (endpointGroups: string[]) => {
// ----------------------------------

/**
* Initialize a silent logger for test runs.
* Initialize a logger for test runs.
*/
export function initTestLogger() {
config.set('logs.output', 'file'); // declutter console output
LoggerProxy.init(getLogger());
}

Expand Down
16 changes: 9 additions & 7 deletions packages/cli/test/integration/users.endpoints.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express = require('express');
import validator from 'validator';
import { v4 as uuid } from 'uuid';
import { compare } from 'bcryptjs';
import { compare, genSaltSync, hashSync } from 'bcryptjs';

import { Db } from '../../src';
import config = require('../../config');
Expand All @@ -18,6 +18,8 @@ import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity';
import * as utils from './shared/utils';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';
let globalOwnerRole: Role;
Expand Down Expand Up @@ -404,7 +406,7 @@ test('POST /users/:id should fail with invalid inputs', async () => {
}
});

test.skip('POST /users/:id should fail with already accepted invite', async () => {
test('POST /users/:id should fail with already accepted invite', async () => {
const authlessAgent = utils.createAgent(app);

const globalMemberRole = await Db.collections.Role!.findOneOrFail({
Expand All @@ -414,7 +416,7 @@ test.skip('POST /users/:id should fail with already accepted invite', async () =

const shell = await Db.collections.User!.save({
email: randomEmail(),
password: randomValidPassword(), // simulate accepted invite
password: hashSync(randomValidPassword(), genSaltSync(10)), // simulate accepted invite
globalRole: globalMemberRole,
});

Expand All @@ -424,7 +426,7 @@ test.skip('POST /users/:id should fail with already accepted invite', async () =
inviterId: INITIAL_TEST_USER.id,
firstName: randomName(),
lastName: randomName(),
password: newPassword,
password: randomValidPassword(),
});

expect(response.statusCode).toBe(400);
Expand Down Expand Up @@ -458,7 +460,7 @@ test('POST /users should fail if user management is disabled', async () => {
expect(response.statusCode).toBe(500);
});

test.skip('POST /users should email invites and create user shells', async () => {
test('POST /users should email invites and create user shells', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

Expand Down Expand Up @@ -502,7 +504,7 @@ test.skip('POST /users should email invites and create user shells', async () =>
}
});

test.skip('POST /users should fail with invalid inputs', async () => {
test('POST /users should fail with invalid inputs', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

Expand All @@ -525,7 +527,7 @@ test.skip('POST /users should fail with invalid inputs', async () => {
}
});

test.skip('POST /users should ignore an empty payload', async () => {
test('POST /users should ignore an empty payload', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

Expand Down

0 comments on commit 7625421

Please sign in to comment.