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

Fix randomly failing UM tests #3061

Merged
merged 10 commits into from
Apr 1, 2022
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
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();
ivov marked this conversation as resolved.
Show resolved Hide resolved

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.
ivov marked this conversation as resolved.
Show resolved Hide resolved
*/
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