mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
🚨 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:
parent
e78bf15ba9
commit
1e2d6daaa3
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 +=
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
283
packages/cli/test/integration/auth.api.test.ts
Normal file
283
packages/cli/test/integration/auth.api.test.ts
Normal 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();
|
||||||
|
});
|
|
@ -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(),
|
|
||||||
};
|
|
|
@ -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);
|
||||||
});
|
});
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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, {}];
|
|
@ -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: '',
|
|
@ -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(),
|
|
||||||
};
|
|
|
@ -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,27 +150,17 @@ 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') {
|
function truncateMySql(connection: Connection, collections: Array<keyof IDatabaseCollections>) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
entities.map((entity) =>
|
collections.map(async (collection) => {
|
||||||
getConnection(testDbName).query(
|
const tableName = toTableName(collection);
|
||||||
`TRUNCATE TABLE "${map[entity]}" RESTART IDENTITY CASCADE;`,
|
await connection.query(`DELETE FROM ${tableName};`);
|
||||||
),
|
await connection.query(`ALTER TABLE ${tableName} AUTO_INCREMENT = 1;`);
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MySQL truncation requires globals, which cannot be safely manipulated by parallel tests
|
|
||||||
if (dbType === 'mysqldb') {
|
|
||||||
await Promise.all(
|
|
||||||
entities.map(async (entity) => {
|
|
||||||
await Db.collections[entity]!.delete({});
|
|
||||||
await getConnection(testDbName).query(`ALTER TABLE ${map[entity]} AUTO_INCREMENT = 1;`);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -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',
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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: [] },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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()];
|
|
Loading…
Reference in a new issue