🚨 Optimize UM tests (#3066)

*  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`

* 🔥 Remove unused imports

* 🔥 Remove unused import

*  Add util for configuring test SMTP

*  Isolate user creation

* 🔥 De-duplicate `createFullUser`

*  Centralize hashing

* 🔥 Remove superfluous arg

* 🔥 Remove outdated comment

*  Prioritize shared tables during trucation

* 🧪 Add login tests

*  Use token helper

* ✏️ Improve naming

*  Make `createMemberShell` consistent

* 🔥 Remove unneeded helper

* 🔥 De-duplicate `beforeEach`

* ✏️ Improve naming

* 🚚 Move `categorize` to utils

* ✏️ Update comment

* 🧪 Simplify test

* 📘 Improve `User.password` type

*  Silence logger

*  Simplify condition

*  Unhash password in payload

* 🐛 Fix comparison against unhashed password

*  Increase timeout for fake SMTP service

* 🔥 Remove unneeded import

*  Use `isNull()`

* 🧪 Use `Promise.all()` in creds tests

* 🧪 Use `Promise.all()` in me tests

* 🧪 Use `Promise.all()` in owner tests

* 🧪 Use `Promise.all()` in password tests

* 🧪 Use `Promise.all()` in users tests

*  Re-set cookie if UM disabled

* 🔥 Remove repeated line

*  Refactor out shared owner data

* 🔥 Remove unneeded import

* 🔥 Remove repeated lines

*  Organize imports

*  Reuse helper

* 🚚 Rename tests to match routers

* 🚚 Rename `createFullUser()` to `createUser()`

*  Consolidate user shell creation

*  Make hashing async

*  Add email to user shell

*  Optimize array building

* 🛠 refactor user shell factory

* 🐛 Fix MySQL tests

*  Silence logger in other DBs

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
This commit is contained in:
Iván Ovejero 2022-04-08 18:37:07 +02:00 committed by GitHub
parent e78bf15ba9
commit 1e2d6daaa3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 831 additions and 674 deletions

View file

@ -31,8 +31,8 @@
"start:windows": "cd bin && n8n", "start:windows": "cd bin && n8n",
"test": "npm run test:sqlite", "test": "npm run test:sqlite",
"test:sqlite": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=sqlite; jest", "test:sqlite": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=sqlite; jest",
"test:postgres": "export DB_TYPE=postgresdb && jest", "test:postgres": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=postgresdb && jest",
"test:mysql": "export DB_TYPE=mysqldb && jest", "test:mysql": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=mysqldb && jest",
"watch": "tsc --watch", "watch": "tsc --watch",
"typeorm": "ts-node ../../node_modules/typeorm/cli.js" "typeorm": "ts-node ../../node_modules/typeorm/cli.js"
}, },

View file

@ -4,7 +4,7 @@
import { Workflow } from 'n8n-workflow'; import { Workflow } from 'n8n-workflow';
import { In, IsNull, Not } from 'typeorm'; import { In, IsNull, Not } from 'typeorm';
import express = require('express'); import express = require('express');
import { compare } from 'bcryptjs'; import { compare, genSaltSync, hash } from 'bcryptjs';
import { PublicUser } from './Interfaces'; import { PublicUser } from './Interfaces';
import { Db, ResponseHelper } from '..'; import { Db, ResponseHelper } from '..';
@ -63,11 +63,6 @@ export function getInstanceBaseUrl(): string {
return n8nBaseUrl.endsWith('/') ? n8nBaseUrl.slice(0, n8nBaseUrl.length - 1) : n8nBaseUrl; return n8nBaseUrl.endsWith('/') ? n8nBaseUrl.slice(0, n8nBaseUrl.length - 1) : n8nBaseUrl;
} }
export async function isInstanceOwnerSetup(): Promise<boolean> {
const users = await Db.collections.User!.find({ email: Not(IsNull()) });
return users.length !== 0;
}
// TODO: Enforce at model level // TODO: Enforce at model level
export function validatePassword(password?: string): string { export function validatePassword(password?: string): string {
if (!password) { if (!password) {
@ -223,9 +218,12 @@ export function isAuthenticatedRequest(request: express.Request): request is Aut
// hashing // hashing
// ---------------------------------- // ----------------------------------
export async function compareHash(str: string, hash: string): Promise<boolean | undefined> { export const hashPassword = async (validPassword: string): Promise<string> =>
hash(validPassword, genSaltSync(10));
export async function compareHash(plaintext: string, hashed: string): Promise<boolean | undefined> {
try { try {
return await compare(str, hash); return await compare(plaintext, hashed);
} catch (error) { } catch (error) {
if (error instanceof Error && error.message.includes('Invalid salt version')) { if (error instanceof Error && error.message.includes('Invalid salt version')) {
error.message += error.message +=

View file

@ -8,9 +8,10 @@ import { Db, ResponseHelper } from '../..';
import { AUTH_COOKIE_NAME } from '../../constants'; import { AUTH_COOKIE_NAME } from '../../constants';
import { issueCookie, resolveJwt } from '../auth/jwt'; import { issueCookie, resolveJwt } from '../auth/jwt';
import { N8nApp, PublicUser } from '../Interfaces'; import { N8nApp, PublicUser } from '../Interfaces';
import { compareHash, isInstanceOwnerSetup, sanitizeUser } from '../UserManagementHelper'; import { compareHash, sanitizeUser } from '../UserManagementHelper';
import { User } from '../../databases/entities/User'; import { User } from '../../databases/entities/User';
import type { LoginRequest } from '../../requests'; import type { LoginRequest } from '../../requests';
import config = require('../../../config');
export function authenticationMethods(this: N8nApp): void { export function authenticationMethods(this: N8nApp): void {
/** /**
@ -71,13 +72,18 @@ export function authenticationMethods(this: N8nApp): void {
// If logged in, return user // If logged in, return user
try { try {
user = await resolveJwt(cookieContents); user = await resolveJwt(cookieContents);
if (!config.get('userManagement.isInstanceOwnerSetUp')) {
res.cookie(AUTH_COOKIE_NAME, cookieContents);
}
return sanitizeUser(user); return sanitizeUser(user);
} catch (error) { } catch (error) {
res.clearCookie(AUTH_COOKIE_NAME); res.clearCookie(AUTH_COOKIE_NAME);
} }
} }
if (await isInstanceOwnerSetup()) { if (config.get('userManagement.isInstanceOwnerSetUp')) {
const error = new Error('Not logged in'); const error = new Error('Not logged in');
// @ts-ignore // @ts-ignore
error.httpStatusCode = 401; error.httpStatusCode = 401;

View file

@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable import/no-cycle */ /* eslint-disable import/no-cycle */
import { compare, genSaltSync, hashSync } from 'bcryptjs';
import express = require('express'); import express = require('express');
import validator from 'validator'; import validator from 'validator';
import { LoggerProxy as Logger } from 'n8n-workflow'; import { LoggerProxy as Logger } from 'n8n-workflow';
@ -9,7 +8,7 @@ import { LoggerProxy as Logger } from 'n8n-workflow';
import { Db, InternalHooksManager, ResponseHelper } from '../..'; import { Db, InternalHooksManager, ResponseHelper } from '../..';
import { issueCookie } from '../auth/jwt'; import { issueCookie } from '../auth/jwt';
import { N8nApp, PublicUser } from '../Interfaces'; import { N8nApp, PublicUser } from '../Interfaces';
import { validatePassword, sanitizeUser } from '../UserManagementHelper'; import { validatePassword, sanitizeUser, compareHash, hashPassword } from '../UserManagementHelper';
import type { AuthenticatedRequest, MeRequest } from '../../requests'; import type { AuthenticatedRequest, MeRequest } from '../../requests';
import { validateEntity } from '../../GenericHelpers'; import { validateEntity } from '../../GenericHelpers';
import { User } from '../../databases/entities/User'; import { User } from '../../databases/entities/User';
@ -87,7 +86,7 @@ export function meNamespace(this: N8nApp): void {
throw new ResponseHelper.ResponseError('Requesting user not set up.'); throw new ResponseHelper.ResponseError('Requesting user not set up.');
} }
const isCurrentPwCorrect = await compare(currentPassword, req.user.password); const isCurrentPwCorrect = await compareHash(currentPassword, req.user.password);
if (!isCurrentPwCorrect) { if (!isCurrentPwCorrect) {
throw new ResponseHelper.ResponseError( throw new ResponseHelper.ResponseError(
'Provided current password is incorrect.', 'Provided current password is incorrect.',
@ -98,7 +97,7 @@ export function meNamespace(this: N8nApp): void {
const validPassword = validatePassword(newPassword); const validPassword = validatePassword(newPassword);
req.user.password = hashSync(validPassword, genSaltSync(10)); req.user.password = await hashPassword(validPassword);
const user = await Db.collections.User!.save(req.user); const user = await Db.collections.User!.save(req.user);
Logger.info('Password updated successfully', { userId: user.id }); Logger.info('Password updated successfully', { userId: user.id });

View file

@ -1,6 +1,5 @@
/* eslint-disable import/no-cycle */ /* eslint-disable import/no-cycle */
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import { hashSync, genSaltSync } from 'bcryptjs';
import * as express from 'express'; import * as express from 'express';
import validator from 'validator'; import validator from 'validator';
import { LoggerProxy as Logger } from 'n8n-workflow'; import { LoggerProxy as Logger } from 'n8n-workflow';
@ -11,7 +10,7 @@ import { validateEntity } from '../../GenericHelpers';
import { AuthenticatedRequest, OwnerRequest } from '../../requests'; import { AuthenticatedRequest, OwnerRequest } from '../../requests';
import { issueCookie } from '../auth/jwt'; import { issueCookie } from '../auth/jwt';
import { N8nApp } from '../Interfaces'; import { N8nApp } from '../Interfaces';
import { sanitizeUser, validatePassword } from '../UserManagementHelper'; import { hashPassword, sanitizeUser, validatePassword } from '../UserManagementHelper';
export function ownerNamespace(this: N8nApp): void { export function ownerNamespace(this: N8nApp): void {
/** /**
@ -74,7 +73,7 @@ export function ownerNamespace(this: N8nApp): void {
email, email,
firstName, firstName,
lastName, lastName,
password: hashSync(validPassword, genSaltSync(10)), password: await hashPassword(validPassword),
}); });
await validateEntity(owner); await validateEntity(owner);

View file

@ -4,14 +4,13 @@
import express = require('express'); import express = require('express');
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { URL } from 'url'; import { URL } from 'url';
import { genSaltSync, hashSync } from 'bcryptjs';
import validator from 'validator'; import validator from 'validator';
import { IsNull, MoreThanOrEqual, Not } from 'typeorm'; import { IsNull, MoreThanOrEqual, Not } from 'typeorm';
import { LoggerProxy as Logger } from 'n8n-workflow'; import { LoggerProxy as Logger } from 'n8n-workflow';
import { Db, InternalHooksManager, ResponseHelper } from '../..'; import { Db, InternalHooksManager, ResponseHelper } from '../..';
import { N8nApp } from '../Interfaces'; import { N8nApp } from '../Interfaces';
import { getInstanceBaseUrl, validatePassword } from '../UserManagementHelper'; import { getInstanceBaseUrl, hashPassword, validatePassword } from '../UserManagementHelper';
import * as UserManagementMailer from '../email'; import * as UserManagementMailer from '../email';
import type { PasswordResetRequest } from '../../requests'; import type { PasswordResetRequest } from '../../requests';
import { issueCookie } from '../auth/jwt'; import { issueCookie } from '../auth/jwt';
@ -206,7 +205,7 @@ export function passwordResetNamespace(this: N8nApp): void {
} }
await Db.collections.User!.update(userId, { await Db.collections.User!.update(userId, {
password: hashSync(validPassword, genSaltSync(10)), password: await hashPassword(validPassword),
resetPasswordToken: null, resetPasswordToken: null,
resetPasswordTokenExpiration: null, resetPasswordTokenExpiration: null,
}); });

View file

@ -3,7 +3,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Response } from 'express'; import { Response } from 'express';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { genSaltSync, hashSync } from 'bcryptjs';
import validator from 'validator'; import validator from 'validator';
import { LoggerProxy as Logger } from 'n8n-workflow'; import { LoggerProxy as Logger } from 'n8n-workflow';
@ -12,6 +11,7 @@ import { N8nApp, PublicUser } from '../Interfaces';
import { UserRequest } from '../../requests'; import { UserRequest } from '../../requests';
import { import {
getInstanceBaseUrl, getInstanceBaseUrl,
hashPassword,
isEmailSetUp, isEmailSetUp,
sanitizeUser, sanitizeUser,
validatePassword, validatePassword,
@ -349,7 +349,7 @@ export function usersNamespace(this: N8nApp): void {
invitee.firstName = firstName; invitee.firstName = firstName;
invitee.lastName = lastName; invitee.lastName = lastName;
invitee.password = hashSync(validPassword, genSaltSync(10)); invitee.password = await hashPassword(validPassword);
const updatedUser = await Db.collections.User!.save(invitee); const updatedUser = await Db.collections.User!.save(invitee);

View file

@ -62,7 +62,7 @@ export class User {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column({ length: 254 }) @Column({ length: 254, nullable: true })
@Index({ unique: true }) @Index({ unique: true })
@IsEmail() @IsEmail()
email: string; email: string;
@ -81,7 +81,7 @@ export class User {
@Column({ nullable: true }) @Column({ nullable: true })
@IsString({ message: 'Password must be of type string.' }) @IsString({ message: 'Password must be of type string.' })
password?: string; password: string;
@Column({ type: String, nullable: true }) @Column({ type: String, nullable: true })
resetPasswordToken?: string | null; resetPasswordToken?: string | null;

View file

@ -0,0 +1,283 @@
import express = require('express');
import validator from 'validator';
import config = require('../../config');
import * as utils from './shared/utils';
import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants';
import { Db } from '../../src';
import type { Role } from '../../src/databases/entities/Role';
import { randomValidPassword } from './shared/random';
import * as testDb from './shared/testDb';
import { AUTH_COOKIE_NAME } from '../../src/constants';
jest.mock('../../src/telemetry');
let app: express.Application;
let testDbName = '';
let globalOwnerRole: Role;
let globalMemberRole: Role;
beforeAll(async () => {
app = utils.initTestServer({ endpointGroups: ['auth'], applyAuth: true });
const initResult = await testDb.init();
testDbName = initResult.testDbName;
globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole();
utils.initTestLogger();
utils.initTestTelemetry();
});
beforeEach(async () => {
await testDb.truncate(['User'], testDbName);
config.set('userManagement.isInstanceOwnerSetUp', true);
await Db.collections.Settings!.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(true) },
);
});
afterAll(async () => {
await testDb.terminate(testDbName);
});
test('POST /login should log user in', async () => {
const ownerPassword = randomValidPassword();
const owner = await testDb.createUser({
password: ownerPassword,
globalRole: globalOwnerRole,
});
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.post('/login').send({
email: owner.email,
password: ownerPassword,
});
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(owner.email);
expect(firstName).toBe(owner.firstName);
expect(lastName).toBe(owner.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
});
test('GET /login should return 401 Unauthorized if no cookie', async () => {
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.get('/login');
expect(response.statusCode).toBe(401);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return cookie if UM is disabled', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
config.set('userManagement.isInstanceOwnerSetUp', false);
await Db.collections.Settings!.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(false) },
);
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authOwnerShellAgent.get('/login');
expect(response.statusCode).toBe(200);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
});
test('GET /login should return 401 Unauthorized if invalid cookie', async () => {
const invalidAuthAgent = utils.createAgent(app);
invalidAuthAgent.jar.setCookie(`${AUTH_COOKIE_NAME}=invalid`);
const response = await invalidAuthAgent.get('/login');
expect(response.statusCode).toBe(401);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in owner shell', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authMemberAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authMemberAgent.get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBeDefined();
expect(firstName).toBeNull();
expect(lastName).toBeNull();
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in member shell', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
const authMemberAgent = utils.createAgent(app, { auth: true, user: memberShell });
const response = await authMemberAgent.get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBeDefined();
expect(firstName).toBeNull();
expect(lastName).toBeNull();
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in owner', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const response = await authOwnerAgent.get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(owner.email);
expect(firstName).toBe(owner.firstName);
expect(lastName).toBe(owner.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const response = await authMemberAgent.get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(member.email);
expect(firstName).toBe(member.firstName);
expect(lastName).toBe(member.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('POST /logout should log user out', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const response = await authOwnerAgent.post('/logout');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});

View file

@ -1,149 +0,0 @@
import { hashSync, genSaltSync } from 'bcryptjs';
import express = require('express');
import validator from 'validator';
import { v4 as uuid } from 'uuid';
import config = require('../../config');
import * as utils from './shared/utils';
import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants';
import { Db } from '../../src';
import { Role } from '../../src/databases/entities/Role';
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;
let testDbName = '';
beforeAll(async () => {
app = utils.initTestServer({ endpointGroups: ['auth'], applyAuth: true });
const initResult = await testDb.init();
testDbName = initResult.testDbName;
await testDb.truncate(['User'], testDbName);
globalOwnerRole = await getGlobalOwnerRole();
utils.initTestLogger();
utils.initTestTelemetry();
});
beforeEach(async () => {
await testDb.createUser({
id: uuid(),
email: TEST_USER.email,
firstName: TEST_USER.firstName,
lastName: TEST_USER.lastName,
password: TEST_USER.password,
globalRole: globalOwnerRole,
});
config.set('userManagement.isInstanceOwnerSetUp', true);
await Db.collections.Settings!.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(true) },
);
});
afterEach(async () => {
await testDb.truncate(['User'], testDbName);
});
afterAll(async () => {
await testDb.terminate(testDbName);
});
test('POST /login should log user in', async () => {
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.post('/login').send({
email: TEST_USER.email,
password: TEST_USER.password,
});
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(TEST_USER.email);
expect(firstName).toBe(TEST_USER.firstName);
expect(lastName).toBe(TEST_USER.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
});
test('GET /login should receive logged in user', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const response = await authOwnerAgent.get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(TEST_USER.email);
expect(firstName).toBe(TEST_USER.firstName);
expect(lastName).toBe(TEST_USER.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(response.headers['set-cookie']).toBeUndefined();
});
test('POST /logout should log user out', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const response = await authOwnerAgent.post('/logout');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
const TEST_USER = {
email: randomEmail(),
password: randomValidPassword(),
firstName: randomName(),
lastName: randomName(),
};

View file

@ -8,11 +8,13 @@ import {
} from './shared/constants'; } from './shared/constants';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { Role } from '../../src/databases/entities/Role';
jest.mock('../../src/telemetry'); jest.mock('../../src/telemetry');
let app: express.Application; let app: express.Application;
let testDbName = ''; let testDbName = '';
let globalMemberRole: Role;
beforeAll(async () => { beforeAll(async () => {
app = utils.initTestServer({ app = utils.initTestServer({
@ -21,6 +23,9 @@ beforeAll(async () => {
}); });
const initResult = await testDb.init(); const initResult = await testDb.init();
testDbName = initResult.testDbName; testDbName = initResult.testDbName;
globalMemberRole = await testDb.getGlobalMemberRole();
utils.initTestLogger(); utils.initTestLogger();
utils.initTestTelemetry(); utils.initTestTelemetry();
}); });
@ -43,12 +48,9 @@ ROUTES_REQUIRING_AUTHORIZATION.forEach(async (route) => {
const [method, endpoint] = getMethodAndEndpoint(route); const [method, endpoint] = getMethodAndEndpoint(route);
test(`${route} should return 403 Forbidden for member`, async () => { test(`${route} should return 403 Forbidden for member`, async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const response = await authMemberAgent[method](endpoint); const response = await authMemberAgent[method](endpoint);
if (response.statusCode === 500) {
console.log(response);
}
expect(response.statusCode).toBe(403); expect(response.statusCode).toBe(403);
}); });

View file

@ -4,14 +4,17 @@ import { Db } from '../../src';
import { randomName, randomString } from './shared/random'; import { randomName, randomString } from './shared/random';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import type { CredentialPayload, SaveCredentialFunction } from './shared/types'; import type { CredentialPayload, SaveCredentialFunction } from './shared/types';
import { Role } from '../../src/databases/entities/Role'; import type { Role } from '../../src/databases/entities/Role';
import { User } from '../../src/databases/entities/User'; import type { User } from '../../src/databases/entities/User';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity';
jest.mock('../../src/telemetry'); jest.mock('../../src/telemetry');
let app: express.Application; let app: express.Application;
let testDbName = ''; let testDbName = '';
let globalOwnerRole: Role;
let globalMemberRole: Role;
let saveCredential: SaveCredentialFunction; let saveCredential: SaveCredentialFunction;
beforeAll(async () => { beforeAll(async () => {
@ -24,19 +27,17 @@ beforeAll(async () => {
utils.initConfigFile(); utils.initConfigFile();
globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole();
const credentialOwnerRole = await testDb.getCredentialOwnerRole(); const credentialOwnerRole = await testDb.getCredentialOwnerRole();
saveCredential = affixRoleToSaveCredential(credentialOwnerRole); saveCredential = affixRoleToSaveCredential(credentialOwnerRole);
utils.initTestLogger();
utils.initTestTelemetry(); utils.initTestTelemetry();
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.createOwnerShell(); await testDb.truncate(['User', 'SharedCredentials', 'Credentials'], testDbName);
});
afterEach(async () => {
// do not combine calls - shared table must be cleared first and separately
await testDb.truncate(['SharedCredentials'], testDbName);
await testDb.truncate(['User', 'Credentials'], testDbName);
}); });
afterAll(async () => { afterAll(async () => {
@ -44,8 +45,9 @@ afterAll(async () => {
}); });
test('POST /credentials should create cred', async () => { test('POST /credentials should create cred', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const payload = credentialPayload(); const payload = credentialPayload();
const response = await authOwnerAgent.post('/credentials').send(payload); const response = await authOwnerAgent.post('/credentials').send(payload);
@ -71,26 +73,28 @@ test('POST /credentials should create cred', async () => {
where: { credentials: credential }, where: { credentials: credential },
}); });
expect(sharedCredential.user.id).toBe(owner.id); expect(sharedCredential.user.id).toBe(ownerShell.id);
expect(sharedCredential.credentials.name).toBe(payload.name); expect(sharedCredential.credentials.name).toBe(payload.name);
}); });
test('POST /credentials should fail with invalid inputs', async () => { test('POST /credentials should fail with invalid inputs', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
for (const invalidPayload of INVALID_PAYLOADS) { await Promise.all(
INVALID_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/credentials').send(invalidPayload); const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
} }),
);
}); });
test('POST /credentials should fail with missing encryption key', async () => { test('POST /credentials should fail with missing encryption key', async () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockResolvedValue(undefined); mock.mockResolvedValue(undefined);
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authOwnerAgent.post('/credentials').send(credentialPayload()); const response = await authOwnerAgent.post('/credentials').send(credentialPayload());
@ -100,8 +104,8 @@ test('POST /credentials should fail with missing encryption key', async () => {
}); });
test('POST /credentials should ignore ID in payload', async () => { test('POST /credentials should ignore ID in payload', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const firstResponse = await authOwnerAgent const firstResponse = await authOwnerAgent
.post('/credentials') .post('/credentials')
@ -117,9 +121,9 @@ test('POST /credentials should ignore ID in payload', async () => {
}); });
test('DELETE /credentials/:id should delete owned cred for owner', async () => { test('DELETE /credentials/:id should delete owned cred for owner', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
@ -136,9 +140,9 @@ test('DELETE /credentials/:id should delete owned cred for owner', async () => {
}); });
test('DELETE /credentials/:id should delete non-owned cred for owner', async () => { test('DELETE /credentials/:id should delete non-owned cred for owner', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(credentialPayload(), { user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member });
const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
@ -156,7 +160,7 @@ test('DELETE /credentials/:id should delete non-owned cred for owner', async ()
}); });
test('DELETE /credentials/:id should delete owned cred for member', async () => { test('DELETE /credentials/:id should delete owned cred for member', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const savedCredential = await saveCredential(credentialPayload(), { user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member });
@ -175,10 +179,10 @@ test('DELETE /credentials/:id should delete owned cred for member', async () =>
}); });
test('DELETE /credentials/:id should not delete non-owned cred for member', async () => { test('DELETE /credentials/:id should not delete non-owned cred for member', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
@ -194,8 +198,8 @@ test('DELETE /credentials/:id should not delete non-owned cred for member', asyn
}); });
test('DELETE /credentials/:id should fail if cred not found', async () => { test('DELETE /credentials/:id should fail if cred not found', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authOwnerAgent.delete('/credentials/123'); const response = await authOwnerAgent.delete('/credentials/123');
@ -203,9 +207,9 @@ test('DELETE /credentials/:id should fail if cred not found', async () => {
}); });
test('PATCH /credentials/:id should update owned cred for owner', async () => { test('PATCH /credentials/:id should update owned cred for owner', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
const patchPayload = credentialPayload(); const patchPayload = credentialPayload();
const response = await authOwnerAgent const response = await authOwnerAgent
@ -237,9 +241,9 @@ test('PATCH /credentials/:id should update owned cred for owner', async () => {
}); });
test('PATCH /credentials/:id should update non-owned cred for owner', async () => { test('PATCH /credentials/:id should update non-owned cred for owner', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(credentialPayload(), { user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member });
const patchPayload = credentialPayload(); const patchPayload = credentialPayload();
@ -272,7 +276,7 @@ test('PATCH /credentials/:id should update non-owned cred for owner', async () =
}); });
test('PATCH /credentials/:id should update owned cred for member', async () => { test('PATCH /credentials/:id should update owned cred for member', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const savedCredential = await saveCredential(credentialPayload(), { user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member });
const patchPayload = credentialPayload(); const patchPayload = credentialPayload();
@ -306,10 +310,10 @@ test('PATCH /credentials/:id should update owned cred for member', async () => {
}); });
test('PATCH /credentials/:id should not update non-owned cred for member', async () => { test('PATCH /credentials/:id should not update non-owned cred for member', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
const patchPayload = credentialPayload(); const patchPayload = credentialPayload();
const response = await authMemberAgent const response = await authMemberAgent
@ -324,22 +328,24 @@ test('PATCH /credentials/:id should not update non-owned cred for member', async
}); });
test('PATCH /credentials/:id should fail with invalid inputs', async () => { test('PATCH /credentials/:id should fail with invalid inputs', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
for (const invalidPayload of INVALID_PAYLOADS) { await Promise.all(
INVALID_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent const response = await authOwnerAgent
.patch(`/credentials/${savedCredential.id}`) .patch(`/credentials/${savedCredential.id}`)
.send(invalidPayload); .send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
} }),
);
}); });
test('PATCH /credentials/:id should fail if cred not found', async () => { test('PATCH /credentials/:id should fail if cred not found', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authOwnerAgent.patch('/credentials/123').send(credentialPayload()); const response = await authOwnerAgent.patch('/credentials/123').send(credentialPayload());
@ -350,8 +356,8 @@ test('PATCH /credentials/:id should fail with missing encryption key', async ()
const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockResolvedValue(undefined); mock.mockResolvedValue(undefined);
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authOwnerAgent.post('/credentials').send(credentialPayload()); const response = await authOwnerAgent.post('/credentials').send(credentialPayload());
@ -361,14 +367,14 @@ test('PATCH /credentials/:id should fail with missing encryption key', async ()
}); });
test('GET /credentials should retrieve all creds for owner', async () => { test('GET /credentials should retrieve all creds for owner', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
await saveCredential(credentialPayload(), { user: owner }); await saveCredential(credentialPayload(), { user: ownerShell });
} }
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
await saveCredential(credentialPayload(), { user: member }); await saveCredential(credentialPayload(), { user: member });
@ -377,18 +383,20 @@ test('GET /credentials should retrieve all creds for owner', async () => {
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(4); // 3 owner + 1 member expect(response.body.data.length).toBe(4); // 3 owner + 1 member
for (const credential of response.body.data) { await Promise.all(
response.body.data.map(async (credential: CredentialsEntity) => {
const { name, type, nodesAccess, data: encryptedData } = credential; const { name, type, nodesAccess, data: encryptedData } = credential;
expect(typeof name).toBe('string'); expect(typeof name).toBe('string');
expect(typeof type).toBe('string'); expect(typeof type).toBe('string');
expect(typeof nodesAccess[0].nodeType).toBe('string'); expect(typeof nodesAccess[0].nodeType).toBe('string');
expect(encryptedData).toBeUndefined(); expect(encryptedData).toBeUndefined();
} }),
);
}); });
test('GET /credentials should retrieve owned creds for member', async () => { test('GET /credentials should retrieve owned creds for member', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
@ -400,23 +408,25 @@ test('GET /credentials should retrieve owned creds for member', async () => {
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(3); expect(response.body.data.length).toBe(3);
for (const credential of response.body.data) { await Promise.all(
response.body.data.map(async (credential: CredentialsEntity) => {
const { name, type, nodesAccess, data: encryptedData } = credential; const { name, type, nodesAccess, data: encryptedData } = credential;
expect(typeof name).toBe('string'); expect(typeof name).toBe('string');
expect(typeof type).toBe('string'); expect(typeof type).toBe('string');
expect(typeof nodesAccess[0].nodeType).toBe('string'); expect(typeof nodesAccess[0].nodeType).toBe('string');
expect(encryptedData).toBeUndefined(); expect(encryptedData).toBeUndefined();
} }),
);
}); });
test('GET /credentials should not retrieve non-owned creds for member', async () => { test('GET /credentials should not retrieve non-owned creds for member', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
await saveCredential(credentialPayload(), { user: owner }); await saveCredential(credentialPayload(), { user: ownerShell });
} }
const response = await authMemberAgent.get('/credentials'); const response = await authMemberAgent.get('/credentials');
@ -426,9 +436,9 @@ test('GET /credentials should not retrieve non-owned creds for member', async ()
}); });
test('GET /credentials/:id should retrieve owned cred for owner', async () => { test('GET /credentials/:id should retrieve owned cred for owner', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`);
@ -451,7 +461,7 @@ test('GET /credentials/:id should retrieve owned cred for owner', async () => {
}); });
test('GET /credentials/:id should retrieve owned cred for member', async () => { test('GET /credentials/:id should retrieve owned cred for member', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const savedCredential = await saveCredential(credentialPayload(), { user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member });
@ -477,10 +487,10 @@ test('GET /credentials/:id should retrieve owned cred for member', async () => {
}); });
test('GET /credentials/:id should not retrieve non-owned cred for member', async () => { test('GET /credentials/:id should not retrieve non-owned cred for member', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
const response = await authMemberAgent.get(`/credentials/${savedCredential.id}`); const response = await authMemberAgent.get(`/credentials/${savedCredential.id}`);
@ -489,9 +499,9 @@ test('GET /credentials/:id should not retrieve non-owned cred for member', async
}); });
test('GET /credentials/:id should fail with missing encryption key', async () => { test('GET /credentials/:id should fail with missing encryption key', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell });
const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockResolvedValue(undefined); mock.mockResolvedValue(undefined);
@ -506,8 +516,8 @@ test('GET /credentials/:id should fail with missing encryption key', async () =>
}); });
test('GET /credentials/:id should return 404 if cred not found', async () => { test('GET /credentials/:id should return 404 if cred not found', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authMemberAgent = utils.createAgent(app, { auth: true, user: owner }); const authMemberAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authMemberAgent.get('/credentials/789'); const response = await authMemberAgent.get('/credentials/789');

View file

@ -1,12 +1,12 @@
import { hashSync, genSaltSync } from 'bcryptjs';
import express = require('express'); import express = require('express');
import validator from 'validator'; import validator from 'validator';
import { IsNull } from 'typeorm';
import config = require('../../config'); import config = require('../../config');
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import { Db } from '../../src'; import { Db } from '../../src';
import { Role } from '../../src/databases/entities/Role'; import type { Role } from '../../src/databases/entities/Role';
import { randomValidPassword, randomEmail, randomName, randomString } from './shared/random'; import { randomValidPassword, randomEmail, randomName, randomString } from './shared/random';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
@ -15,6 +15,7 @@ jest.mock('../../src/telemetry');
let app: express.Application; let app: express.Application;
let testDbName = ''; let testDbName = '';
let globalOwnerRole: Role; let globalOwnerRole: Role;
let globalMemberRole: Role;
beforeAll(async () => { beforeAll(async () => {
app = utils.initTestServer({ endpointGroups: ['me'], applyAuth: true }); app = utils.initTestServer({ endpointGroups: ['me'], applyAuth: true });
@ -22,6 +23,7 @@ beforeAll(async () => {
testDbName = initResult.testDbName; testDbName = initResult.testDbName;
globalOwnerRole = await testDb.getGlobalOwnerRole(); globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole();
utils.initTestLogger(); utils.initTestLogger();
utils.initTestTelemetry(); utils.initTestTelemetry();
}); });
@ -32,15 +34,11 @@ afterAll(async () => {
describe('Owner shell', () => { describe('Owner shell', () => {
beforeEach(async () => { beforeEach(async () => {
await testDb.createOwnerShell();
});
afterEach(async () => {
await testDb.truncate(['User'], testDbName); await testDb.truncate(['User'], testDbName);
}); });
test('GET /me should return sanitized owner shell', async () => { test('GET /me should return sanitized owner shell', async () => {
const ownerShell = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authOwnerShellAgent.get('/me'); const response = await authOwnerShellAgent.get('/me');
@ -72,7 +70,7 @@ describe('Owner shell', () => {
}); });
test('PATCH /me should succeed with valid inputs', async () => { test('PATCH /me should succeed with valid inputs', async () => {
const ownerShell = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
for (const validPayload of VALID_PATCH_ME_PAYLOADS) { for (const validPayload of VALID_PATCH_ME_PAYLOADS) {
@ -112,7 +110,7 @@ describe('Owner shell', () => {
}); });
test('PATCH /me should fail with invalid inputs', async () => { test('PATCH /me should fail with invalid inputs', async () => {
const ownerShell = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) {
@ -127,7 +125,7 @@ describe('Owner shell', () => {
}); });
test('PATCH /me/password should fail for shell', async () => { test('PATCH /me/password should fail for shell', async () => {
const ownerShell = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const validPasswordPayload = { const validPasswordPayload = {
@ -135,9 +133,10 @@ describe('Owner shell', () => {
newPassword: randomValidPassword(), newPassword: randomValidPassword(),
}; };
const payloads = [validPasswordPayload, ...INVALID_PASSWORD_PAYLOADS]; const validPayloads = [validPasswordPayload, ...INVALID_PASSWORD_PAYLOADS];
for (const payload of payloads) { await Promise.all(
validPayloads.map(async (payload) => {
const response = await authOwnerShellAgent.patch('/me/password').send(payload); const response = await authOwnerShellAgent.patch('/me/password').send(payload);
expect([400, 500].includes(response.statusCode)).toBe(true); expect([400, 500].includes(response.statusCode)).toBe(true);
@ -146,29 +145,34 @@ describe('Owner shell', () => {
if (payload.newPassword) { if (payload.newPassword) {
expect(storedMember.password).not.toBe(payload.newPassword); expect(storedMember.password).not.toBe(payload.newPassword);
} }
if (payload.currentPassword) { if (payload.currentPassword) {
expect(storedMember.password).not.toBe(payload.currentPassword); expect(storedMember.password).not.toBe(payload.currentPassword);
} }
} }),
);
const storedOwnerShell = await Db.collections.User!.findOneOrFail(); const storedOwnerShell = await Db.collections.User!.findOneOrFail();
expect(storedOwnerShell.password).toBeNull(); expect(storedOwnerShell.password).toBeNull();
}); });
test('POST /me/survey should succeed with valid inputs', async () => { test('POST /me/survey should succeed with valid inputs', async () => {
const ownerShell = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const validPayloads = [SURVEY, {}]; const validPayloads = [SURVEY, {}];
for (const validPayload of validPayloads) { for (const validPayload of validPayloads) {
const response = await authOwnerShellAgent.post('/me/survey').send(validPayload); const response = await authOwnerShellAgent.post('/me/survey').send(validPayload);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
const { personalizationAnswers: storedAnswers } = await Db.collections.User!.findOneOrFail(); const storedShellOwner = await Db.collections.User!.findOneOrFail({
where: { email: IsNull() },
});
expect(storedAnswers).toEqual(validPayload); expect(storedShellOwner.personalizationAnswers).toEqual(validPayload);
} }
}); });
}); });
@ -188,7 +192,7 @@ describe('Member', () => {
}); });
test('GET /me should return sanitized member', async () => { test('GET /me should return sanitized member', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const response = await authMemberAgent.get('/me'); const response = await authMemberAgent.get('/me');
@ -220,7 +224,7 @@ describe('Member', () => {
}); });
test('PATCH /me should succeed with valid inputs', async () => { test('PATCH /me should succeed with valid inputs', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
for (const validPayload of VALID_PATCH_ME_PAYLOADS) { for (const validPayload of VALID_PATCH_ME_PAYLOADS) {
@ -260,7 +264,7 @@ describe('Member', () => {
}); });
test('PATCH /me should fail with invalid inputs', async () => { test('PATCH /me should fail with invalid inputs', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) {
@ -278,6 +282,7 @@ describe('Member', () => {
const memberPassword = randomValidPassword(); const memberPassword = randomValidPassword();
const member = await testDb.createUser({ const member = await testDb.createUser({
password: memberPassword, password: memberPassword,
globalRole: globalMemberRole,
}); });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
@ -296,7 +301,7 @@ describe('Member', () => {
}); });
test('PATCH /me/password should fail with invalid inputs', async () => { test('PATCH /me/password should fail with invalid inputs', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
for (const payload of INVALID_PASSWORD_PAYLOADS) { for (const payload of INVALID_PASSWORD_PAYLOADS) {
@ -315,7 +320,7 @@ describe('Member', () => {
}); });
test('POST /me/survey should succeed with valid inputs', async () => { test('POST /me/survey should succeed with valid inputs', async () => {
const member = await testDb.createUser(); const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member });
const validPayloads = [SURVEY, {}]; const validPayloads = [SURVEY, {}];

View file

@ -11,26 +11,25 @@ import {
randomValidPassword, randomValidPassword,
randomInvalidPassword, randomInvalidPassword,
} from './shared/random'; } from './shared/random';
import type { Role } from '../../src/databases/entities/Role';
jest.mock('../../src/telemetry'); jest.mock('../../src/telemetry');
let app: express.Application; let app: express.Application;
let testDbName = ''; let testDbName = '';
let globalOwnerRole: Role;
beforeAll(async () => { beforeAll(async () => {
app = utils.initTestServer({ endpointGroups: ['owner'], applyAuth: true }); app = utils.initTestServer({ endpointGroups: ['owner'], applyAuth: true });
const initResult = await testDb.init(); const initResult = await testDb.init();
testDbName = initResult.testDbName; testDbName = initResult.testDbName;
globalOwnerRole = await testDb.getGlobalOwnerRole();
utils.initTestLogger(); utils.initTestLogger();
utils.initTestTelemetry(); utils.initTestTelemetry();
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.createOwnerShell();
});
afterEach(async () => {
await testDb.truncate(['User'], testDbName); await testDb.truncate(['User'], testDbName);
}); });
@ -39,10 +38,17 @@ afterAll(async () => {
}); });
test('POST /owner should create owner and enable isInstanceOwnerSetUp', async () => { test('POST /owner should create owner and enable isInstanceOwnerSetUp', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authOwnerAgent.post('/owner').send(TEST_USER); const newOwnerData = {
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const response = await authOwnerAgent.post('/owner').send(newOwnerData);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
@ -59,9 +65,9 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async ()
} = response.body.data; } = response.body.data;
expect(validator.isUUID(id)).toBe(true); expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(TEST_USER.email); expect(email).toBe(newOwnerData.email);
expect(firstName).toBe(TEST_USER.firstName); expect(firstName).toBe(newOwnerData.firstName);
expect(lastName).toBe(TEST_USER.lastName); expect(lastName).toBe(newOwnerData.lastName);
expect(personalizationAnswers).toBeNull(); expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined(); expect(password).toBeUndefined();
expect(isPending).toBe(false); expect(isPending).toBe(false);
@ -70,10 +76,10 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async ()
expect(globalRole.scope).toBe('global'); expect(globalRole.scope).toBe('global');
const storedOwner = await Db.collections.User!.findOneOrFail(id); const storedOwner = await Db.collections.User!.findOneOrFail(id);
expect(storedOwner.password).not.toBe(TEST_USER.password); expect(storedOwner.password).not.toBe(newOwnerData.password);
expect(storedOwner.email).toBe(TEST_USER.email); expect(storedOwner.email).toBe(newOwnerData.email);
expect(storedOwner.firstName).toBe(TEST_USER.firstName); expect(storedOwner.firstName).toBe(newOwnerData.firstName);
expect(storedOwner.lastName).toBe(TEST_USER.lastName); expect(storedOwner.lastName).toBe(newOwnerData.lastName);
const isInstanceOwnerSetUpConfig = config.get('userManagement.isInstanceOwnerSetUp'); const isInstanceOwnerSetUpConfig = config.get('userManagement.isInstanceOwnerSetUp');
expect(isInstanceOwnerSetUpConfig).toBe(true); expect(isInstanceOwnerSetUpConfig).toBe(true);
@ -83,18 +89,20 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async ()
}); });
test('POST /owner should fail with invalid inputs', async () => { test('POST /owner should fail with invalid inputs', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) { await Promise.all(
INVALID_POST_OWNER_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/owner').send(invalidPayload); const response = await authOwnerAgent.post('/owner').send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
} }),
);
}); });
test('POST /owner/skip-setup should persist skipping setup to the DB', async () => { test('POST /owner/skip-setup should persist skipping setup to the DB', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const response = await authOwnerAgent.post('/owner/skip-setup').send(); const response = await authOwnerAgent.post('/owner/skip-setup').send();
@ -109,13 +117,6 @@ test('POST /owner/skip-setup should persist skipping setup to the DB', async ()
expect(value).toBe('true'); expect(value).toBe('true');
}); });
const TEST_USER = {
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const INVALID_POST_OWNER_PAYLOADS = [ const INVALID_POST_OWNER_PAYLOADS = [
{ {
email: '', email: '',

View file

@ -11,51 +11,35 @@ import {
randomName, randomName,
randomValidPassword, randomValidPassword,
} from './shared/random'; } from './shared/random';
import { Role } from '../../src/databases/entities/Role';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { Role } from '../../src/databases/entities/Role';
jest.mock('../../src/telemetry'); jest.mock('../../src/telemetry');
let app: express.Application; let app: express.Application;
let globalOwnerRole: Role;
let testDbName = ''; let testDbName = '';
let globalOwnerRole: Role;
beforeAll(async () => { beforeAll(async () => {
app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true }); app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true });
const initResult = await testDb.init(); const initResult = await testDb.init();
testDbName = initResult.testDbName; testDbName = initResult.testDbName;
await testDb.truncate(['User'], testDbName); globalOwnerRole = await testDb.getGlobalOwnerRole();
globalOwnerRole = await Db.collections.Role!.findOneOrFail({
name: 'owner',
scope: 'global',
});
utils.initTestTelemetry(); utils.initTestTelemetry();
utils.initTestLogger(); utils.initTestLogger();
}); });
beforeEach(async () => { beforeEach(async () => {
jest.isolateModules(() => { await testDb.truncate(['User'], testDbName);
jest.mock('../../config'); jest.mock('../../config');
});
config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.isInstanceOwnerSetUp', true);
config.set('userManagement.emails.mode', ''); config.set('userManagement.emails.mode', '');
await testDb.createUser({ jest.setTimeout(30000); // fake SMTP service might be slow
id: INITIAL_TEST_USER.id,
email: INITIAL_TEST_USER.email,
password: INITIAL_TEST_USER.password,
firstName: INITIAL_TEST_USER.firstName,
lastName: INITIAL_TEST_USER.lastName,
globalRole: globalOwnerRole,
});
});
afterEach(async () => {
await testDb.truncate(['User'], testDbName);
}); });
afterAll(async () => { afterAll(async () => {
@ -63,47 +47,38 @@ afterAll(async () => {
}); });
test('POST /forgot-password should send password reset email', async () => { test('POST /forgot-password should send password reset email', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const { await utils.configureSmtp();
user,
pass,
smtp: { host, port, secure },
} = await utils.getSmtpTestAccount();
config.set('userManagement.emails.mode', 'smtp'); const response = await authlessAgent.post('/forgot-password').send({ email: owner.email });
config.set('userManagement.emails.smtp.host', host);
config.set('userManagement.emails.smtp.port', port);
config.set('userManagement.emails.smtp.secure', secure);
config.set('userManagement.emails.smtp.auth.user', user);
config.set('userManagement.emails.smtp.auth.pass', pass);
const response = await authlessAgent
.post('/forgot-password')
.send({ email: INITIAL_TEST_USER.email });
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body).toEqual({}); expect(response.body).toEqual({});
const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email });
expect(owner.resetPasswordToken).toBeDefined(); expect(storedOwner.resetPasswordToken).toBeDefined();
expect(owner.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000)); expect(storedOwner.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
}); });
test('POST /forgot-password should fail if emailing is not set up', async () => { test('POST /forgot-password should fail if emailing is not set up', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const response = await authlessAgent const response = await authlessAgent.post('/forgot-password').send({ email: owner.email });
.post('/forgot-password')
.send({ email: INITIAL_TEST_USER.email });
expect(response.statusCode).toBe(500); expect(response.statusCode).toBe(500);
const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email });
expect(owner.resetPasswordToken).toBeNull(); expect(storedOwner.resetPasswordToken).toBeNull();
}); });
test('POST /forgot-password should fail with invalid inputs', async () => { test('POST /forgot-password should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
config.set('userManagement.emails.mode', 'smtp'); config.set('userManagement.emails.mode', 'smtp');
@ -116,13 +91,15 @@ test('POST /forgot-password should fail with invalid inputs', async () => {
[{ email: randomName() }], [{ email: randomName() }],
]; ];
for (const invalidPayload of invalidPayloads) { await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
const response = await authlessAgent.post('/forgot-password').send(invalidPayload); const response = await authlessAgent.post('/forgot-password').send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email });
expect(owner.resetPasswordToken).toBeNull(); expect(storedOwner.resetPasswordToken).toBeNull();
} }),
);
}); });
test('POST /forgot-password should fail if user is not found', async () => { test('POST /forgot-password should fail if user is not found', async () => {
@ -132,38 +109,39 @@ test('POST /forgot-password should fail if user is not found', async () => {
const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() }); const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() });
// response should have 200 to not provide any information to the requester expect(response.statusCode).toBe(200); // expect 200 to remain vague
expect(response.statusCode).toBe(200);
}); });
test('GET /resolve-password-token should succeed with valid inputs', async () => { test('GET /resolve-password-token should succeed with valid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid(); const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User!.update(INITIAL_TEST_USER.id, { await Db.collections.User!.update(owner.id, {
resetPasswordToken, resetPasswordToken,
resetPasswordTokenExpiration, resetPasswordTokenExpiration,
}); });
const response = await authlessAgent const response = await authlessAgent
.get('/resolve-password-token') .get('/resolve-password-token')
.query({ userId: INITIAL_TEST_USER.id, token: resetPasswordToken }); .query({ userId: owner.id, token: resetPasswordToken });
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
}); });
test('GET /resolve-password-token should fail with invalid inputs', async () => { test('GET /resolve-password-token should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
config.set('userManagement.emails.mode', 'smtp'); config.set('userManagement.emails.mode', 'smtp');
const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() }); const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() });
const second = await authlessAgent const second = await authlessAgent.get('/resolve-password-token').query({ userId: owner.id });
.get('/resolve-password-token')
.query({ userId: INITIAL_TEST_USER.id });
for (const response of [first, second]) { for (const response of [first, second]) {
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
@ -171,24 +149,28 @@ test('GET /resolve-password-token should fail with invalid inputs', async () =>
}); });
test('GET /resolve-password-token should fail if user is not found', async () => { test('GET /resolve-password-token should fail if user is not found', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
config.set('userManagement.emails.mode', 'smtp'); config.set('userManagement.emails.mode', 'smtp');
const response = await authlessAgent const response = await authlessAgent
.get('/resolve-password-token') .get('/resolve-password-token')
.query({ userId: INITIAL_TEST_USER.id, token: uuid() }); .query({ userId: owner.id, token: uuid() });
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
}); });
test('GET /resolve-password-token should fail if token is expired', async () => { test('GET /resolve-password-token should fail if token is expired', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid(); const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1; const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
await Db.collections.User!.update(INITIAL_TEST_USER.id, { await Db.collections.User!.update(owner.id, {
resetPasswordToken, resetPasswordToken,
resetPasswordTokenExpiration, resetPasswordTokenExpiration,
}); });
@ -197,18 +179,20 @@ test('GET /resolve-password-token should fail if token is expired', async () =>
const response = await authlessAgent const response = await authlessAgent
.get('/resolve-password-token') .get('/resolve-password-token')
.query({ userId: INITIAL_TEST_USER.id, token: resetPasswordToken }); .query({ userId: owner.id, token: resetPasswordToken });
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
}); });
test('POST /change-password should succeed with valid inputs', async () => { test('POST /change-password should succeed with valid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid(); const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User!.update(INITIAL_TEST_USER.id, { await Db.collections.User!.update(owner.id, {
resetPasswordToken, resetPasswordToken,
resetPasswordTokenExpiration, resetPasswordTokenExpiration,
}); });
@ -217,7 +201,7 @@ test('POST /change-password should succeed with valid inputs', async () => {
const response = await authlessAgent.post('/change-password').send({ const response = await authlessAgent.post('/change-password').send({
token: resetPasswordToken, token: resetPasswordToken,
userId: INITIAL_TEST_USER.id, userId: owner.id,
password: passwordToStore, password: passwordToStore,
}); });
@ -226,63 +210,65 @@ test('POST /change-password should succeed with valid inputs', async () => {
const authToken = utils.getAuthToken(response); const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined(); expect(authToken).toBeDefined();
const { password: storedPassword } = await Db.collections.User!.findOneOrFail( const { password: storedPassword } = await Db.collections.User!.findOneOrFail(owner.id);
INITIAL_TEST_USER.id,
);
const comparisonResult = await compare(passwordToStore, storedPassword!); const comparisonResult = await compare(passwordToStore, storedPassword);
expect(comparisonResult).toBe(true); expect(comparisonResult).toBe(true);
expect(storedPassword).not.toBe(passwordToStore); expect(storedPassword).not.toBe(passwordToStore);
}); });
test('POST /change-password should fail with invalid inputs', async () => { test('POST /change-password should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid(); const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User!.update(INITIAL_TEST_USER.id, { await Db.collections.User!.update(owner.id, {
resetPasswordToken, resetPasswordToken,
resetPasswordTokenExpiration, resetPasswordTokenExpiration,
}); });
const invalidPayloads = [ const invalidPayloads = [
{ token: uuid() }, { token: uuid() },
{ id: INITIAL_TEST_USER.id }, { id: owner.id },
{ password: randomValidPassword() }, { password: randomValidPassword() },
{ token: uuid(), id: INITIAL_TEST_USER.id }, { token: uuid(), id: owner.id },
{ token: uuid(), password: randomValidPassword() }, { token: uuid(), password: randomValidPassword() },
{ id: INITIAL_TEST_USER.id, password: randomValidPassword() }, { id: owner.id, password: randomValidPassword() },
{ {
id: INITIAL_TEST_USER.id, id: owner.id,
password: randomInvalidPassword(), password: randomInvalidPassword(),
token: resetPasswordToken, token: resetPasswordToken,
}, },
{ {
id: INITIAL_TEST_USER.id, id: owner.id,
password: randomValidPassword(), password: randomValidPassword(),
token: uuid(), token: uuid(),
}, },
]; ];
const { password: originalHashedPassword } = await Db.collections.User!.findOneOrFail(); await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
for (const invalidPayload of invalidPayloads) {
const response = await authlessAgent.post('/change-password').query(invalidPayload); const response = await authlessAgent.post('/change-password').query(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
const { password: fetchedHashedPassword } = await Db.collections.User!.findOneOrFail(); const { password: storedPassword } = await Db.collections.User!.findOneOrFail();
expect(originalHashedPassword).toBe(fetchedHashedPassword); expect(owner.password).toBe(storedPassword);
} }),
);
}); });
test('POST /change-password should fail when token has expired', async () => { test('POST /change-password should fail when token has expired', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid(); const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1; const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
await Db.collections.User!.update(INITIAL_TEST_USER.id, { await Db.collections.User!.update(owner.id, {
resetPasswordToken, resetPasswordToken,
resetPasswordTokenExpiration, resetPasswordTokenExpiration,
}); });
@ -291,17 +277,9 @@ test('POST /change-password should fail when token has expired', async () => {
const response = await authlessAgent.post('/change-password').send({ const response = await authlessAgent.post('/change-password').send({
token: resetPasswordToken, token: resetPasswordToken,
userId: INITIAL_TEST_USER.id, userId: owner.id,
password: passwordToStore, password: passwordToStore,
}); });
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
}); });
const INITIAL_TEST_USER = {
id: uuid(),
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};

View file

@ -1,4 +1,4 @@
import { createConnection, getConnection, ConnectionOptions } from 'typeorm'; import { createConnection, getConnection, ConnectionOptions, Connection } from 'typeorm';
import { Credentials, UserSettings } from 'n8n-core'; import { Credentials, UserSettings } from 'n8n-core';
import config = require('../../../config'); import config = require('../../../config');
@ -6,17 +6,17 @@ import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } f
import { DatabaseType, Db, ICredentialsDb, IDatabaseCollections } from '../../../src'; import { DatabaseType, Db, ICredentialsDb, IDatabaseCollections } from '../../../src';
import { randomEmail, randomName, randomString, randomValidPassword } from './random'; import { randomEmail, randomName, randomString, randomValidPassword } from './random';
import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity'; import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
import { hashPassword } from '../../../src/UserManagement/UserManagementHelper';
import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants'; import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants';
import { entities } from '../../../src/databases/entities'; import { entities } from '../../../src/databases/entities';
import { mysqlMigrations } from '../../../src/databases/mysqldb/migrations'; import { mysqlMigrations } from '../../../src/databases/mysqldb/migrations';
import { postgresMigrations } from '../../../src/databases/postgresdb/migrations'; import { postgresMigrations } from '../../../src/databases/postgresdb/migrations';
import { sqliteMigrations } from '../../../src/databases/sqlite/migrations'; import { sqliteMigrations } from '../../../src/databases/sqlite/migrations';
import { categorize } from './utils';
import type { Role } from '../../../src/databases/entities/Role'; import type { Role } from '../../../src/databases/entities/Role';
import type { User } from '../../../src/databases/entities/User'; import type { User } from '../../../src/databases/entities/User';
import type { CredentialPayload } from './types'; import type { CollectionName, CredentialPayload } from './types';
import { genSaltSync, hashSync } from 'bcryptjs';
/** /**
* Initialize one test DB per suite run, with bootstrap connection if needed. * Initialize one test DB per suite run, with bootstrap connection if needed.
@ -97,22 +97,49 @@ export async function terminate(testDbName: string) {
} }
/** /**
* Truncate DB tables for specified entities. * Truncate DB tables for collections.
* *
* @param entities Array of entity names whose tables to truncate. * @param collections Array of entity names whose tables to truncate.
* @param testDbName Name of the test DB to truncate tables in. * @param testDbName Name of the test DB to truncate tables in.
*/ */
export async function truncate(entities: Array<keyof IDatabaseCollections>, testDbName: string) { export async function truncate(collections: CollectionName[], testDbName: string) {
const dbType = config.get('database.type'); const dbType = config.get('database.type');
if (dbType === 'sqlite') {
const testDb = getConnection(testDbName); const testDb = getConnection(testDbName);
if (dbType === 'sqlite') {
await testDb.query('PRAGMA foreign_keys=OFF'); await testDb.query('PRAGMA foreign_keys=OFF');
await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); await Promise.all(collections.map((collection) => Db.collections[collection]!.clear()));
return testDb.query('PRAGMA foreign_keys=ON'); return testDb.query('PRAGMA foreign_keys=ON');
} }
const map: { [K in keyof IDatabaseCollections]: string } = { if (dbType === 'postgresdb') {
return Promise.all(
collections.map((collection) => {
const tableName = toTableName(collection);
testDb.query(`TRUNCATE TABLE "${tableName}" RESTART IDENTITY CASCADE;`);
}),
);
}
/**
* MySQL `TRUNCATE` requires enabling and disabling the global variable `foreign_key_checks`,
* which cannot be safely manipulated by parallel tests, so use `DELETE` and `AUTO_INCREMENT`.
* Clear shared tables first to avoid deadlock: https://stackoverflow.com/a/41174997
*/
if (dbType === 'mysqldb') {
const { pass: isShared, fail: isNotShared } = categorize(
collections,
(collectionName: CollectionName) => collectionName.toLowerCase().startsWith('shared'),
);
await truncateMySql(testDb, isShared);
await truncateMySql(testDb, isNotShared);
}
}
function toTableName(collectionName: CollectionName) {
return {
Credentials: 'credentials_entity', Credentials: 'credentials_entity',
Workflow: 'workflow_entity', Workflow: 'workflow_entity',
Execution: 'execution_entity', Execution: 'execution_entity',
@ -123,28 +150,18 @@ export async function truncate(entities: Array<keyof IDatabaseCollections>, test
SharedCredentials: 'shared_credentials', SharedCredentials: 'shared_credentials',
SharedWorkflow: 'shared_workflow', SharedWorkflow: 'shared_workflow',
Settings: 'settings', Settings: 'settings',
}; }[collectionName];
if (dbType === 'postgresdb') {
return Promise.all(
entities.map((entity) =>
getConnection(testDbName).query(
`TRUNCATE TABLE "${map[entity]}" RESTART IDENTITY CASCADE;`,
),
),
);
} }
// MySQL truncation requires globals, which cannot be safely manipulated by parallel tests function truncateMySql(connection: Connection, collections: Array<keyof IDatabaseCollections>) {
if (dbType === 'mysqldb') { return Promise.all(
await Promise.all( collections.map(async (collection) => {
entities.map(async (entity) => { const tableName = toTableName(collection);
await Db.collections[entity]!.delete({}); await connection.query(`DELETE FROM ${tableName};`);
await getConnection(testDbName).query(`ALTER TABLE ${map[entity]} AUTO_INCREMENT = 1;`); await connection.query(`ALTER TABLE ${tableName} AUTO_INCREMENT = 1;`);
}), }),
); );
} }
}
// ---------------------------------- // ----------------------------------
// credential creation // credential creation
@ -182,60 +199,62 @@ export async function saveCredential(
// user creation // user creation
// ---------------------------------- // ----------------------------------
/** export async function createUser(attributes: Partial<User> & { globalRole: Role }): Promise<User> {
* Store a user in the DB, defaulting to a `member`.
*/
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
const { email, password, firstName, lastName, globalRole, ...rest } = attributes; const { email, password, firstName, lastName, globalRole, ...rest } = attributes;
const user = { const user = {
email: email ?? randomEmail(), email: email ?? randomEmail(),
password: hashSync(password ?? randomValidPassword(), genSaltSync(10)), password: await hashPassword(password ?? randomValidPassword()),
firstName: firstName ?? randomName(), firstName: firstName ?? randomName(),
lastName: lastName ?? randomName(), lastName: lastName ?? randomName(),
globalRole: globalRole ?? (await getGlobalMemberRole()), globalRole,
...rest, ...rest,
}; };
return Db.collections.User!.save(user); return Db.collections.User!.save(user);
} }
export async function createOwnerShell() { export function createUserShell(globalRole: Role): Promise<User> {
const globalRole = await getGlobalOwnerRole(); if (globalRole.scope !== 'global') {
return Db.collections.User!.save({ globalRole }); throw new Error(`Invalid role received: ${JSON.stringify(globalRole)}`);
} }
export async function createMemberShell() { const shell: Partial<User> = { globalRole };
const globalRole = await getGlobalMemberRole();
return Db.collections.User!.save({ globalRole }); if (globalRole.name !== 'owner') {
shell.email = randomEmail();
}
return Db.collections.User!.save(shell);
} }
// ---------------------------------- // ----------------------------------
// role fetchers // role fetchers
// ---------------------------------- // ----------------------------------
export async function getGlobalOwnerRole() { export function getGlobalOwnerRole() {
return await Db.collections.Role!.findOneOrFail({ return Db.collections.Role!.findOneOrFail({
name: 'owner', name: 'owner',
scope: 'global', scope: 'global',
}); });
} }
export async function getGlobalMemberRole() { export function getGlobalMemberRole() {
return await Db.collections.Role!.findOneOrFail({ return Db.collections.Role!.findOneOrFail({
name: 'member', name: 'member',
scope: 'global', scope: 'global',
}); });
} }
export async function getWorkflowOwnerRole() { export function getWorkflowOwnerRole() {
return await Db.collections.Role!.findOneOrFail({ return Db.collections.Role!.findOneOrFail({
name: 'owner', name: 'owner',
scope: 'workflow', scope: 'workflow',
}); });
} }
export async function getCredentialOwnerRole() { export function getCredentialOwnerRole() {
return await Db.collections.Role!.findOneOrFail({ return Db.collections.Role!.findOneOrFail({
name: 'owner', name: 'owner',
scope: 'credential', scope: 'credential',
}); });

View file

@ -1,8 +1,10 @@
import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-workflow'; import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-workflow';
import type { ICredentialsDb } from '../../../src'; import type { ICredentialsDb, IDatabaseCollections } from '../../../src';
import type { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity'; import type { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
import type { User } from '../../../src/databases/entities/User'; import type { User } from '../../../src/databases/entities/User';
export type CollectionName = keyof IDatabaseCollections;
export type SmtpTestAccount = { export type SmtpTestAccount = {
user: string; user: string;
pass: string; pass: string;

View file

@ -23,9 +23,7 @@ import { passwordResetNamespace as passwordResetEndpoints } from '../../../src/U
import { issueJWT } from '../../../src/UserManagement/auth/jwt'; import { issueJWT } from '../../../src/UserManagement/auth/jwt';
import { getLogger } from '../../../src/Logger'; import { getLogger } from '../../../src/Logger';
import { credentialsController } from '../../../src/api/credentials.api'; import { credentialsController } from '../../../src/api/credentials.api';
import type { User } from '../../../src/databases/entities/User'; import type { User } from '../../../src/databases/entities/User';
import { Telemetry } from '../../../src/telemetry';
import type { EndpointGroup, SmtpTestAccount } from './types'; import type { EndpointGroup, SmtpTestAccount } from './types';
import type { N8nApp } from '../../../src/UserManagement/Interfaces'; import type { N8nApp } from '../../../src/UserManagement/Interfaces';
@ -182,9 +180,7 @@ export function prefix(pathSegment: string) {
export function getAuthToken(response: request.Response, authCookieName = AUTH_COOKIE_NAME) { export function getAuthToken(response: request.Response, authCookieName = AUTH_COOKIE_NAME) {
const cookies: string[] = response.headers['set-cookie']; const cookies: string[] = response.headers['set-cookie'];
if (!cookies) { if (!cookies) return undefined;
throw new Error("No 'set-cookie' header found in response");
}
const authCookie = cookies.find((c) => c.startsWith(`${authCookieName}=`)); const authCookie = cookies.find((c) => c.startsWith(`${authCookieName}=`));
@ -216,5 +212,37 @@ export async function isInstanceOwnerSetUp() {
/** /**
* Get an SMTP test account from https://ethereal.email to test sending emails. * Get an SMTP test account from https://ethereal.email to test sending emails.
*/ */
export const getSmtpTestAccount = util.promisify<SmtpTestAccount>(createTestAccount); const getSmtpTestAccount = util.promisify<SmtpTestAccount>(createTestAccount);
export async function configureSmtp() {
const {
user,
pass,
smtp: { host, port, secure },
} = await getSmtpTestAccount();
config.set('userManagement.emails.mode', 'smtp');
config.set('userManagement.emails.smtp.host', host);
config.set('userManagement.emails.smtp.port', port);
config.set('userManagement.emails.smtp.secure', secure);
config.set('userManagement.emails.smtp.auth.user', user);
config.set('userManagement.emails.smtp.auth.pass', pass);
}
// ----------------------------------
// 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: [] },
);
};

View file

@ -1,12 +1,10 @@
import express = require('express'); import express = require('express');
import validator from 'validator'; import validator from 'validator';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { compare, genSaltSync, hashSync } from 'bcryptjs';
import { Db } from '../../src'; import { Db } from '../../src';
import config = require('../../config'); import config = require('../../config');
import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import { Role } from '../../src/databases/entities/Role';
import { import {
randomEmail, randomEmail,
randomValidPassword, randomValidPassword,
@ -15,15 +13,18 @@ import {
} from './shared/random'; } from './shared/random';
import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity';
import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity';
import type { Role } from '../../src/databases/entities/Role';
import type { User } from '../../src/databases/entities/User';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import { compareHash } from '../../src/UserManagement/UserManagementHelper';
jest.mock('../../src/telemetry'); jest.mock('../../src/telemetry');
let app: express.Application; let app: express.Application;
let testDbName = ''; let testDbName = '';
let globalOwnerRole: Role;
let globalMemberRole: Role; let globalMemberRole: Role;
let globalOwnerRole: Role;
let workflowOwnerRole: Role; let workflowOwnerRole: Role;
let credentialOwnerRole: Role; let credentialOwnerRole: Role;
@ -46,25 +47,17 @@ beforeAll(async () => {
utils.initTestTelemetry(); utils.initTestTelemetry();
utils.initTestLogger(); utils.initTestLogger();
jest.setTimeout(30000); // fake SMTP service might be slow
}); });
beforeEach(async () => { beforeEach(async () => {
// do not combine calls - shared tables must be cleared first and separately await testDb.truncate(
await testDb.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName); ['User', 'SharedCredentials', 'SharedWorkflow', 'Workflow', 'Credentials'],
await testDb.truncate(['User', 'Workflow', 'Credentials'], testDbName); testDbName,
);
jest.isolateModules(() => {
jest.mock('../../config'); jest.mock('../../config');
});
await testDb.createUser({
id: INITIAL_TEST_USER.id,
email: INITIAL_TEST_USER.email,
password: INITIAL_TEST_USER.password,
firstName: INITIAL_TEST_USER.firstName,
lastName: INITIAL_TEST_USER.lastName,
globalRole: globalOwnerRole,
});
config.set('userManagement.disabled', false); config.set('userManagement.disabled', false);
config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.isInstanceOwnerSetUp', true);
@ -76,17 +69,18 @@ afterAll(async () => {
}); });
test('GET /users should return all users', async () => { test('GET /users should return all users', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
await testDb.createUser(); await testDb.createUser({ globalRole: globalMemberRole });
const response = await authOwnerAgent.get('/users'); const response = await authOwnerAgent.get('/users');
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(2); expect(response.body.data.length).toBe(2);
for (const user of response.body.data) { await Promise.all(
response.body.data.map(async (user: User) => {
const { const {
id, id,
email, email,
@ -108,14 +102,15 @@ test('GET /users should return all users', async () => {
expect(resetPasswordToken).toBeUndefined(); expect(resetPasswordToken).toBeUndefined();
expect(isPending).toBe(false); expect(isPending).toBe(false);
expect(globalRole).toBeDefined(); expect(globalRole).toBeDefined();
} }),
);
}); });
test('DELETE /users/:id should delete the user', async () => { test('DELETE /users/:id should delete the user', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const userToDelete = await testDb.createUser(); const userToDelete = await testDb.createUser({ globalRole: globalMemberRole });
const newWorkflow = new WorkflowEntity(); const newWorkflow = new WorkflowEntity();
@ -181,7 +176,7 @@ test('DELETE /users/:id should delete the user', async () => {
}); });
test('DELETE /users/:id should fail to delete self', async () => { test('DELETE /users/:id should fail to delete self', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const response = await authOwnerAgent.delete(`/users/${owner.id}`); const response = await authOwnerAgent.delete(`/users/${owner.id}`);
@ -193,10 +188,10 @@ test('DELETE /users/:id should fail to delete self', async () => {
}); });
test('DELETE /users/:id should fail if user to delete is transferee', async () => { test('DELETE /users/:id should fail if user to delete is transferee', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const { id: idToDelete } = await testDb.createUser(); const { id: idToDelete } = await testDb.createUser({ globalRole: globalMemberRole });
const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({ const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({
transferId: idToDelete, transferId: idToDelete,
@ -209,7 +204,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () =
}); });
test('DELETE /users/:id with transferId should perform transfer', async () => { test('DELETE /users/:id with transferId should perform transfer', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const userToDelete = await Db.collections.User!.save({ const userToDelete = await Db.collections.User!.save({
@ -281,36 +276,34 @@ test('DELETE /users/:id with transferId should perform transfer', async () => {
}); });
test('GET /resolve-signup-token should validate invite token', async () => { test('GET /resolve-signup-token should validate invite token', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const { id: inviteeId } = await testDb.createMemberShell(); const memberShell = await testDb.createUserShell(globalMemberRole);
const response = await authOwnerAgent const response = await authOwnerAgent
.get('/resolve-signup-token') .get('/resolve-signup-token')
.query({ inviterId: INITIAL_TEST_USER.id }) .query({ inviterId: owner.id })
.query({ inviteeId }); .query({ inviteeId: memberShell.id });
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ expect(response.body).toEqual({
data: { data: {
inviter: { inviter: {
firstName: INITIAL_TEST_USER.firstName, firstName: owner.firstName,
lastName: INITIAL_TEST_USER.lastName, lastName: owner.lastName,
}, },
}, },
}); });
}); });
test('GET /resolve-signup-token should fail with invalid inputs', async () => { test('GET /resolve-signup-token should fail with invalid inputs', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const { id: inviteeId } = await testDb.createUser(); const { id: inviteeId } = await testDb.createUser({ globalRole: globalMemberRole });
const first = await authOwnerAgent const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id });
.get('/resolve-signup-token')
.query({ inviterId: INITIAL_TEST_USER.id });
const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId }); const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId });
@ -322,14 +315,14 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => {
// user is already set up, so call should error // user is already set up, so call should error
const fourth = await authOwnerAgent const fourth = await authOwnerAgent
.get('/resolve-signup-token') .get('/resolve-signup-token')
.query({ inviterId: INITIAL_TEST_USER.id }) .query({ inviterId: owner.id })
.query({ inviteeId }); .query({ inviteeId });
// cause inconsistent DB state // cause inconsistent DB state
await Db.collections.User!.update(owner.id, { email: '' }); await Db.collections.User!.update(owner.id, { email: '' });
const fifth = await authOwnerAgent const fifth = await authOwnerAgent
.get('/resolve-signup-token') .get('/resolve-signup-token')
.query({ inviterId: INITIAL_TEST_USER.id }) .query({ inviterId: owner.id })
.query({ inviteeId }); .query({ inviteeId });
for (const response of [first, second, third, fourth, fifth]) { for (const response of [first, second, third, fourth, fifth]) {
@ -338,21 +331,20 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => {
}); });
test('POST /users/:id should fill out a user shell', async () => { test('POST /users/:id should fill out a user shell', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const memberShell = await testDb.createUserShell(globalMemberRole);
const memberData = {
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const userToFillOut = await Db.collections.User!.save({ const response = await authlessAgent.post(`/users/${memberShell.id}`).send(memberData);
email: randomEmail(),
globalRole: globalMemberRole,
});
const newPassword = randomValidPassword();
const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send({
inviterId: INITIAL_TEST_USER.id,
firstName: INITIAL_TEST_USER.firstName,
lastName: INITIAL_TEST_USER.lastName,
password: newPassword,
});
const { const {
id, id,
@ -368,8 +360,8 @@ test('POST /users/:id should fill out a user shell', async () => {
expect(validator.isUUID(id)).toBe(true); expect(validator.isUUID(id)).toBe(true);
expect(email).toBeDefined(); expect(email).toBeDefined();
expect(firstName).toBe(INITIAL_TEST_USER.firstName); expect(firstName).toBe(memberData.firstName);
expect(lastName).toBe(INITIAL_TEST_USER.lastName); expect(lastName).toBe(memberData.lastName);
expect(personalizationAnswers).toBeNull(); expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined(); expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined(); expect(resetPasswordToken).toBeUndefined();
@ -379,69 +371,98 @@ test('POST /users/:id should fill out a user shell', async () => {
const authToken = utils.getAuthToken(response); const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined(); expect(authToken).toBeDefined();
const filledOutUser = await Db.collections.User!.findOneOrFail(userToFillOut.id); const member = await Db.collections.User!.findOneOrFail(memberShell.id);
expect(filledOutUser.firstName).toBe(INITIAL_TEST_USER.firstName); expect(member.firstName).toBe(memberData.firstName);
expect(filledOutUser.lastName).toBe(INITIAL_TEST_USER.lastName); expect(member.lastName).toBe(memberData.lastName);
expect(filledOutUser.password).not.toBe(newPassword); expect(member.password).not.toBe(memberData.password);
}); });
test('POST /users/:id should fail with invalid inputs', async () => { test('POST /users/:id should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app); const authlessAgent = utils.createAgent(app);
const emailToStore = randomEmail(); const memberShellEmail = randomEmail();
const userToFillOut = await Db.collections.User!.save({ const memberShell = await Db.collections.User!.save({
email: emailToStore, email: memberShellEmail,
globalRole: globalMemberRole, globalRole: globalMemberRole,
}); });
for (const invalidPayload of INVALID_FILL_OUT_USER_PAYLOADS) { const invalidPayloads = [
const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send(invalidPayload); {
expect(response.statusCode).toBe(400);
const user = await Db.collections.User!.findOneOrFail({ where: { email: emailToStore } });
expect(user.firstName).toBeNull();
expect(user.lastName).toBeNull();
expect(user.password).toBeNull();
}
});
test('POST /users/:id should fail with already accepted invite', async () => {
const authlessAgent = utils.createAgent(app);
const globalMemberRole = await Db.collections.Role!.findOneOrFail({
name: 'member',
scope: 'global',
});
const shell = await Db.collections.User!.save({
email: randomEmail(),
password: hashSync(randomValidPassword(), genSaltSync(10)), // simulate accepted invite
globalRole: globalMemberRole,
});
const newPassword = randomValidPassword();
const response = await authlessAgent.post(`/users/${shell.id}`).send({
inviterId: INITIAL_TEST_USER.id,
firstName: randomName(), firstName: randomName(),
lastName: randomName(), lastName: randomName(),
password: randomValidPassword(), password: randomValidPassword(),
},
{
inviterId: owner.id,
firstName: randomName(),
password: randomValidPassword(),
},
{
inviterId: owner.id,
firstName: randomName(),
password: randomValidPassword(),
},
{
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
},
{
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
password: randomInvalidPassword(),
},
];
await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
const response = await authlessAgent.post(`/users/${memberShell.id}`).send(invalidPayload);
expect(response.statusCode).toBe(400);
const storedUser = await Db.collections.User!.findOneOrFail({
where: { email: memberShellEmail },
}); });
expect(storedUser.firstName).toBeNull();
expect(storedUser.lastName).toBeNull();
expect(storedUser.password).toBeNull();
}),
);
});
test('POST /users/:id should fail with already accepted invite', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const member = await testDb.createUser({ globalRole: globalMemberRole });
const newMemberData = {
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.post(`/users/${member.id}`).send(newMemberData);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
const fetchedShell = await Db.collections.User!.findOneOrFail({ where: { email: shell.email } }); const storedMember = await Db.collections.User!.findOneOrFail({
expect(fetchedShell.firstName).toBeNull(); where: { email: member.email },
expect(fetchedShell.lastName).toBeNull(); });
expect(storedMember.firstName).not.toBe(newMemberData.firstName);
expect(storedMember.lastName).not.toBe(newMemberData.lastName);
const comparisonResult = await compare(shell.password, newPassword); const comparisonResult = await compareHash(member.password, storedMember.password);
expect(comparisonResult).toBe(false); expect(comparisonResult).toBe(false);
expect(newPassword).not.toBe(fetchedShell.password); expect(storedMember.password).not.toBe(newMemberData.password);
}); });
test('POST /users should fail if emailing is not set up', async () => { test('POST /users should fail if emailing is not set up', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]); const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]);
@ -450,7 +471,7 @@ test('POST /users should fail if emailing is not set up', async () => {
}); });
test('POST /users should fail if user management is disabled', async () => { test('POST /users should fail if user management is disabled', async () => {
const owner = await Db.collections.User!.findOneOrFail(); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
config.set('userManagement.disabled', true); config.set('userManagement.disabled', true);
@ -461,54 +482,47 @@ test('POST /users should fail if user management is disabled', async () => {
}); });
test('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 owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
const { await utils.configureSmtp();
user,
pass,
smtp: { host, port, secure },
} = await utils.getSmtpTestAccount();
config.set('userManagement.emails.mode', 'smtp'); const testEmails = [randomEmail(), randomEmail(), randomEmail()];
config.set('userManagement.emails.smtp.host', host);
config.set('userManagement.emails.smtp.port', port);
config.set('userManagement.emails.smtp.secure', secure);
config.set('userManagement.emails.smtp.auth.user', user);
config.set('userManagement.emails.smtp.auth.pass', pass);
const payload = TEST_EMAILS_TO_CREATE_USER_SHELLS.map((e) => ({ email: e })); const payload = testEmails.map((e) => ({ email: e }));
const response = await authOwnerAgent.post('/users').send(payload); const response = await authOwnerAgent.post('/users').send(payload);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
for (const { await Promise.all(
user: { id, email: receivedEmail }, response.body.data.map(async ({ user, error }: { user: User; error: Error }) => {
error, const { id, email: receivedEmail } = user;
} of response.body.data) {
expect(validator.isUUID(id)).toBe(true); expect(validator.isUUID(id)).toBe(true);
expect(TEST_EMAILS_TO_CREATE_USER_SHELLS.some((e) => e === receivedEmail)).toBe(true); expect(testEmails.some((e) => e === receivedEmail)).toBe(true);
if (error) { if (error) {
expect(error).toBe('Email could not be sent'); expect(error).toBe('Email could not be sent');
} }
const user = await Db.collections.User!.findOneOrFail(id); const storedUser = await Db.collections.User!.findOneOrFail(id);
const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } = user; const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } =
storedUser;
expect(firstName).toBeNull(); expect(firstName).toBeNull();
expect(lastName).toBeNull(); expect(lastName).toBeNull();
expect(personalizationAnswers).toBeNull(); expect(personalizationAnswers).toBeNull();
expect(password).toBeNull(); expect(password).toBeNull();
expect(resetPasswordToken).toBeNull(); expect(resetPasswordToken).toBeNull();
} }),
);
}); });
test('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 owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
config.set('userManagement.emails.mode', 'smtp'); await utils.configureSmtp();
const invalidPayloads = [ const invalidPayloads = [
randomEmail(), randomEmail(),
@ -518,20 +532,22 @@ test('POST /users should fail with invalid inputs', async () => {
[{ email: randomName() }], [{ email: randomName() }],
]; ];
for (const invalidPayload of invalidPayloads) { await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/users').send(invalidPayload); const response = await authOwnerAgent.post('/users').send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
const users = await Db.collections.User!.find(); const users = await Db.collections.User!.find();
expect(users.length).toBe(1); // DB unaffected expect(users.length).toBe(1); // DB unaffected
} }),
);
}); });
test('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 owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
config.set('userManagement.emails.mode', 'smtp'); await utils.configureSmtp();
const response = await authOwnerAgent.post('/users').send([]); const response = await authOwnerAgent.post('/users').send([]);
@ -561,42 +577,3 @@ test('POST /users should ignore an empty payload', async () => {
// expect(response.statusCode).toBe(500); // expect(response.statusCode).toBe(500);
// }); // });
const INITIAL_TEST_USER = {
id: uuid(),
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const INVALID_FILL_OUT_USER_PAYLOADS = [
{
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
},
{
inviterId: INITIAL_TEST_USER.id,
firstName: randomName(),
password: randomValidPassword(),
},
{
inviterId: INITIAL_TEST_USER.id,
firstName: randomName(),
password: randomValidPassword(),
},
{
inviterId: INITIAL_TEST_USER.id,
firstName: randomName(),
lastName: randomName(),
},
{
inviterId: INITIAL_TEST_USER.id,
firstName: randomName(),
lastName: randomName(),
password: randomInvalidPassword(),
},
];
const TEST_EMAILS_TO_CREATE_USER_SHELLS = [randomEmail(), randomEmail(), randomEmail()];