From 5ae6b3fb8af7116efd7d8ae5d01b3a088296737a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 9 Feb 2022 11:11:03 +0100 Subject: [PATCH 01/81] :zap: Refactor users namespace --- .../UserManagement/UserManagementHelper.ts | 21 +- .../cli/src/UserManagement/routes/index.ts | 2 +- .../cli/src/UserManagement/routes/users.ts | 228 +++++++++--------- packages/cli/src/requests.d.ts | 19 ++ 4 files changed, 146 insertions(+), 124 deletions(-) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 51f40183ae71d..966120af99a99 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -1,17 +1,28 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable import/no-cycle */ -import { IsNull, Not } from 'typeorm'; -import { Db, ResponseHelper } from '..'; +import { IsNull, Not, QueryFailedError } from 'typeorm'; +import { Db, GenericHelpers, ResponseHelper } from '..'; import config = require('../../config'); import { User } from '../databases/entities/User'; import { PublicUser } from './Interfaces'; -export function isEmailSetup(): boolean { - const emailMode = config.get('userManagement.emails.mode') as string; - return !!emailMode; +export const isEmailSetUp = Boolean(config.get('userManagement.emails.mode')); + +export function getInstanceDomain(): string { + let domain = GenericHelpers.getBaseUrl(); + if (domain.endsWith('/')) { + domain = domain.slice(0, domain.length - 1); + } + + return domain; } +export const isFailedQuery = ( + error: unknown, +): error is QueryFailedError & { parameters: Array } => + error instanceof QueryFailedError; + export async function isInstanceOwnerSetup(): Promise { const users = await Db.collections.User!.find({ email: Not(IsNull()) }); return users.length !== 0; diff --git a/packages/cli/src/UserManagement/routes/index.ts b/packages/cli/src/UserManagement/routes/index.ts index 847da8e0c0153..beb2c1f6013b3 100644 --- a/packages/cli/src/UserManagement/routes/index.ts +++ b/packages/cli/src/UserManagement/routes/index.ts @@ -65,7 +65,7 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint req.url.startsWith('/fonts/') || req.url.startsWith(`/${restEndpoint}/settings`) || req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) || - req.url === `/${restEndpoint}/user` + new RegExp(`/${restEndpoint}/user(/?)$`).test(req.url) ) { return next(); } diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index f0f374fea30ce..2f17215cfa879 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -1,15 +1,19 @@ /* eslint-disable import/no-cycle */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { Request, Response } from 'express'; +import { Response } from 'express'; import { getConnection, In } from 'typeorm'; -import { LoggerProxy } from 'n8n-workflow'; import { genSaltSync, hashSync } from 'bcryptjs'; import validator from 'validator'; -import { Db, GenericHelpers, ResponseHelper } from '../..'; +import { Db, ResponseHelper } from '../..'; import { N8nApp } from '../Interfaces'; -import { AuthenticatedRequest, UserRequest } from '../../requests'; -import { isEmailSetup, sanitizeUser } from '../UserManagementHelper'; +import { UserRequest } from '../../requests'; +import { + getInstanceDomain, + isEmailSetUp, + isFailedQuery, + sanitizeUser, +} from '../UserManagementHelper'; import { User } from '../../databases/entities/User'; import { SharedWorkflow } from '../../databases/entities/SharedWorkflow'; import { SharedCredentials } from '../../databases/entities/SharedCredentials'; @@ -17,10 +21,13 @@ import { getInstance } from '../email/UserManagementMailer'; import { issueJWT } from '../auth/jwt'; export function usersNamespace(this: N8nApp): void { + /** + * Send email invite(s) to one or multiple users and create user shell(s). + */ this.app.post( `/${this.restEndpoint}/users`, ResponseHelper.send(async (req: UserRequest.Invite) => { - if (!isEmailSetup()) { + if (!isEmailSetUp) { throw new ResponseHelper.ResponseError( 'Email sending must be set up in order to invite other users', undefined, @@ -28,61 +35,44 @@ export function usersNamespace(this: N8nApp): void { ); } - const invitations = req.body; - - if (!Array.isArray(invitations)) { + if (!Array.isArray(req.body)) { throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } - // Validate payload - invitations.forEach((invitation) => { - if (!validator.isEmail(invitation.email)) { - throw new ResponseHelper.ResponseError( - `Invalid email address ${invitation.email}`, - undefined, - 400, - ); + const invites = req.body; + + invites.forEach(({ email }) => { + if (!validator.isEmail(email)) { + throw new ResponseHelper.ResponseError(`Invalid email address: ${email}`, undefined, 400); } }); - const role = await Db.collections.Role!.findOne({ scope: 'global', name: 'member' }); + const role = await Db.collections.Role!.findOneOrFail({ scope: 'global', name: 'member' }); - if (!role) { - throw new ResponseHelper.ResponseError( - 'Members role not found in database - inconsistent state', - undefined, - 500, - ); - } - - let domain = GenericHelpers.getBaseUrl(); - if (domain.endsWith('/')) { - domain = domain.slice(0, domain.length - 1); - } - - let createdUsers = []; + let createdUsers: User[] = []; try { createdUsers = await getConnection().transaction(async (transactionManager) => { return Promise.all( - invitations.map(async ({ email }) => { - const newUser = Object.assign(new User(), { - email, - globalRole: role, - }); + invites.map(async ({ email }) => { + const newUser = new User(); + Object.assign(newUser, { email, globalRole: role }); return transactionManager.save(newUser); }), ); }); } catch (error) { - throw new ResponseHelper.ResponseError( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access - `Email address ${error.parameters[1]} already exists`, - undefined, - 400, - ); + if (isFailedQuery(error)) { + throw new ResponseHelper.ResponseError( + `Email address ${error.parameters[1]} already exists`, + undefined, + 400, + ); + } } + const domain = getInstanceDomain(); const mailer = getInstance(); + return Promise.all( createdUsers.map(async ({ id, email }) => { const inviteAcceptUrl = `${domain}/signup/inviterId=${req.user.id}&inviteeId=${id}`; @@ -91,69 +81,62 @@ export function usersNamespace(this: N8nApp): void { inviteAcceptUrl, domain, }); + if (!result.success) { throw new ResponseHelper.ResponseError(`Email to ${email} could not be sent`); } + return { id, email }; }), ); }), ); + /** + * Validate invite token to enable invitee to set up their account. + */ this.app.get( `/${this.restEndpoint}/resolve-signup-token`, - ResponseHelper.send(async (req: Request) => { - const inviterId = req.query.inviterId as string; - const inviteeId = req.query.inviteeId as string; + ResponseHelper.send(async (req: UserRequest.ResolveSignUp) => { + const { inviterId, inviteeId } = req.query; if (!inviterId || !inviteeId) { - LoggerProxy.error('Invalid invite URL - did not receive user IDs', { - inviterId, - inviteeId, - }); throw new ResponseHelper.ResponseError('Invalid payload', undefined, 500); } const users = await Db.collections.User!.find({ where: { id: In([inviterId, inviteeId]) } }); if (users.length !== 2) { - LoggerProxy.error('Invalid invite URL - did not find users', { inviterId, inviteeId }); throw new ResponseHelper.ResponseError('Invalid invite URL', undefined, 500); } const inviter = users.find((user) => user.id === inviterId); if (!inviter || !inviter.email || !inviter.firstName) { - LoggerProxy.error('Invalid invite URL - inviter does not have email set', { - inviterId, - inviteeId, - }); throw new ResponseHelper.ResponseError('Invalid request', undefined, 500); } + const { firstName, lastName } = inviter; return { inviter: { firstName, lastName } }; }), ); + /** + * Fill out user shell with first name, last name, and password. + */ this.app.post( `/${this.restEndpoint}/user`, - ResponseHelper.send(async (req: AuthenticatedRequest, res: Response) => { + ResponseHelper.send(async (req: UserRequest.Update, res: Response) => { if (req.user) { throw new ResponseHelper.ResponseError( - 'Please logout before accepting another invite.', + 'Please log out before accepting another invite', undefined, 500, ); } - const { inviterId, inviteeId, firstName, lastName, password } = req.body as { - inviterId: string; - inviteeId: string; - firstName: string; - lastName: string; - password: string; - }; + const { inviterId, inviteeId, firstName, lastName, password } = req.body; if (!inviterId || !inviteeId || !firstName || !lastName || !password) { throw new ResponseHelper.ResponseError('Invalid payload', undefined, 500); @@ -185,6 +168,7 @@ export function usersNamespace(this: N8nApp): void { const userData = await issueJWT(updatedUser); res.cookie('n8n-auth', userData.token, { maxAge: userData.expiresIn, httpOnly: true }); + return sanitizeUser(updatedUser); }), ); @@ -194,78 +178,91 @@ export function usersNamespace(this: N8nApp): void { ResponseHelper.send(async () => { const users = await Db.collections.User!.find(); - return users.map((user) => sanitizeUser(user)); + return users.map(sanitizeUser); }), ); + /** + * Delete a user. Optionally, designate a transferee for their workflows and credentials. + */ this.app.delete( `/${this.restEndpoint}/users/:id`, ResponseHelper.send(async (req: UserRequest.Delete) => { - if (req.user.id === req.params.id) { - throw new ResponseHelper.ResponseError('You cannot delete your own user', undefined, 400); + const { id: idToDelete } = req.params; + + if (req.user.id === idToDelete) { + throw new ResponseHelper.ResponseError('Cannot delete your own user', undefined, 400); } const { transferId } = req.query; - const searchIds = [req.params.id]; - if (transferId) { - if (transferId === req.params.id) { - throw new ResponseHelper.ResponseError( - 'Removed user and transferred user cannot be the same', - undefined, - 400, - ); - } - searchIds.push(transferId); + if (transferId && transferId === idToDelete) { + throw new ResponseHelper.ResponseError( + 'User to delete and transferee cannot be the same', + undefined, + 400, + ); } - const users = await Db.collections.User!.find({ where: { id: In(searchIds) } }); - if ((transferId && users.length !== 2) || users.length === 0) { + const users = await Db.collections.User!.find({ + where: { id: In([transferId, idToDelete]) }, + }); + + if (!users.length || (transferId && users.length !== 2)) { throw new ResponseHelper.ResponseError('Could not find user', undefined, 404); } - const deleteUser = users.find((user) => user.id === req.params.id) as User; + const userToDelete = users.find((user) => user.id === req.params.id)!; if (transferId) { - const transferUser = users.find((user) => user.id === transferId) as User; + const transferee = users.find((user) => user.id === transferId); await getConnection().transaction(async (transactionManager) => { await transactionManager.update( SharedWorkflow, - { user: deleteUser }, - { user: transferUser }, + { user: userToDelete }, + { user: transferee }, ); await transactionManager.update( SharedCredentials, - { user: deleteUser }, - { user: transferUser }, + { user: userToDelete }, + { user: transferee }, ); - await transactionManager.delete(User, { id: deleteUser.id }); - }); - } else { - const [ownedWorkflows, ownedCredentials] = await Promise.all([ - Db.collections.SharedWorkflow!.find({ - relations: ['workflow'], - where: { user: deleteUser }, - }), - Db.collections.SharedCredentials!.find({ - relations: ['credentials'], - where: { user: deleteUser }, - }), - ]); - await getConnection().transaction(async (transactionManager) => { - await transactionManager.remove(ownedWorkflows.map(({ workflow }) => workflow)); - await transactionManager.remove(ownedCredentials.map(({ credentials }) => credentials)); - await transactionManager.delete(User, { id: deleteUser.id }); + await transactionManager.delete(User, { id: userToDelete.id }); }); + + return { success: true }; } + + const [ownedWorkflows, ownedCredentials] = await Promise.all([ + Db.collections.SharedWorkflow!.find({ + relations: ['workflow'], + where: { user: userToDelete }, + }), + Db.collections.SharedCredentials!.find({ + relations: ['credentials'], + where: { user: userToDelete }, + }), + ]); + + await getConnection().transaction(async (transactionManager) => { + await transactionManager.remove(ownedWorkflows.map(({ workflow }) => workflow)); + await transactionManager.remove(ownedCredentials.map(({ credentials }) => credentials)); + await transactionManager.delete(User, { id: userToDelete.id }); + }); + return { success: true }; }), ); + /** + * Resend email invite to user. + */ this.app.post( `/${this.restEndpoint}/users/:id/reinvite`, ResponseHelper.send(async (req: UserRequest.Reinvite) => { - if (!isEmailSetup()) { + const { id: idToReinvite } = req.params; + + if (!isEmailSetUp) { throw new ResponseHelper.ResponseError( 'Email sending must be set up in order to invite other users', undefined, @@ -273,13 +270,13 @@ export function usersNamespace(this: N8nApp): void { ); } - const user = await Db.collections.User!.findOne({ id: req.params.id }); + const reinvitee = await Db.collections.User!.findOne({ id: idToReinvite }); - if (!user) { - throw new ResponseHelper.ResponseError('User not found', undefined, 404); + if (!reinvitee) { + throw new ResponseHelper.ResponseError('Could not find user', undefined, 404); } - if (user.password) { + if (reinvitee.password) { throw new ResponseHelper.ResponseError( 'User has already accepted the invite', undefined, @@ -287,27 +284,22 @@ export function usersNamespace(this: N8nApp): void { ); } - let domain = GenericHelpers.getBaseUrl(); - if (domain.endsWith('/')) { - domain = domain.slice(0, domain.length - 1); - } + const domain = getInstanceDomain(); - const inviteAcceptUrl = `${domain}/signup/inviterId=${req.user.id}&inviteeId=${user.id}`; - - const mailer = getInstance(); - const result = await mailer.invite({ - email: user.email, - inviteAcceptUrl, + const result = await getInstance().invite({ + email: reinvitee.email, + inviteAcceptUrl: `${domain}/signup/inviterId=${req.user.id}&inviteeId=${reinvitee.id}`, domain, }); if (!result.success) { throw new ResponseHelper.ResponseError( - `Failed to send email to ${user.email}`, + `Failed to send email to ${reinvitee.email}`, undefined, 500, ); } + return { success: true }; }), ); diff --git a/packages/cli/src/requests.d.ts b/packages/cli/src/requests.d.ts index e57b6ec7d1b51..67932e4c32b07 100644 --- a/packages/cli/src/requests.d.ts +++ b/packages/cli/src/requests.d.ts @@ -157,6 +157,13 @@ export declare namespace PasswordResetRequest { export declare namespace UserRequest { export type Invite = AuthenticatedRequest<{}, {}, Array<{ email: string }>>; + export type ResolveSignUp = AuthenticatedRequest< + {}, + {}, + {}, + { inviterId?: string; inviteeId?: string } + >; + export type SignUp = AuthenticatedRequest< { id: string }, { inviterId?: string; inviteeId?: string } @@ -165,6 +172,18 @@ export declare namespace UserRequest { export type Delete = AuthenticatedRequest<{ id: string }, {}, {}, { transferId?: string }>; export type Reinvite = AuthenticatedRequest<{ id: string }>; + + export type Update = AuthenticatedRequest< + {}, + {}, + { + inviterId: string; + inviteeId: string; + firstName: string; + lastName: string; + password: string; + } + >; } // ---------------------------------- From eadbec05342533014788f89b25ae6b3ba47b117b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 9 Feb 2022 11:16:43 +0100 Subject: [PATCH 02/81] :zap: Adjust fillout endpoint --- packages/cli/src/UserManagement/routes/index.ts | 2 +- packages/cli/src/UserManagement/routes/users.ts | 6 ++++-- packages/cli/src/requests.d.ts | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/UserManagement/routes/index.ts b/packages/cli/src/UserManagement/routes/index.ts index beb2c1f6013b3..eb111332371a4 100644 --- a/packages/cli/src/UserManagement/routes/index.ts +++ b/packages/cli/src/UserManagement/routes/index.ts @@ -65,7 +65,7 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint req.url.startsWith('/fonts/') || req.url.startsWith(`/${restEndpoint}/settings`) || req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) || - new RegExp(`/${restEndpoint}/user(/?)$`).test(req.url) + new RegExp(`/${restEndpoint}/users/(\\d)+`).test(req.url) ) { return next(); } diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 2f17215cfa879..00ebf6121d8f4 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -126,7 +126,7 @@ export function usersNamespace(this: N8nApp): void { * Fill out user shell with first name, last name, and password. */ this.app.post( - `/${this.restEndpoint}/user`, + `/${this.restEndpoint}/users/:id`, ResponseHelper.send(async (req: UserRequest.Update, res: Response) => { if (req.user) { throw new ResponseHelper.ResponseError( @@ -136,7 +136,9 @@ export function usersNamespace(this: N8nApp): void { ); } - const { inviterId, inviteeId, firstName, lastName, password } = req.body; + const { id: inviteeId } = req.params; + + const { inviterId, firstName, lastName, password } = req.body; if (!inviterId || !inviteeId || !firstName || !lastName || !password) { throw new ResponseHelper.ResponseError('Invalid payload', undefined, 500); diff --git a/packages/cli/src/requests.d.ts b/packages/cli/src/requests.d.ts index 67932e4c32b07..05ec2e7bc76ad 100644 --- a/packages/cli/src/requests.d.ts +++ b/packages/cli/src/requests.d.ts @@ -174,11 +174,10 @@ export declare namespace UserRequest { export type Reinvite = AuthenticatedRequest<{ id: string }>; export type Update = AuthenticatedRequest< - {}, + { id: string }, {}, { inviterId: string; - inviteeId: string; firstName: string; lastName: string; password: string; From ba9a7ad8d58b221853b919f257302cbe49615355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 9 Feb 2022 11:41:33 +0100 Subject: [PATCH 03/81] :zap: Refactor initTestServer arg --- .../test/integration/auth.endpoints.test.ts | 2 +- .../test/integration/auth.middleware.test.ts | 8 ++-- .../cli/test/integration/me.endpoints.test.ts | 6 +-- .../test/integration/owner.endpoints.test.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 45 ++++++++++++++----- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 0bff6552d6d02..e964c41909c9a 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -16,7 +16,7 @@ describe('auth endpoints', () => { let app: express.Application; beforeAll(async () => { - app = utils.initTestServer({ auth: true }, { applyAuth: true }); + app = utils.initTestServer({ namespaces: ['auth'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); }); diff --git a/packages/cli/test/integration/auth.middleware.test.ts b/packages/cli/test/integration/auth.middleware.test.ts index 77601a475787e..0e193f596079c 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.middleware.test.ts @@ -7,14 +7,12 @@ import * as utils from './shared/utils'; describe('/me endpoints', () => { let app: express.Application; - const meRoutes = ['GET /me', 'PATCH /me', 'PATCH /me/password', 'POST /me/survey']; - beforeAll(async () => { - app = utils.initTestServer({}, { applyAuth: true }); + app = utils.initTestServer({ applyAuth: true }); }); describe('Unauthorized requests', () => { - meRoutes.forEach((route) => { + ['GET /me', 'PATCH /me', 'PATCH /me/password', 'POST /me/survey'].forEach((route) => { const [method, endpoint] = route.split(' ').map((i) => i.toLowerCase()); test(`${route} should return 401 Unauthorized`, async () => { @@ -30,7 +28,7 @@ describe('/owner endpoint', () => { let app: express.Application; beforeAll(async () => { - app = utils.initTestServer({}, { applyAuth: true }); + app = utils.initTestServer({ applyAuth: true }); }); describe('Unauthorized requests', () => { diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 929d2275e5141..d00d23c24fc69 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -15,7 +15,7 @@ describe('/me endpoints', () => { let app: express.Application; beforeAll(async () => { - app = utils.initTestServer({ me: true }, { applyAuth: true }); + app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); }); @@ -161,7 +161,7 @@ describe('/me endpoints', () => { let app: express.Application; beforeAll(async () => { - app = utils.initTestServer({ me: true }, { applyAuth: true }); + app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); }); @@ -320,7 +320,7 @@ describe('/me endpoints', () => { let app: express.Application; beforeAll(async () => { - app = utils.initTestServer({ me: true }, { applyAuth: true }); + app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); }); diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index e6e31120fee50..274682924b846 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -12,7 +12,7 @@ describe('/owner endpoints', () => { let app: express.Application; beforeAll(async () => { - app = utils.initTestServer({ owner: true }, { applyAuth: true }); + app = utils.initTestServer({ namespaces: ['owner'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); }); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index ccd3a9f544b70..7620f0401dc22 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -9,7 +9,11 @@ import config = require('../../../config'); import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes'; import { Db } from '../../../src'; -import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, User } from '../../../src/databases/entities/User'; +import { + MAX_PASSWORD_LENGTH, + MIN_PASSWORD_LENGTH, + User, +} from '../../../src/databases/entities/User'; import { meNamespace as meEndpoints } from '../../../src/UserManagement/routes/me'; import { usersNamespace as usersEndpoints } from '../../../src/UserManagement/routes/users'; import { authenticationMethods as authEndpoints } from '../../../src/UserManagement/routes/auth'; @@ -21,14 +25,23 @@ export const isTestRun = process.argv[1].split('/').includes('jest'); const POPULAR_TOP_LEVEL_DOMAINS = ['com', 'org', 'net', 'io', 'edu']; +type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner'; + /** - * Initialize a test server to make requests from, - * passing in endpoints to enable in the test server. + * Initialize a test server to make requests to. + * + * @param namespaces Namespaces of endpoints to apply to the test server. + * @param applyAuth Whether to apply auth middleware to the test server. */ -export const initTestServer = ( - endpointNamespaces: { [K in 'me' | 'users' | 'auth' | 'owner']?: true } = {}, - { applyAuth } = { applyAuth: false }, -) => { +export function initTestServer( + { + applyAuth, + namespaces, + }: { + applyAuth: boolean; + namespaces?: EndpointNamespace[]; + } = { applyAuth: false }, +) { const testServer = { app: express(), restEndpoint: REST_PATH_SEGMENT, @@ -44,13 +57,21 @@ export const initTestServer = ( authMiddleware.apply(testServer, [AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT]); } - if (endpointNamespaces.me) meEndpoints.apply(testServer); - if (endpointNamespaces.users) usersEndpoints.apply(testServer); - if (endpointNamespaces.auth) authEndpoints.apply(testServer); - if (endpointNamespaces.owner) ownerEndpoints.apply(testServer); + const map = { + me: meEndpoints, + users: usersEndpoints, + auth: authEndpoints, + owner: ownerEndpoints, + }; + + if (namespaces) { + for (const namespace of namespaces) { + map[namespace].apply(testServer); + } + } return testServer.app; -}; +} export async function initTestDb() { await Db.init(); From 4d17eb89225512b6681fb135fa93dfe4d7578151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 9 Feb 2022 11:56:45 +0100 Subject: [PATCH 04/81] :pencil2: Specify agent type --- .../test/integration/auth.endpoints.test.ts | 8 +-- .../cli/test/integration/me.endpoints.test.ts | 56 +++++++++---------- .../test/integration/owner.endpoints.test.ts | 8 +-- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index e964c41909c9a..87098e2f52771 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -91,9 +91,9 @@ describe('auth endpoints', () => { test('GET /login should check cookie contents', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const ownerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAuthAgent(app, owner); - const response = await ownerAgent.get('/login'); + const response = await authOwnerAgent.get('/login'); expect(response.statusCode).toBe(200); @@ -110,9 +110,9 @@ describe('auth endpoints', () => { test('GET /logout should log user out', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const ownerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAuthAgent(app, owner); - const response = await ownerAgent.get('/logout'); + const response = await authOwnerAgent.get('/logout'); expect(response.statusCode).toBe(200); expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY); diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index d00d23c24fc69..7ab82a070e4e0 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -41,9 +41,9 @@ describe('/me endpoints', () => { test('GET /me should return sanitized shell', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const shellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAuthAgent(app, shell); - const response = await shellAgent.get('/me'); + const response = await authShellAgent.get('/me'); expect(response.statusCode).toBe(200); @@ -71,10 +71,10 @@ describe('/me endpoints', () => { test('PATCH /me should succeed with valid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const shellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAuthAgent(app, shell); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { - const response = await shellAgent.patch('/me').send(validPayload); + const response = await authShellAgent.patch('/me').send(validPayload); expect(response.statusCode).toBe(200); @@ -103,24 +103,24 @@ describe('/me endpoints', () => { test('PATCH /me should fail with invalid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const shellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAuthAgent(app, shell); for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { - const response = await shellAgent.patch('/me').send(invalidPayload); + const response = await authShellAgent.patch('/me').send(invalidPayload); expect(response.statusCode).toBe(400); } }); test('PATCH /me/password should succeed with valid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const shellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAuthAgent(app, shell); const validPayloads = Array.from({ length: 3 }, () => ({ password: utils.randomValidPassword(), })); for (const validPayload of validPayloads) { - const response = await shellAgent.patch('/me/password').send(validPayload); + const response = await authShellAgent.patch('/me/password').send(validPayload); expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); } @@ -128,7 +128,7 @@ describe('/me endpoints', () => { test('PATCH /me/password should fail with invalid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const shellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAuthAgent(app, shell); const invalidPayloads = [ ...Array.from({ length: 3 }, () => ({ password: utils.randomInvalidPassword() })), @@ -138,19 +138,19 @@ describe('/me endpoints', () => { ]; for (const invalidPayload of invalidPayloads) { - const response = await shellAgent.patch('/me/password').send(invalidPayload); + const response = await authShellAgent.patch('/me/password').send(invalidPayload); expect(response.statusCode).toBe(400); } }); test('POST /me/survey should succeed with valid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const shellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAuthAgent(app, shell); const validPayloads = [SURVEY, {}]; for (const validPayload of validPayloads) { - const response = await shellAgent.post('/me/survey').send(validPayload); + const response = await authShellAgent.post('/me/survey').send(validPayload); expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); } @@ -200,9 +200,9 @@ describe('/me endpoints', () => { test('GET /me should return sanitized member', async () => { const member = await Db.collections.User!.findOneOrFail(); - const memberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAuthAgent(app, member); - const response = await memberAgent.get('/me'); + const response = await authMemberAgent.get('/me'); expect(response.statusCode).toBe(200); @@ -230,10 +230,10 @@ describe('/me endpoints', () => { test('PATCH /me should succeed with valid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const memberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAuthAgent(app, member); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { - const response = await memberAgent.patch('/me').send(validPayload); + const response = await authMemberAgent.patch('/me').send(validPayload); expect(response.statusCode).toBe(200); @@ -262,24 +262,24 @@ describe('/me endpoints', () => { test('PATCH /me should fail with invalid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const memberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAuthAgent(app, member); for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { - const response = await memberAgent.patch('/me').send(invalidPayload); + const response = await authMemberAgent.patch('/me').send(invalidPayload); expect(response.statusCode).toBe(400); } }); test('PATCH /me/password should succeed with valid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const memberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAuthAgent(app, member); const validPayloads = Array.from({ length: 3 }, () => ({ password: utils.randomValidPassword(), })); for (const validPayload of validPayloads) { - const response = await memberAgent.patch('/me/password').send(validPayload); + const response = await authMemberAgent.patch('/me/password').send(validPayload); expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); } @@ -287,7 +287,7 @@ describe('/me endpoints', () => { test('PATCH /me/password should fail with invalid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const memberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAuthAgent(app, member); const invalidPayloads = [ ...Array.from({ length: 3 }, () => ({ password: utils.randomInvalidPassword() })), @@ -297,19 +297,19 @@ describe('/me endpoints', () => { ]; for (const invalidPayload of invalidPayloads) { - const response = await memberAgent.patch('/me/password').send(invalidPayload); + const response = await authMemberAgent.patch('/me/password').send(invalidPayload); expect(response.statusCode).toBe(400); } }); test('POST /me/survey should succeed with valid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const memberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAuthAgent(app, member); const validPayloads = [SURVEY, {}]; for (const validPayload of validPayloads) { - const response = await memberAgent.post('/me/survey').send(validPayload); + const response = await authMemberAgent.post('/me/survey').send(validPayload); expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); } @@ -359,9 +359,9 @@ describe('/me endpoints', () => { test('GET /me should return sanitized owner', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const ownerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAuthAgent(app, owner); - const response = await ownerAgent.get('/me'); + const response = await authOwnerAgent.get('/me'); expect(response.statusCode).toBe(200); @@ -389,10 +389,10 @@ describe('/me endpoints', () => { test('PATCH /me should succeed with valid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const ownerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAuthAgent(app, owner); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { - const response = await ownerAgent.patch('/me').send(validPayload); + const response = await authOwnerAgent.patch('/me').send(validPayload); expect(response.statusCode).toBe(200); diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 274682924b846..6cb6298c73828 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -38,9 +38,9 @@ describe('/owner endpoints', () => { test('POST /owner should create owner and enable hasOwner setting', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const shellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAuthAgent(app, shell); - const response = await shellAgent.post('/owner').send(TEST_USER); + const response = await authShellAgent.post('/owner').send(TEST_USER); expect(response.statusCode).toBe(200); @@ -77,10 +77,10 @@ describe('/owner endpoints', () => { test('POST /owner should fail with invalid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const shellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAuthAgent(app, shell); for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) { - const response = await shellAgent.post('/owner').send(invalidPayload); + const response = await authShellAgent.post('/owner').send(invalidPayload); expect(response.statusCode).toBe(400); } }); From b251fe044735147d59261b690703df5355828057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 9 Feb 2022 11:59:05 +0100 Subject: [PATCH 05/81] :pencil2: Specify role type --- packages/cli/test/integration/auth.endpoints.test.ts | 4 ++-- packages/cli/test/integration/me.endpoints.test.ts | 12 ++++++------ .../cli/test/integration/owner.endpoints.test.ts | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 87098e2f52771..992734b267742 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -22,7 +22,7 @@ describe('auth endpoints', () => { }); beforeEach(async () => { - const role = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); const newOwner = new User(); @@ -32,7 +32,7 @@ describe('auth endpoints', () => { firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, password: hashSync(TEST_USER.password, genSaltSync(10)), - globalRole: role, + globalRole: ownerRole, }); await Db.collections.User!.save(newOwner); diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 7ab82a070e4e0..ec049b3b6e0bb 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -21,13 +21,13 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const role = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); await Db.collections.User!.save({ id: uuid(), createdAt: new Date(), updatedAt: new Date(), - globalRole: role, + globalRole: ownerRole, }); }); @@ -167,7 +167,7 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const role = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); const newMember = new User(); @@ -177,7 +177,7 @@ describe('/me endpoints', () => { firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, password: hashSync(utils.randomValidPassword(), genSaltSync(10)), - globalRole: role, + globalRole: memberRole, }); await Db.collections.User!.save(newMember); @@ -326,7 +326,7 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const role = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); const newOwner = new User(); @@ -336,7 +336,7 @@ describe('/me endpoints', () => { firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, password: hashSync(utils.randomValidPassword(), genSaltSync(10)), - globalRole: role, + globalRole: ownerRole, }); await Db.collections.User!.save(newOwner); diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 6cb6298c73828..d3103f3b888f5 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -18,13 +18,13 @@ describe('/owner endpoints', () => { }); beforeEach(async () => { - const role = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); await Db.collections.User!.save({ id: uuid(), createdAt: new Date(), updatedAt: new Date(), - globalRole: role, + globalRole: ownerRole, }); }); From a7b74dba370e26949b647d21546370518c7a573b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 9 Feb 2022 12:20:11 +0100 Subject: [PATCH 06/81] :zap: Tighten `/users/:id` check --- packages/cli/src/UserManagement/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/UserManagement/routes/index.ts b/packages/cli/src/UserManagement/routes/index.ts index eb111332371a4..3ac0937bbe306 100644 --- a/packages/cli/src/UserManagement/routes/index.ts +++ b/packages/cli/src/UserManagement/routes/index.ts @@ -65,7 +65,7 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint req.url.startsWith('/fonts/') || req.url.startsWith(`/${restEndpoint}/settings`) || req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) || - new RegExp(`/${restEndpoint}/users/(\\d)+`).test(req.url) + (req.method === 'POST' && new RegExp(`/${restEndpoint}/users/(\\d)+`).test(req.url)) ) { return next(); } From ccea112e1f9f63ed6b662a373d25c9aa7c888bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 9 Feb 2022 13:16:27 +0100 Subject: [PATCH 07/81] :sparkles: Add initial tests --- .../cli/src/UserManagement/routes/users.ts | 6 +- .../test/integration/users.endpoints.test.ts | 254 ++++++++++++++++++ 2 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 packages/cli/test/integration/users.endpoints.test.ts diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 00ebf6121d8f4..03a2c19a33ae9 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -101,19 +101,19 @@ export function usersNamespace(this: N8nApp): void { const { inviterId, inviteeId } = req.query; if (!inviterId || !inviteeId) { - throw new ResponseHelper.ResponseError('Invalid payload', undefined, 500); + throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } const users = await Db.collections.User!.find({ where: { id: In([inviterId, inviteeId]) } }); if (users.length !== 2) { - throw new ResponseHelper.ResponseError('Invalid invite URL', undefined, 500); + throw new ResponseHelper.ResponseError('Invalid invite URL', undefined, 400); } const inviter = users.find((user) => user.id === inviterId); if (!inviter || !inviter.email || !inviter.firstName) { - throw new ResponseHelper.ResponseError('Invalid request', undefined, 500); + throw new ResponseHelper.ResponseError('Invalid invite URL', undefined, 400); } const { firstName, lastName } = inviter; diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts new file mode 100644 index 0000000000000..9f1072d0e5425 --- /dev/null +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -0,0 +1,254 @@ +import express = require('express'); +import { getConnection } from 'typeorm'; +import validator from 'validator'; +import { v4 as uuid } from 'uuid'; + +import * as utils from './shared/utils'; +import { Db } from '../../src'; +import config = require('../../config'); +import { SUCCESS_RESPONSE_BODY } from './shared/constants'; + +let app: express.Application; + +beforeAll(async () => { + app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); + await utils.initTestDb(); + await utils.truncateUserTable(); +}); + +beforeEach(async () => { + const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + + await Db.collections.User!.save({ + 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, + createdAt: new Date(), + updatedAt: new Date(), + globalRole: ownerRole, + }); + + config.set('userManagement.hasOwner', true); +}); + +afterEach(async () => { + await utils.truncateUserTable(); +}); + +afterAll(() => { + return getConnection().close(); +}); + +test('GET /users should return all users', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const response = await authOwnerAgent.get('/users'); + + expect(response.statusCode).toBe(200); + + for (const user of response.body.data) { + const { + id, + email, + firstName, + lastName, + personalizationAnswers, + globalRole, + password, + resetPasswordToken, + } = user; + + expect(validator.isUUID(id)).toBe(true); + expect(email).toBeDefined(); + expect(firstName).toBeDefined(); + expect(lastName).toBeDefined(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(globalRole).toBeUndefined(); + } +}); + +test('DELETE /users/:id should delete the user', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const { id: idToDelete } = await Db.collections.User!.save({ + id: uuid(), + email: utils.randomEmail(), + password: utils.randomValidPassword(), + firstName: utils.randomName(), + lastName: utils.randomName(), + createdAt: new Date(), + updatedAt: new Date(), + globalRole: memberRole, + }); + + const response = await authOwnerAgent.delete(`/users/${idToDelete}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); +}); + +test('DELETE /users/:id should fail to delete self', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const response = await authOwnerAgent.delete(`/users/${owner.id}`); + + expect(response.statusCode).toBe(400); +}); + +test('DELETE /users/:id should fail if deletee equals transferee', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const { id: idToDelete } = await Db.collections.User!.save({ + id: uuid(), + email: utils.randomEmail(), + password: utils.randomValidPassword(), + firstName: utils.randomName(), + lastName: utils.randomName(), + createdAt: new Date(), + updatedAt: new Date(), + globalRole: memberRole, + }); + + const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({ + transferId: idToDelete, + }); + + expect(response.statusCode).toBe(400); +}); + +test('DELETE /users/:id with transferId should perform transfer', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const workflowOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'workflow', + }); + + const userToDelete = await Db.collections.User!.save({ + id: uuid(), + email: utils.randomEmail(), + password: utils.randomValidPassword(), + firstName: utils.randomName(), + lastName: utils.randomName(), + createdAt: new Date(), + updatedAt: new Date(), + globalRole: workflowOwnerRole, + }); + + const savedWorkflow = await Db.collections.Workflow!.save({ + name: utils.randomName(), + active: false, + connections: {}, + }); + + await Db.collections.SharedWorkflow!.save({ + role: workflowOwnerRole, + user: userToDelete, + workflow: savedWorkflow, + }); + + const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`).query({ + transferId: owner.id, + }); + + expect(response.statusCode).toBe(200); + + const shared = await Db.collections.SharedWorkflow!.findOneOrFail({ relations: ['user'] }); + + expect(shared.user.id).toBe(owner.id); +}); + +test('GET /resolve-signup-token should validate invite token', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const { id: inviteeId } = await Db.collections.User!.save({ + id: uuid(), + email: utils.randomEmail(), + password: utils.randomValidPassword(), + firstName: utils.randomName(), + lastName: utils.randomName(), + createdAt: new Date(), + updatedAt: new Date(), + globalRole: memberRole, + }); + + const response = await authOwnerAgent + .get('/resolve-signup-token') + .query({ inviterId: INITIAL_TEST_USER.id }) + .query({ inviteeId }); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ + data: { + inviter: { + firstName: INITIAL_TEST_USER.firstName, + lastName: INITIAL_TEST_USER.lastName, + }, + }, + }); +}); + +test('GET /resolve-signup-token should fail with invalid inputs', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const { id: inviteeId } = await Db.collections.User!.save({ + id: uuid(), + email: utils.randomEmail(), + password: utils.randomValidPassword(), + firstName: utils.randomName(), + lastName: utils.randomName(), + createdAt: new Date(), + updatedAt: new Date(), + globalRole: memberRole, + }); + + const firstResponse = await authOwnerAgent + .get('/resolve-signup-token') + .query({ inviterId: INITIAL_TEST_USER.id }); + + expect(firstResponse.statusCode).toBe(400); + + const secondResponse = await authOwnerAgent + .get('/resolve-signup-token') + .query({ inviteeId }); + + expect(secondResponse.statusCode).toBe(400); + + const thirdResponse = await authOwnerAgent + .get('/resolve-signup-token') + .query({ inviterId: '123', inviteeId: '456' }); + + expect(thirdResponse.statusCode).toBe(400); + + await Db.collections.User!.update(owner.id, { email: '' }); + + const fourthResponse = await authOwnerAgent + .get('/resolve-signup-token') + .query({ inviterId: INITIAL_TEST_USER.id }) + .query({ inviteeId }); + + expect(fourthResponse.statusCode).toBe(400); +}); + +const INITIAL_TEST_USER = { + id: uuid(), + email: utils.randomEmail(), + firstName: utils.randomName(), + lastName: utils.randomName(), + password: utils.randomValidPassword(), +}; From 381f111e7195a1a54ebd3d00a4f91b8a0a6431ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 9 Feb 2022 13:18:51 +0100 Subject: [PATCH 08/81] :truck: Reposition init server map --- packages/cli/test/integration/shared/utils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 7620f0401dc22..5887bb5a820fb 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -57,14 +57,14 @@ export function initTestServer( authMiddleware.apply(testServer, [AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT]); } - const map = { - me: meEndpoints, - users: usersEndpoints, - auth: authEndpoints, - owner: ownerEndpoints, - }; - if (namespaces) { + const map = { + me: meEndpoints, + users: usersEndpoints, + auth: authEndpoints, + owner: ownerEndpoints, + }; + for (const namespace of namespaces) { map[namespace].apply(testServer); } From 27d2393b17d0c4f761bb9d327dda05fa6c83111e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:40:37 +0100 Subject: [PATCH 09/81] :zap: Set constants in `validatePassword()` --- packages/cli/src/UserManagement/UserManagementHelper.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 966120af99a99..5c4976c38ab39 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -4,7 +4,7 @@ import { IsNull, Not, QueryFailedError } from 'typeorm'; import { Db, GenericHelpers, ResponseHelper } from '..'; import config = require('../../config'); -import { User } from '../databases/entities/User'; +import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, User } from '../databases/entities/User'; import { PublicUser } from './Interfaces'; export const isEmailSetUp = Boolean(config.get('userManagement.emails.mode')); @@ -28,12 +28,13 @@ export async function isInstanceOwnerSetup(): Promise { return users.length !== 0; } +// TODO: Enforce at model level export function validatePassword(password?: string): string { if (!password) { throw new ResponseHelper.ResponseError('Password is mandatory', undefined, 400); } - if (password.length < 8 || password.length > 64) { + if (password.length < MIN_PASSWORD_LENGTH || password.length > MAX_PASSWORD_LENGTH) { throw new ResponseHelper.ResponseError( 'Password must be 8 to 64 characters long', undefined, From 4d613e7a67be2427d0537a2d9ecf50f84ae71bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:41:12 +0100 Subject: [PATCH 10/81] :zap: Tighten `/users/:id` check --- packages/cli/src/UserManagement/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/UserManagement/routes/index.ts b/packages/cli/src/UserManagement/routes/index.ts index 3ac0937bbe306..053fd1f3b3e77 100644 --- a/packages/cli/src/UserManagement/routes/index.ts +++ b/packages/cli/src/UserManagement/routes/index.ts @@ -65,7 +65,7 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint req.url.startsWith('/fonts/') || req.url.startsWith(`/${restEndpoint}/settings`) || req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) || - (req.method === 'POST' && new RegExp(`/${restEndpoint}/users/(\\d)+`).test(req.url)) + (req.method === 'POST' && new RegExp(`/${restEndpoint}/users/[\\w\\d-]*`).test(req.url)) ) { return next(); } From 9b55b2e2ddd26a2d94651356102bfdf7046e2cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:41:38 +0100 Subject: [PATCH 11/81] :zap: Improve checks in `/users/:id` --- .../cli/src/UserManagement/routes/users.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 03a2c19a33ae9..cc2bbba94fabd 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -13,6 +13,7 @@ import { isEmailSetUp, isFailedQuery, sanitizeUser, + validatePassword, } from '../UserManagementHelper'; import { User } from '../../databases/entities/User'; import { SharedWorkflow } from '../../databases/entities/SharedWorkflow'; @@ -124,47 +125,43 @@ export function usersNamespace(this: N8nApp): void { /** * Fill out user shell with first name, last name, and password. + * + * Authless endpoint. */ this.app.post( `/${this.restEndpoint}/users/:id`, ResponseHelper.send(async (req: UserRequest.Update, res: Response) => { - if (req.user) { - throw new ResponseHelper.ResponseError( - 'Please log out before accepting another invite', - undefined, - 500, - ); - } - const { id: inviteeId } = req.params; const { inviterId, firstName, lastName, password } = req.body; if (!inviterId || !inviteeId || !firstName || !lastName || !password) { - throw new ResponseHelper.ResponseError('Invalid payload', undefined, 500); + throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } + const validPassword = validatePassword(password); + const users = await Db.collections.User!.find({ where: { id: In([inviterId, inviteeId]) }, }); if (users.length !== 2) { - throw new ResponseHelper.ResponseError('Invalid invite URL', undefined, 500); + throw new ResponseHelper.ResponseError('Invalid payload or URL', undefined, 400); } - const invitee = users.find((user) => user.id === inviteeId); + const invitee = users.find((user) => user.id === inviteeId) as User; - if (!invitee || invitee.password) { + if (invitee.password) { throw new ResponseHelper.ResponseError( 'This invite has been accepted already', undefined, - 500, + 400, ); } invitee.firstName = firstName; invitee.lastName = lastName; - invitee.password = hashSync(password, genSaltSync(10)); + invitee.password = hashSync(validPassword, genSaltSync(10)); const updatedUser = await Db.collections.User!.save(invitee); From 3e65857d6c915c2fa11a4aaaf06ae10e70baf658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:42:09 +0100 Subject: [PATCH 12/81] :sparkles: Add tests for `/users/:id` --- packages/cli/test/integration/shared/utils.ts | 7 +- .../test/integration/users.endpoints.test.ts | 140 ++++++++++++++++-- 2 files changed, 131 insertions(+), 16 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 5887bb5a820fb..3206764a5f3fc 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -94,7 +94,7 @@ export async function createAuthAgent(app: express.Application, user: User) { return agent; } -export async function createAgent(app: express.Application, user: User) { +export async function createAuthlessAgent(app: express.Application) { const agent = request.agent(app); agent.use(prefix(REST_PATH_SEGMENT)); @@ -136,6 +136,11 @@ export async function getHasOwnerSetting() { */ export function getAuthToken(response: request.Response, authCookieName = 'n8n-auth') { const cookies: string[] = response.headers['set-cookie']; + + if (!cookies) { + throw new Error('No \'set-cookie\' header found in response'); + } + const authCookie = cookies.find((c) => c.startsWith(`${authCookieName}=`)); if (!authCookie) return undefined; diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 9f1072d0e5425..2c57172212248 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -103,7 +103,7 @@ test('DELETE /users/:id should fail to delete self', async () => { expect(response.statusCode).toBe(400); }); -test('DELETE /users/:id should fail if deletee equals transferee', async () => { +test('DELETE /users/:id should fail if deleted equals transferee', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); @@ -217,32 +217,113 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { globalRole: memberRole, }); - const firstResponse = await authOwnerAgent + const first = await authOwnerAgent .get('/resolve-signup-token') .query({ inviterId: INITIAL_TEST_USER.id }); - expect(firstResponse.statusCode).toBe(400); + const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId }); - const secondResponse = await authOwnerAgent - .get('/resolve-signup-token') - .query({ inviteeId }); - - expect(secondResponse.statusCode).toBe(400); - - const thirdResponse = await authOwnerAgent + const third = await authOwnerAgent .get('/resolve-signup-token') .query({ inviterId: '123', inviteeId: '456' }); - expect(thirdResponse.statusCode).toBe(400); + await Db.collections.User!.update(owner.id, { email: '' }); // cause inconsistent DB state - await Db.collections.User!.update(owner.id, { email: '' }); - - const fourthResponse = await authOwnerAgent + const fourth = await authOwnerAgent .get('/resolve-signup-token') .query({ inviterId: INITIAL_TEST_USER.id }) .query({ inviteeId }); - expect(fourthResponse.statusCode).toBe(400); + for (const response of [first, second, third, fourth]) { + expect(response.statusCode).toBe(400); + } +}); + +test('POST /users/:id should fill out a user shell', async () => { + const authlessAgent = await utils.createAuthlessAgent(app); + + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); + + const userToFillOut = await Db.collections.User!.save({ + email: utils.randomEmail(), + globalRole: globalMemberRole, + }); + + const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send({ + inviterId: INITIAL_TEST_USER.id, + firstName: utils.randomName(), + lastName: utils.randomName(), + password: utils.randomValidPassword(), + }); + + const { + id, + email, + firstName, + lastName, + personalizationAnswers, + password, + resetPasswordToken, + globalRole, + } = response.body.data; + + expect(validator.isUUID(id)).toBe(true); + expect(email).toBeDefined(); + expect(firstName).toBeDefined(); + expect(lastName).toBeDefined(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(globalRole).toBeUndefined(); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); +}); + +test('POST /users/:id should fail with invalid inputs', async () => { + const authlessAgent = await utils.createAuthlessAgent(app); + + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); + + const userToFillOut = await Db.collections.User!.save({ + email: utils.randomEmail(), + globalRole: globalMemberRole, + }); + + for (const invalidPayload of INVALID_FILL_OUT_USER_PAYLOADS) { + const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send(invalidPayload); + expect(response.statusCode).toBe(400); + } +}); + +test('POST /users/:id should fail with already accepted invite', async () => { + const authlessAgent = await utils.createAuthlessAgent(app); + + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); + + const userToFillOut = await Db.collections.User!.save({ + email: utils.randomEmail(), + password: utils.randomValidPassword(), // simulate accepted invite + globalRole: globalMemberRole, + }); + + const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send({ + inviterId: INITIAL_TEST_USER.id, + firstName: utils.randomName(), + lastName: utils.randomName(), + password: utils.randomValidPassword(), + }); + + expect(response.statusCode).toBe(400); }); const INITIAL_TEST_USER = { @@ -252,3 +333,32 @@ const INITIAL_TEST_USER = { lastName: utils.randomName(), password: utils.randomValidPassword(), }; + +const INVALID_FILL_OUT_USER_PAYLOADS = [ + { + firstName: utils.randomName(), + lastName: utils.randomName(), + password: utils.randomValidPassword(), + }, + { + inviterId: INITIAL_TEST_USER.id, + firstName: utils.randomName(), + password: utils.randomValidPassword(), + }, + { + inviterId: INITIAL_TEST_USER.id, + firstName: utils.randomName(), + password: utils.randomValidPassword(), + }, + { + inviterId: INITIAL_TEST_USER.id, + firstName: utils.randomName(), + lastName: utils.randomName(), + }, + { + inviterId: INITIAL_TEST_USER.id, + firstName: utils.randomName(), + lastName: utils.randomName(), + password: utils.randomInvalidPassword(), + }, +]; From 03cba0ea982e51048668cf7862c90f6b01956ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:42:19 +0100 Subject: [PATCH 13/81] :package: Update package-lock.json --- package-lock.json | 277 ++++++++++++++++++++++++++-------------------- 1 file changed, 159 insertions(+), 118 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6c332744b94e..394b8b13a64fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13734,6 +13734,15 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -13769,6 +13778,21 @@ } } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -13831,6 +13855,58 @@ "worker-rpc": "^0.1.0" } }, + "fork-ts-checker-webpack-plugin-v5": { + "version": "npm:fork-ts-checker-webpack-plugin@5.2.1", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", + "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==", + "optional": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "optional": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -13865,6 +13941,12 @@ "slash": "^2.0.0" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -13893,6 +13975,16 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -13928,6 +14020,17 @@ } } }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "optional": true, + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -13938,6 +14041,15 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", @@ -14031,6 +14143,12 @@ "requires": { "tslib": "^1.8.1" } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "optional": true } } }, @@ -18035,6 +18153,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" }, + "clamp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", + "integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ=" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -23521,124 +23644,6 @@ } } }, - "fork-ts-checker-webpack-plugin-v5": { - "version": "npm:fork-ts-checker-webpack-plugin@5.2.1", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", - "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==", - "optional": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "optional": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "optional": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "optional": true - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "optional": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "optional": true - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "optional": true, - "requires": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "optional": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "optional": true - } - } - }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -32810,6 +32815,11 @@ "lodash._reinterpolate": "^3.0.0" } }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" + }, "lodash.transform": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", @@ -33320,6 +33330,11 @@ } } }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -41646,6 +41661,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tinycolor2": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", + "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" + }, "title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", @@ -43270,6 +43290,17 @@ "webpack-bundle-analyzer": "^3.6.0" } }, + "vue-color": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.8.1.tgz", + "integrity": "sha512-BoLCEHisXi2QgwlhZBg9UepvzZZmi4176vbr+31Shen5WWZwSLVgdScEPcB+yrAtuHAz42309C0A4+WiL9lNBw==", + "requires": { + "clamp": "^1.0.1", + "lodash.throttle": "^4.0.0", + "material-colors": "^1.0.0", + "tinycolor2": "^1.1.2" + } + }, "vue-docgen-api": { "version": "4.44.15", "resolved": "https://registry.npmjs.org/vue-docgen-api/-/vue-docgen-api-4.44.15.tgz", @@ -43451,6 +43482,16 @@ "resolved": "https://registry.npmjs.org/vue-typed-mixins/-/vue-typed-mixins-0.2.0.tgz", "integrity": "sha512-0OxuinandPWv3nm5k/reYkuKtX3jjPZ40Sy9roJz0ih8PUzmI7zSRiXFEJ62LsyRegw9Tqy+qMkajk7ipKP8Vg==" }, + "vue2-boring-avatars": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/vue2-boring-avatars/-/vue2-boring-avatars-0.3.6.tgz", + "integrity": "sha512-soUoArS5H7pIb8vC0bS9zKatC0FKdF6qj/p3EjSizXTm2XeWkl5ZtjWh66yN7ci8LU7Vxsd+BiO+v3FBHACSSg==", + "requires": { + "core-js": "^3.6.5", + "vue": "^2.6.11", + "vue-color": "^2.8.1" + } + }, "vue2-touch-events": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/vue2-touch-events/-/vue2-touch-events-3.2.2.tgz", From 9d6a47e6df22ca2952883d4475f845fb26fd5196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:43:06 +0100 Subject: [PATCH 14/81] :zap: Simplify expectation --- packages/cli/test/integration/auth.endpoints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 992734b267742..609064f48ce35 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -86,7 +86,7 @@ describe('auth endpoints', () => { expect(globalRole).toBeUndefined(); const authToken = utils.getAuthToken(response); - expect(authToken).not.toBeUndefined(); + expect(authToken).toBeDefined(); }); test('GET /login should check cookie contents', async () => { From 1e0b94f66596ddb966103b458d0fa3e9c8611dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:44:42 +0100 Subject: [PATCH 15/81] :zap: Reuse util for authless agent --- packages/cli/test/integration/auth.endpoints.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 609064f48ce35..2cfc132d08275 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -54,10 +54,9 @@ describe('auth endpoints', () => { }); test('POST /login should log user in', async () => { - const cookieLessAgent = request.agent(app); - cookieLessAgent.use(utils.prefix(REST_PATH_SEGMENT)); + const authlessAgent = await utils.createAuthlessAgent(app); - const response = await cookieLessAgent.post('/login').send({ + const response = await authlessAgent.post('/login').send({ email: TEST_USER.email, password: TEST_USER.password, }); From 218f0854efaf6ec47530128e849fc5814ba13989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:46:24 +0100 Subject: [PATCH 16/81] :truck: Make role names consistent --- .../test/integration/auth.endpoints.test.ts | 4 ++-- .../cli/test/integration/me.endpoints.test.ts | 12 +++++------ .../test/integration/owner.endpoints.test.ts | 4 ++-- .../test/integration/users.endpoints.test.ts | 20 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 2cfc132d08275..e966baf331141 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -22,7 +22,7 @@ describe('auth endpoints', () => { }); beforeEach(async () => { - const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); const newOwner = new User(); @@ -32,7 +32,7 @@ describe('auth endpoints', () => { firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, password: hashSync(TEST_USER.password, genSaltSync(10)), - globalRole: ownerRole, + globalRole: globalOwnerRole, }); await Db.collections.User!.save(newOwner); diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index ec049b3b6e0bb..f4ba944551980 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -21,13 +21,13 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); await Db.collections.User!.save({ id: uuid(), createdAt: new Date(), updatedAt: new Date(), - globalRole: ownerRole, + globalRole: globalOwnerRole, }); }); @@ -167,7 +167,7 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); const newMember = new User(); @@ -177,7 +177,7 @@ describe('/me endpoints', () => { firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, password: hashSync(utils.randomValidPassword(), genSaltSync(10)), - globalRole: memberRole, + globalRole: globalMemberRole, }); await Db.collections.User!.save(newMember); @@ -326,7 +326,7 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); const newOwner = new User(); @@ -336,7 +336,7 @@ describe('/me endpoints', () => { firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, password: hashSync(utils.randomValidPassword(), genSaltSync(10)), - globalRole: ownerRole, + globalRole: globalOwnerRole, }); await Db.collections.User!.save(newOwner); diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index d3103f3b888f5..7d1dc027f9800 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -18,13 +18,13 @@ describe('/owner endpoints', () => { }); beforeEach(async () => { - const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); await Db.collections.User!.save({ id: uuid(), createdAt: new Date(), updatedAt: new Date(), - globalRole: ownerRole, + globalRole: globalOwnerRole, }); }); diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 2c57172212248..416242a617416 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -17,7 +17,7 @@ beforeAll(async () => { }); beforeEach(async () => { - const ownerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); await Db.collections.User!.save({ id: INITIAL_TEST_USER.id, @@ -27,7 +27,7 @@ beforeEach(async () => { lastName: INITIAL_TEST_USER.lastName, createdAt: new Date(), updatedAt: new Date(), - globalRole: ownerRole, + globalRole: globalOwnerRole, }); config.set('userManagement.hasOwner', true); @@ -76,7 +76,7 @@ test('DELETE /users/:id should delete the user', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); const { id: idToDelete } = await Db.collections.User!.save({ id: uuid(), email: utils.randomEmail(), @@ -85,7 +85,7 @@ test('DELETE /users/:id should delete the user', async () => { lastName: utils.randomName(), createdAt: new Date(), updatedAt: new Date(), - globalRole: memberRole, + globalRole: globalMemberRole, }); const response = await authOwnerAgent.delete(`/users/${idToDelete}`); @@ -107,7 +107,7 @@ test('DELETE /users/:id should fail if deleted equals transferee', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); const { id: idToDelete } = await Db.collections.User!.save({ id: uuid(), email: utils.randomEmail(), @@ -116,7 +116,7 @@ test('DELETE /users/:id should fail if deleted equals transferee', async () => { lastName: utils.randomName(), createdAt: new Date(), updatedAt: new Date(), - globalRole: memberRole, + globalRole: globalMemberRole, }); const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({ @@ -173,7 +173,7 @@ test('GET /resolve-signup-token should validate invite token', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); const { id: inviteeId } = await Db.collections.User!.save({ id: uuid(), email: utils.randomEmail(), @@ -182,7 +182,7 @@ test('GET /resolve-signup-token should validate invite token', async () => { lastName: utils.randomName(), createdAt: new Date(), updatedAt: new Date(), - globalRole: memberRole, + globalRole: globalMemberRole, }); const response = await authOwnerAgent @@ -205,7 +205,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const memberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); const { id: inviteeId } = await Db.collections.User!.save({ id: uuid(), email: utils.randomEmail(), @@ -214,7 +214,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { lastName: utils.randomName(), createdAt: new Date(), updatedAt: new Date(), - globalRole: memberRole, + globalRole: globalMemberRole, }); const first = await authOwnerAgent From ce93c666000e35d77069d6b23296858a6781e657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:49:54 +0100 Subject: [PATCH 17/81] :blue_book: Tighten namespaces map type --- packages/cli/test/integration/shared/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 3206764a5f3fc..c68fb1c7143e9 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -20,6 +20,7 @@ import { authenticationMethods as authEndpoints } from '../../../src/UserManagem import { ownerNamespace as ownerEndpoints } from '../../../src/UserManagement/routes/owner'; import { getConnection } from 'typeorm'; import { issueJWT } from '../../../src/UserManagement/auth/jwt'; +import { N8nApp } from '../../../src/UserManagement/Interfaces'; export const isTestRun = process.argv[1].split('/').includes('jest'); @@ -58,7 +59,7 @@ export function initTestServer( } if (namespaces) { - const map = { + const map: Readonly void>> = { me: meEndpoints, users: usersEndpoints, auth: authEndpoints, @@ -138,7 +139,7 @@ export function getAuthToken(response: request.Response, authCookieName = 'n8n-a const cookies: string[] = response.headers['set-cookie']; if (!cookies) { - throw new Error('No \'set-cookie\' header found in response'); + throw new Error("No 'set-cookie' header found in response"); } const authCookie = cookies.find((c) => c.startsWith(`${authCookieName}=`)); From 0894f6e7c05f2bfaf77b40066329d96e94f14bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 12:50:38 +0100 Subject: [PATCH 18/81] :fire: Remove unneeded default arg --- packages/cli/test/integration/shared/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index c68fb1c7143e9..99ebc4137f349 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -41,7 +41,7 @@ export function initTestServer( }: { applyAuth: boolean; namespaces?: EndpointNamespace[]; - } = { applyAuth: false }, + }, ) { const testServer = { app: express(), From 2279efd3ab2e67274ef26c1d3410539641ed2a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 19:59:49 +0100 Subject: [PATCH 19/81] :sparkles: Add tests for `POST /users` --- .../UserManagement/UserManagementHelper.ts | 5 + .../cli/src/UserManagement/routes/users.ts | 15 +- packages/cli/test/integration/shared/utils.ts | 8 + .../test/integration/users.endpoints.test.ts | 144 +++++++++++++++++- 4 files changed, 166 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 5c4976c38ab39..3175eb3a66005 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -7,6 +7,11 @@ import config = require('../../config'); import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, User } from '../databases/entities/User'; import { PublicUser } from './Interfaces'; +// TODO: Find permanent solution +if (process.argv[1].split('/').includes('jest')) { + config.set('userManagement.emails.mode', 'smtp'); +} + export const isEmailSetUp = Boolean(config.get('userManagement.emails.mode')); export function getInstanceDomain(): string { diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index cc2bbba94fabd..f83edcdf1411a 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -40,6 +40,15 @@ export function usersNamespace(this: N8nApp): void { throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } + if (!req.body.length) return []; + + // eslint-disable-next-line no-restricted-syntax + for (const invite of req.body) { + if (typeof invite !== 'object' || !invite.email) { + throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); + } + } + const invites = req.body; invites.forEach(({ email }) => { @@ -84,7 +93,11 @@ export function usersNamespace(this: N8nApp): void { }); if (!result.success) { - throw new ResponseHelper.ResponseError(`Email to ${email} could not be sent`); + throw new ResponseHelper.ResponseError( + `Email to ${email} could not be sent. Please recheck your SMTP config.`, + undefined, + 500, + ); } return { id, email }; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 99ebc4137f349..998519df3b413 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -4,6 +4,8 @@ import * as superagent from 'superagent'; import * as request from 'supertest'; import { URL } from 'url'; import bodyParser = require('body-parser'); +import * as util from 'util'; +import { createTestAccount } from 'nodemailer'; import config = require('../../../config'); import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; @@ -21,6 +23,12 @@ import { ownerNamespace as ownerEndpoints } from '../../../src/UserManagement/ro import { getConnection } from 'typeorm'; import { issueJWT } from '../../../src/UserManagement/auth/jwt'; import { N8nApp } from '../../../src/UserManagement/Interfaces'; +import type { SmtpTestAccount } from './types'; + +/** + * Get an SMTP test account from https://ethereal.email to test sending emails. + */ +export const getSmtpTestAccount = util.promisify(createTestAccount); export const isTestRun = process.argv[1].split('/').includes('jest'); diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 416242a617416..7ec2f28c06a10 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -17,7 +17,10 @@ beforeAll(async () => { }); beforeEach(async () => { - const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); await Db.collections.User!.save({ id: INITIAL_TEST_USER.id, @@ -76,7 +79,11 @@ test('DELETE /users/:id should delete the user', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); + const { id: idToDelete } = await Db.collections.User!.save({ id: uuid(), email: utils.randomEmail(), @@ -107,7 +114,11 @@ test('DELETE /users/:id should fail if deleted equals transferee', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); + const { id: idToDelete } = await Db.collections.User!.save({ id: uuid(), email: utils.randomEmail(), @@ -173,7 +184,11 @@ test('GET /resolve-signup-token should validate invite token', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); + const { id: inviteeId } = await Db.collections.User!.save({ id: uuid(), email: utils.randomEmail(), @@ -205,7 +220,11 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); + const { id: inviteeId } = await Db.collections.User!.save({ id: uuid(), email: utils.randomEmail(), @@ -326,6 +345,115 @@ test('POST /users/:id should fail with already accepted invite', async () => { expect(response.statusCode).toBe(400); }); +// test('POST /users should fail if emailing is not set up', async () => { +// const owner = await Db.collections.User!.findOneOrFail(); +// const authOwnerAgent = await utils.createAuthAgent(app, owner); + +// config.set('userManagement.emails.mode', ''); // TODO: This line has no effect + +// const response = await authOwnerAgent.post('/users').send([{ email: utils.randomEmail() }]); + +// expect(response.statusCode).toBe(500); +// }); + + +// test('POST /users should report error due to wrong SMTP config', async () => { +// const owner = await Db.collections.User!.findOneOrFail(); +// const authOwnerAgent = await utils.createAuthAgent(app, owner); + +// config.set('userManagement.emails.mode', 'smtp'); +// config.set('userManagement.emails.smtp.host', 'XYZ'); // TODO: This breaks following test + +// const payload = TEST_EMAILS_TO_CREATE_USER_SHELLS.map((e) => ({ email: e })); + +// const response = await authOwnerAgent.post('/users').send(payload); + +// expect(response.statusCode).toBe(500); +// }); + +test('POST /users should email invites and create user shells', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const { + user, + pass, + smtp: { host, port, secure }, + } = await utils.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); + + const payload = TEST_EMAILS_TO_CREATE_USER_SHELLS.map((e) => ({ email: e })); + + const response = await authOwnerAgent.post('/users').send(payload); + + expect(response.statusCode).toBe(200); + + for (const { id, email: receivedEmail } of response.body.data) { + expect(validator.isUUID(id)).toBe(true); + expect(TEST_EMAILS_TO_CREATE_USER_SHELLS.some((e) => e === receivedEmail)).toBe(true); + + const user = await Db.collections.User!.findOneOrFail(id); + const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } = user; + + expect(firstName).toBeNull(); + expect(lastName).toBeNull(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeNull(); + expect(resetPasswordToken).toBeNull(); + } +}); + +test('POST /users should fail with invalid inputs', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const invalidPayloads = [ + utils.randomEmail(), + [utils.randomEmail()], + {}, + [{ name: utils.randomName() }], + [{ email: utils.randomName() }], + ]; + + for (const invalidPayload of invalidPayloads) { + const response = await authOwnerAgent.post('/users').send(invalidPayload); + expect(response.statusCode).toBe(400); + } +}); + +test('POST /users should error if user email already exists', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const email = utils.randomEmail(); + const role = await Db.collections.Role!.findOneOrFail({ scope: 'global', name: 'member' }); + await Db.collections.User?.save({ email, globalRole: role }); + + const response = await authOwnerAgent.post('/users').send([{ email }]); + + expect(response.statusCode).toBe(400); +}); + +test('POST /users should ignore an empty payload', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const response = await authOwnerAgent.post('/users').send([]); + + const { data } = response.body; + + expect(response.statusCode).toBe(200); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBe(0); +}); + + const INITIAL_TEST_USER = { id: uuid(), email: utils.randomEmail(), @@ -362,3 +490,9 @@ const INVALID_FILL_OUT_USER_PAYLOADS = [ password: utils.randomInvalidPassword(), }, ]; + +const TEST_EMAILS_TO_CREATE_USER_SHELLS = [ + utils.randomEmail(), + utils.randomEmail(), + utils.randomEmail(), +]; From d99c05ab02ca51e236944160f13419694bad32cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 20:00:44 +0100 Subject: [PATCH 20/81] :blue_book: Create test SMTP account type --- packages/cli/test/integration/shared/types.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/cli/test/integration/shared/types.d.ts diff --git a/packages/cli/test/integration/shared/types.d.ts b/packages/cli/test/integration/shared/types.d.ts new file mode 100644 index 0000000000000..cc6a1d43c4534 --- /dev/null +++ b/packages/cli/test/integration/shared/types.d.ts @@ -0,0 +1,9 @@ +export type SmtpTestAccount = { + user: string; + pass: string; + smtp: { + host: string; + port: number; + secure: boolean; + }; +}; From b857097c9cf2e14df9fd1df1ca2672e8ebb06877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 20:02:02 +0100 Subject: [PATCH 21/81] :pencil2: Improve wording --- packages/cli/test/integration/users.endpoints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 7ec2f28c06a10..a2a92117a7916 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -110,7 +110,7 @@ test('DELETE /users/:id should fail to delete self', async () => { expect(response.statusCode).toBe(400); }); -test('DELETE /users/:id should fail if deleted equals transferee', async () => { +test('DELETE /users/:id should fail if user to delete is transferee', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); From 673ee5d48f574412ffcde71efb3361e49e08e974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 10 Feb 2022 20:03:20 +0100 Subject: [PATCH 22/81] :art: Formatting --- packages/cli/test/integration/users.endpoints.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index a2a92117a7916..4dc2499a58e16 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -356,7 +356,6 @@ test('POST /users/:id should fail with already accepted invite', async () => { // expect(response.statusCode).toBe(500); // }); - // test('POST /users should report error due to wrong SMTP config', async () => { // const owner = await Db.collections.User!.findOneOrFail(); // const authOwnerAgent = await utils.createAuthAgent(app, owner); @@ -453,7 +452,6 @@ test('POST /users should ignore an empty payload', async () => { expect(data.length).toBe(0); }); - const INITIAL_TEST_USER = { id: uuid(), email: utils.randomEmail(), From 858ae4209c593ce7fda914eceafc2e13cb42a11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Feb 2022 13:54:09 +0100 Subject: [PATCH 23/81] :fire: Remove temp fix --- packages/cli/src/UserManagement/UserManagementHelper.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 3175eb3a66005..5c4976c38ab39 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -7,11 +7,6 @@ import config = require('../../config'); import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, User } from '../databases/entities/User'; import { PublicUser } from './Interfaces'; -// TODO: Find permanent solution -if (process.argv[1].split('/').includes('jest')) { - config.set('userManagement.emails.mode', 'smtp'); -} - export const isEmailSetUp = Boolean(config.get('userManagement.emails.mode')); export function getInstanceDomain(): string { From 0adc92920318c95ecd93af4903ad1c07060f53f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Feb 2022 13:54:32 +0100 Subject: [PATCH 24/81] :zap: Replace helper with config call --- packages/cli/src/UserManagement/routes/users.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index f83edcdf1411a..4d174d7a443a2 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -20,6 +20,7 @@ import { SharedWorkflow } from '../../databases/entities/SharedWorkflow'; import { SharedCredentials } from '../../databases/entities/SharedCredentials'; import { getInstance } from '../email/UserManagementMailer'; import { issueJWT } from '../auth/jwt'; +import config = require('../../../config'); export function usersNamespace(this: N8nApp): void { /** @@ -28,7 +29,7 @@ export function usersNamespace(this: N8nApp): void { this.app.post( `/${this.restEndpoint}/users`, ResponseHelper.send(async (req: UserRequest.Invite) => { - if (!isEmailSetUp) { + if (config.get('userManagement.emails.mode') === '') { throw new ResponseHelper.ResponseError( 'Email sending must be set up in order to invite other users', undefined, From 9fbc5bb2d8b37ef16d749f9d396f253c453f450e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Feb 2022 14:21:29 +0100 Subject: [PATCH 25/81] :zap: Fix failing tests --- .../test/integration/users.endpoints.test.ts | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 4dc2499a58e16..656ec2e25830c 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -17,6 +17,13 @@ beforeAll(async () => { }); beforeEach(async () => { + jest.isolateModules(() => { + jest.mock('../../config'); + }); + + config.set('userManagement.hasOwner', true); + config.set('userManagement.emails.mode', ''); + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global', @@ -32,8 +39,6 @@ beforeEach(async () => { updatedAt: new Date(), globalRole: globalOwnerRole, }); - - config.set('userManagement.hasOwner', true); }); afterEach(async () => { @@ -345,30 +350,14 @@ test('POST /users/:id should fail with already accepted invite', async () => { expect(response.statusCode).toBe(400); }); -// test('POST /users should fail if emailing is not set up', async () => { -// const owner = await Db.collections.User!.findOneOrFail(); -// const authOwnerAgent = await utils.createAuthAgent(app, owner); - -// config.set('userManagement.emails.mode', ''); // TODO: This line has no effect - -// const response = await authOwnerAgent.post('/users').send([{ email: utils.randomEmail() }]); - -// expect(response.statusCode).toBe(500); -// }); - -// test('POST /users should report error due to wrong SMTP config', async () => { -// const owner = await Db.collections.User!.findOneOrFail(); -// const authOwnerAgent = await utils.createAuthAgent(app, owner); - -// config.set('userManagement.emails.mode', 'smtp'); -// config.set('userManagement.emails.smtp.host', 'XYZ'); // TODO: This breaks following test - -// const payload = TEST_EMAILS_TO_CREATE_USER_SHELLS.map((e) => ({ email: e })); +test('POST /users should fail if emailing is not set up', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); -// const response = await authOwnerAgent.post('/users').send(payload); + const response = await authOwnerAgent.post('/users').send([{ email: utils.randomEmail() }]); -// expect(response.statusCode).toBe(500); -// }); + expect(response.statusCode).toBe(500); +}); test('POST /users should email invites and create user shells', async () => { const owner = await Db.collections.User!.findOneOrFail(); @@ -412,6 +401,8 @@ test('POST /users should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); + config.set('userManagement.emails.mode', 'smtp'); + const invalidPayloads = [ utils.randomEmail(), [utils.randomEmail()], @@ -430,6 +421,8 @@ test('POST /users should error if user email already exists', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); + config.set('userManagement.emails.mode', 'smtp'); + const email = utils.randomEmail(); const role = await Db.collections.Role!.findOneOrFail({ scope: 'global', name: 'member' }); await Db.collections.User?.save({ email, globalRole: role }); @@ -443,6 +436,8 @@ test('POST /users should ignore an empty payload', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); + config.set('userManagement.emails.mode', 'smtp'); + const response = await authOwnerAgent.post('/users').send([]); const { data } = response.body; @@ -452,6 +447,21 @@ test('POST /users should ignore an empty payload', async () => { expect(data.length).toBe(0); }); +// TODO: UserManagementMailer is a singleton - cannot reinstantiate with wrong creds +// test('POST /users should error for wrong SMTP config', async () => { +// const owner = await Db.collections.User!.findOneOrFail(); +// const authOwnerAgent = await utils.createAuthAgent(app, owner); + +// config.set('userManagement.emails.mode', 'smtp'); +// config.set('userManagement.emails.smtp.host', 'XYZ'); // break SMTP config + +// const payload = TEST_EMAILS_TO_CREATE_USER_SHELLS.map((e) => ({ email: e })); + +// const response = await authOwnerAgent.post('/users').send(payload); + +// expect(response.statusCode).toBe(500); +// }); + const INITIAL_TEST_USER = { id: uuid(), email: utils.randomEmail(), From e0216188369ec05f25f79369f05327dbc2624ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Feb 2022 14:28:00 +0100 Subject: [PATCH 26/81] :fire: Remove outdated test --- .../cli/test/integration/users.endpoints.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 656ec2e25830c..ff16d0bd883b6 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -417,21 +417,6 @@ test('POST /users should fail with invalid inputs', async () => { } }); -test('POST /users should error if user email already exists', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); - - config.set('userManagement.emails.mode', 'smtp'); - - const email = utils.randomEmail(); - const role = await Db.collections.Role!.findOneOrFail({ scope: 'global', name: 'member' }); - await Db.collections.User?.save({ email, globalRole: role }); - - const response = await authOwnerAgent.post('/users').send([{ email }]); - - expect(response.statusCode).toBe(400); -}); - test('POST /users should ignore an empty payload', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); From b449690b1ece23f47419101e4b5c458367df175d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Feb 2022 17:44:18 +0100 Subject: [PATCH 27/81] :sparkles: Add tests for password reset flow --- .../UserManagement/routes/passwordReset.ts | 9 + .../passwordReset.endpoints.test.ts | 231 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 packages/cli/test/integration/passwordReset.endpoints.test.ts diff --git a/packages/cli/src/UserManagement/routes/passwordReset.ts b/packages/cli/src/UserManagement/routes/passwordReset.ts index e9f0c26fdcbf2..08cb690523603 100644 --- a/packages/cli/src/UserManagement/routes/passwordReset.ts +++ b/packages/cli/src/UserManagement/routes/passwordReset.ts @@ -14,6 +14,7 @@ import * as UserManagementMailer from '../email'; import type { PasswordResetRequest } from '../../requests'; import { issueJWT } from '../auth/jwt'; import { getBaseUrl } from '../../GenericHelpers'; +import config = require('../../../config'); export function passwordResetNamespace(this: N8nApp): void { /** @@ -22,6 +23,14 @@ export function passwordResetNamespace(this: N8nApp): void { this.app.post( `/${this.restEndpoint}/forgot-password`, ResponseHelper.send(async (req: PasswordResetRequest.Email) => { + if (config.get('userManagement.emails.mode') === '') { + throw new ResponseHelper.ResponseError( + 'Email sending must be set up in order to request a password reset email', + undefined, + 500, + ); + } + const { email } = req.body; if (!email) { diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts new file mode 100644 index 0000000000000..3322b37cfcf44 --- /dev/null +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -0,0 +1,231 @@ +import express = require('express'); +import { getConnection } from 'typeorm'; +import { v4 as uuid } from 'uuid'; + +import * as utils from './shared/utils'; +import { Db } from '../../src'; +import config = require('../../config'); +import { compare } from 'bcryptjs'; + +let app: express.Application; + +beforeAll(async () => { + app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); + await utils.initTestDb(); + await utils.truncateUserTable(); +}); + +beforeEach(async () => { + jest.isolateModules(() => { + jest.mock('../../config'); + }); + + config.set('userManagement.hasOwner', true); + config.set('userManagement.emails.mode', ''); + + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); + + await Db.collections.User!.save({ + 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, + createdAt: new Date(), + updatedAt: new Date(), + globalRole: globalOwnerRole, + }); +}); + +afterEach(async () => { + await utils.truncateUserTable(); +}); + +afterAll(() => { + return getConnection().close(); +}); + +test('POST /forgot-password should send owner password reset email', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const { + user, + pass, + smtp: { host, port, secure }, + } = await utils.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); + + const response = await authOwnerAgent + .post('/forgot-password') + .send({ email: INITIAL_TEST_USER.email }); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({}); +}); + +test('POST /forgot-password should fail if emailing is not set up', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const response = await authOwnerAgent + .post('/forgot-password') + .send({ email: INITIAL_TEST_USER.email }); + + expect(response.statusCode).toBe(500); +}); + +test('POST /forgot-password should fail with invalid inputs', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + config.set('userManagement.emails.mode', 'smtp'); + + const invalidPayloads = [ + utils.randomEmail(), + [utils.randomEmail()], + {}, + [{ name: utils.randomName() }], + [{ email: utils.randomName() }], + ]; + + for (const invalidPayload of invalidPayloads) { + const response = await authOwnerAgent.post('/forgot-password').send(invalidPayload); + expect(response.statusCode).toBe(400); + } +}); + +test('POST /forgot-password should fail if user is not found', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + config.set('userManagement.emails.mode', 'smtp'); + + const response = await authOwnerAgent + .post('/forgot-password') + .send({ email: utils.randomEmail() }); + + expect(response.statusCode).toBe(404); +}); + +test('GET /resolve-password-token should succeed with valid inputs', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const resetPasswordToken = uuid(); + + await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); + + const response = await authOwnerAgent + .get('/resolve-password-token') + .query({ userId: INITIAL_TEST_USER.id, token: resetPasswordToken }); + + expect(response.statusCode).toBe(200); +}); + +test('GET /resolve-password-token should fail with invalid inputs', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + config.set('userManagement.emails.mode', 'smtp'); + + const first = await authOwnerAgent.get('/resolve-password-token').query({ token: uuid() }); + + const second = await authOwnerAgent + .get('/resolve-password-token') + .query({ userId: INITIAL_TEST_USER.id }); + + for (const response of [first, second]) { + expect(response.statusCode).toBe(400); + } +}); + +test('GET /resolve-password-token should fail if user is not found', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + config.set('userManagement.emails.mode', 'smtp'); + + const response = await authOwnerAgent + .get('/resolve-password-token') + .query({ userId: INITIAL_TEST_USER.id, token: uuid() }); + + expect(response.statusCode).toBe(404); +}); + +test('POST /change-password should succeed with valid inputs', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const resetPasswordToken = uuid(); + await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); + + const passwordToSet = utils.randomValidPassword(); + + const response = await authOwnerAgent.post('/change-password').send({ + token: resetPasswordToken, + id: INITIAL_TEST_USER.id, + password: passwordToSet, + }); + + expect(response.statusCode).toBe(200); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); + + const { password: storedPassword } = await Db.collections.User!.findOneOrFail( + INITIAL_TEST_USER.id, + ); + + const comparisonResult = await compare(passwordToSet, storedPassword!); + expect(comparisonResult).toBe(true); +}); + +test('POST /change-password should fail with invalid inputs', async () => { + const owner = await Db.collections.User!.findOneOrFail(); + const authOwnerAgent = await utils.createAuthAgent(app, owner); + + const resetPasswordToken = uuid(); + await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); + + const invalidPayloads = [ + { token: uuid() }, + { id: INITIAL_TEST_USER.id }, + { password: utils.randomValidPassword() }, + { token: uuid(), id: INITIAL_TEST_USER.id }, + { token: uuid(), password: utils.randomValidPassword() }, + { id: INITIAL_TEST_USER.id, password: utils.randomValidPassword() }, + { + id: INITIAL_TEST_USER.id, + password: utils.randomInvalidPassword(), + token: resetPasswordToken, + }, + { + id: INITIAL_TEST_USER.id, + password: utils.randomValidPassword(), + token: uuid(), + }, + ]; + + for (const invalidPayload of invalidPayloads) { + const response = await authOwnerAgent.post('/change-password').query(invalidPayload); + expect(response.statusCode).toBe(400); + } +}); + +const INITIAL_TEST_USER = { + id: uuid(), + email: utils.randomEmail(), + firstName: utils.randomName(), + lastName: utils.randomName(), + password: utils.randomValidPassword(), +}; From e88e19b056272471b12a3b064e3045c03dab30b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Feb 2022 17:47:47 +0100 Subject: [PATCH 28/81] :pencil2: Fix test wording --- packages/cli/test/integration/passwordReset.endpoints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 3322b37cfcf44..857a75005895f 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -48,7 +48,7 @@ afterAll(() => { return getConnection().close(); }); -test('POST /forgot-password should send owner password reset email', async () => { +test('POST /forgot-password should send password reset email', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); From 458493c7730b0764a60da55edf47ee26906b97a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Feb 2022 17:50:08 +0100 Subject: [PATCH 29/81] :zap: Set password reset namespace --- packages/cli/test/integration/passwordReset.endpoints.test.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 857a75005895f..d6377fa7fd697 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -10,7 +10,7 @@ import { compare } from 'bcryptjs'; let app: express.Application; beforeAll(async () => { - app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); + app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); }); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 998519df3b413..b137cb82d7fe9 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -20,6 +20,7 @@ import { meNamespace as meEndpoints } from '../../../src/UserManagement/routes/m import { usersNamespace as usersEndpoints } from '../../../src/UserManagement/routes/users'; import { authenticationMethods as authEndpoints } from '../../../src/UserManagement/routes/auth'; import { ownerNamespace as ownerEndpoints } from '../../../src/UserManagement/routes/owner'; +import { passwordResetNamespace as passwordResetEndpoints } from '../../../src/UserManagement/routes/passwordReset'; import { getConnection } from 'typeorm'; import { issueJWT } from '../../../src/UserManagement/auth/jwt'; import { N8nApp } from '../../../src/UserManagement/Interfaces'; @@ -34,7 +35,7 @@ export const isTestRun = process.argv[1].split('/').includes('jest'); const POPULAR_TOP_LEVEL_DOMAINS = ['com', 'org', 'net', 'io', 'edu']; -type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner'; +type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset'; /** * Initialize a test server to make requests to. @@ -72,6 +73,7 @@ export function initTestServer( users: usersEndpoints, auth: authEndpoints, owner: ownerEndpoints, + passwordReset: passwordResetEndpoints, }; for (const namespace of namespaces) { From 0c904ebac1f0d44f65515c374a05f7f903db66d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 11 Feb 2022 18:01:45 +0100 Subject: [PATCH 30/81] :fire: Remove unused helper --- packages/cli/src/UserManagement/UserManagementHelper.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 5c4976c38ab39..c1306512f314b 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable import/no-cycle */ -import { IsNull, Not, QueryFailedError } from 'typeorm'; +import { IsNull, Not } from 'typeorm'; import { Db, GenericHelpers, ResponseHelper } from '..'; import config = require('../../config'); import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, User } from '../databases/entities/User'; @@ -18,11 +18,6 @@ export function getInstanceDomain(): string { return domain; } -export const isFailedQuery = ( - error: unknown, -): error is QueryFailedError & { parameters: Array } => - error instanceof QueryFailedError; - export async function isInstanceOwnerSetup(): Promise { const users = await Db.collections.User!.find({ email: Not(IsNull()) }); return users.length !== 0; From 1ae3ebebfcbe08de19c361bb3e00f5336d400aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 09:25:49 +0100 Subject: [PATCH 31/81] :zap: Increase readability of domain fetcher --- .../cli/src/UserManagement/UserManagementHelper.ts | 13 ++++++------- packages/cli/src/UserManagement/routes/users.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index c1306512f314b..b8e69accfe940 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -9,13 +9,12 @@ import { PublicUser } from './Interfaces'; export const isEmailSetUp = Boolean(config.get('userManagement.emails.mode')); -export function getInstanceDomain(): string { - let domain = GenericHelpers.getBaseUrl(); - if (domain.endsWith('/')) { - domain = domain.slice(0, domain.length - 1); - } - - return domain; +/** + * Return the n8n instance base URL without trailing slash. + */ +export function getInstanceBaseUrl(): string { + const baseUrl = GenericHelpers.getBaseUrl(); + return baseUrl.endsWith('/') ? baseUrl.slice(0, baseUrl.length - 1) : baseUrl; } export async function isInstanceOwnerSetup(): Promise { diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index e3d7adc6016d2..a56cc1aa0c91c 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -9,7 +9,7 @@ import { Db, ResponseHelper } from '../..'; import { N8nApp } from '../Interfaces'; import { UserRequest } from '../../requests'; import { - getInstanceDomain, + getInstanceBaseUrl, isEmailSetUp, sanitizeUser, validatePassword, @@ -105,7 +105,7 @@ export function usersNamespace(this: N8nApp): void { throw new ResponseHelper.ResponseError(`An error occurred during user creation`); } - const domain = getInstanceDomain(); + const baseUrl = getInstanceBaseUrl(); // send invite email to new or not yet setup users const mailer = getInstance(); @@ -115,11 +115,11 @@ export function usersNamespace(this: N8nApp): void { .filter(([email, id]) => id && email) .map(async ([email, id]) => { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const inviteAcceptUrl = `${domain}/signup/inviterId=${req.user.id}&inviteeId=${id}`; + const inviteAcceptUrl = `${baseUrl}/signup/inviterId=${req.user.id}&inviteeId=${id}`; const result = await mailer.invite({ email, inviteAcceptUrl, - domain, + domain: baseUrl, }); const resp: { id: string | null; email: string; error?: string } = { id, @@ -325,12 +325,12 @@ export function usersNamespace(this: N8nApp): void { ); } - const domain = getInstanceDomain(); + const baseUrl = getInstanceBaseUrl(); const result = await getInstance().invite({ email: reinvitee.email, - inviteAcceptUrl: `${domain}/signup/inviterId=${req.user.id}&inviteeId=${reinvitee.id}`, - domain, + inviteAcceptUrl: `${baseUrl}/signup/inviterId=${req.user.id}&inviteeId=${reinvitee.id}`, + domain: baseUrl, }); if (!result.success) { From 5c657e123c5166cebeae13aa3e937cdf72525704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 09:50:52 +0100 Subject: [PATCH 32/81] :zap: Refactor payload validation --- packages/cli/src/UserManagement/routes/users.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index a56cc1aa0c91c..8b0813c7fa4fc 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -42,24 +42,21 @@ export function usersNamespace(this: N8nApp): void { if (!req.body.length) return []; - // eslint-disable-next-line no-restricted-syntax - for (const invite of req.body) { + const createUsers: { [key: string]: string | null } = {}; + // Validate payload + req.body.forEach((invite) => { if (typeof invite !== 'object' || !invite.email) { throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } - } - const createUsers: { [key: string]: string | null } = {}; - // Validate payload - req.body.forEach((invitation) => { - if (!validator.isEmail(invitation.email)) { + if (!validator.isEmail(invite.email)) { throw new ResponseHelper.ResponseError( - `Invalid email address ${invitation.email}`, + `Invalid email address ${invite.email}`, undefined, 400, ); } - createUsers[invitation.email] = null; + createUsers[invite.email] = null; }); const role = await Db.collections.Role!.findOne({ scope: 'global', name: 'member' }); From 3639cda45e6fde914200936ddb6d83170ed75a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 09:56:52 +0100 Subject: [PATCH 33/81] :fire: Remove repetition --- packages/cli/src/UserManagement/routes/users.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 8b0813c7fa4fc..06b8660f8f83e 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -61,14 +61,6 @@ export function usersNamespace(this: N8nApp): void { const role = await Db.collections.Role!.findOne({ scope: 'global', name: 'member' }); - const invites = req.body; - - invites.forEach(({ email }) => { - if (!validator.isEmail(email)) { - throw new ResponseHelper.ResponseError(`Invalid email address: ${email}`, undefined, 400); - } - }); - // remove/exclude existing users from creation const existingUsers = await Db.collections.User!.find({ where: { email: In(Object.keys(createUsers)) }, From 14000a4f00bc6e0eb4fda7c77eb8f55287d7b9aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 09:59:53 +0100 Subject: [PATCH 34/81] :rewind: Restore logging --- packages/cli/src/UserManagement/routes/users.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 06b8660f8f83e..b3f5ddc24bb43 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -20,6 +20,7 @@ import { SharedCredentials } from '../../databases/entities/SharedCredentials'; import { getInstance } from '../email/UserManagementMailer'; import { issueJWT } from '../auth/jwt'; import config = require('../../../config'); +import { LoggerProxy } from '../../../../workflow/dist/src'; export function usersNamespace(this: N8nApp): void { /** @@ -133,6 +134,10 @@ export function usersNamespace(this: N8nApp): void { const { inviterId, inviteeId } = req.query; if (!inviterId || !inviteeId) { + LoggerProxy.error('Invalid invite URL - did not receive user IDs', { + inviterId, + inviteeId, + }); throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } From 84d1ee09c7cfbc325372dc122e49adb3e5bdcad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 10:43:05 +0100 Subject: [PATCH 35/81] :zap: Initialize logger in tests --- packages/cli/test/integration/users.endpoints.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index ff16d0bd883b6..0d58d382d15ef 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -7,6 +7,8 @@ import * as utils from './shared/utils'; import { Db } from '../../src'; import config = require('../../config'); import { SUCCESS_RESPONSE_BODY } from './shared/constants'; +import { getLogger } from '../../src/Logger'; +import { LoggerProxy } from 'n8n-workflow'; let app: express.Application; @@ -14,6 +16,9 @@ beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); + config.set('logs.output', 'file'); // declutter console output + const logger = getLogger(); + LoggerProxy.init(logger); }); beforeEach(async () => { From 93c3e0262a60f540cbfc4ba0c3c8f4a5ced00418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 10:44:37 +0100 Subject: [PATCH 36/81] :fire: Remove redundancy from check --- packages/cli/src/UserManagement/routes/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index b3f5ddc24bb43..986c1c2f47746 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -231,7 +231,7 @@ export function usersNamespace(this: N8nApp): void { const { transferId } = req.query; - if (transferId && transferId === idToDelete) { + if (transferId === idToDelete) { throw new ResponseHelper.ResponseError( 'User to delete and transferee cannot be the same', undefined, From b11698157c1ec4c2e8eb1f445a4bf88eb2206fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 10:56:45 +0100 Subject: [PATCH 37/81] :truck: Move `globalOwnerRole` fetching to global scope --- .../test/integration/auth.endpoints.test.ts | 7 ++++++- .../cli/test/integration/me.endpoints.test.ts | 20 +++++++++++++++---- .../test/integration/owner.endpoints.test.ts | 10 ++++++++-- .../test/integration/users.endpoints.test.ts | 13 +++++++----- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 972fded48d63b..f6fa8a31bcaa2 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -10,10 +10,12 @@ import * as utils from './shared/utils'; import { LOGGED_OUT_RESPONSE_BODY, REST_PATH_SEGMENT } from './shared/constants'; import { Db } from '../../src'; import { User } from '../../src/databases/entities/User'; +import { Role } from '../../src/databases/entities/Role'; describe('auth endpoints', () => { describe('Owner requests', () => { let app: express.Application; + let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['auth'], applyAuth: true }); @@ -22,7 +24,10 @@ describe('auth endpoints', () => { }); beforeEach(async () => { - const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); const newOwner = new User(); diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index f4ba944551980..9c269ae568399 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -9,6 +9,9 @@ import * as utils from './shared/utils'; import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { Db } from '../../src'; import { User } from '../../src/databases/entities/User'; +import { Role } from '../../src/databases/entities/Role'; + +let globalOwnerRole: Role; describe('/me endpoints', () => { describe('Shell requests', () => { @@ -18,11 +21,14 @@ describe('/me endpoints', () => { app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); + + globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); }); beforeEach(async () => { - const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); - await Db.collections.User!.save({ id: uuid(), createdAt: new Date(), @@ -167,7 +173,10 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global' }); + const globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); const newMember = new User(); @@ -326,7 +335,10 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); + const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); const newOwner = new User(); diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 7d1dc027f9800..68ea342a3414f 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -6,6 +6,9 @@ import { v4 as uuid } from 'uuid'; import * as utils from './shared/utils'; import { Db } from '../../src'; import config = require('../../config'); +import { Role } from '../../src/databases/entities/Role'; + +let globalOwnerRole: Role; describe('/owner endpoints', () => { describe('Shell requests', () => { @@ -15,11 +18,14 @@ describe('/owner endpoints', () => { app = utils.initTestServer({ namespaces: ['owner'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); + + globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); }); beforeEach(async () => { - const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global' }); - await Db.collections.User!.save({ id: uuid(), createdAt: new Date(), diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 0d58d382d15ef..9434afb9ed5d6 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -9,13 +9,21 @@ import config = require('../../config'); import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { getLogger } from '../../src/Logger'; import { LoggerProxy } from 'n8n-workflow'; +import { Role } from '../../src/databases/entities/Role'; let app: express.Application; +let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); + + globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); + config.set('logs.output', 'file'); // declutter console output const logger = getLogger(); LoggerProxy.init(logger); @@ -29,11 +37,6 @@ beforeEach(async () => { config.set('userManagement.hasOwner', true); config.set('userManagement.emails.mode', ''); - const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); - await Db.collections.User!.save({ id: INITIAL_TEST_USER.id, email: INITIAL_TEST_USER.email, From bc1c4289682e08db2162cdfaaf7b65b55e65cf81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 10:57:56 +0100 Subject: [PATCH 38/81] :fire: Remove unused imports --- packages/cli/test/integration/auth.endpoints.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index f6fa8a31bcaa2..bc98bdd3b3262 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -3,11 +3,10 @@ import express = require('express'); import { getConnection } from 'typeorm'; import validator from 'validator'; import { v4 as uuid } from 'uuid'; -import * as request from 'supertest'; import config = require('../../config'); import * as utils from './shared/utils'; -import { LOGGED_OUT_RESPONSE_BODY, REST_PATH_SEGMENT } from './shared/constants'; +import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; import { Db } from '../../src'; import { User } from '../../src/databases/entities/User'; import { Role } from '../../src/databases/entities/Role'; From 8c96a51f6286074e34b952479d9abebb893000fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:04:03 +0100 Subject: [PATCH 39/81] :truck: Move random utils to own module --- .../test/integration/auth.endpoints.test.ts | 9 +- .../cli/test/integration/me.endpoints.test.ts | 69 +++++------ .../test/integration/owner.endpoints.test.ts | 51 ++++---- .../cli/test/integration/shared/random.ts | 31 +++++ packages/cli/test/integration/shared/utils.ts | 49 ++------ .../test/integration/users.endpoints.test.ts | 111 +++++++++--------- 6 files changed, 161 insertions(+), 159 deletions(-) create mode 100644 packages/cli/test/integration/shared/random.ts diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index bc98bdd3b3262..63804d840df33 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -10,6 +10,7 @@ import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; import { Db } from '../../src'; import { User } from '../../src/databases/entities/User'; import { Role } from '../../src/databases/entities/Role'; +import { randomEmail, randomValidPassword, randomName } from './shared/random'; describe('auth endpoints', () => { describe('Owner requests', () => { @@ -144,8 +145,8 @@ describe('auth endpoints', () => { }); const TEST_USER = { - email: utils.randomEmail(), - password: utils.randomValidPassword(), - firstName: utils.randomName(), - lastName: utils.randomName(), + email: randomEmail(), + password: randomValidPassword(), + firstName: randomName(), + lastName: randomName(), }; diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 9c269ae568399..9eaed14ba476d 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -10,6 +10,7 @@ import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { Db } from '../../src'; import { User } from '../../src/databases/entities/User'; import { Role } from '../../src/databases/entities/Role'; +import { randomValidPassword, randomInvalidPassword, randomEmail, randomName, randomString } from './shared/random'; let globalOwnerRole: Role; @@ -122,7 +123,7 @@ describe('/me endpoints', () => { const authShellAgent = await utils.createAuthAgent(app, shell); const validPayloads = Array.from({ length: 3 }, () => ({ - password: utils.randomValidPassword(), + password: randomValidPassword(), })); for (const validPayload of validPayloads) { @@ -137,7 +138,7 @@ describe('/me endpoints', () => { const authShellAgent = await utils.createAuthAgent(app, shell); const invalidPayloads = [ - ...Array.from({ length: 3 }, () => ({ password: utils.randomInvalidPassword() })), + ...Array.from({ length: 3 }, () => ({ password: randomInvalidPassword() })), {}, undefined, '', @@ -185,7 +186,7 @@ describe('/me endpoints', () => { email: TEST_USER.email, firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, - password: hashSync(utils.randomValidPassword(), genSaltSync(10)), + password: hashSync(randomValidPassword(), genSaltSync(10)), globalRole: globalMemberRole, }); @@ -284,7 +285,7 @@ describe('/me endpoints', () => { const authMemberAgent = await utils.createAuthAgent(app, member); const validPayloads = Array.from({ length: 3 }, () => ({ - password: utils.randomValidPassword(), + password: randomValidPassword(), })); for (const validPayload of validPayloads) { @@ -299,7 +300,7 @@ describe('/me endpoints', () => { const authMemberAgent = await utils.createAuthAgent(app, member); const invalidPayloads = [ - ...Array.from({ length: 3 }, () => ({ password: utils.randomInvalidPassword() })), + ...Array.from({ length: 3 }, () => ({ password: randomInvalidPassword() })), {}, undefined, '', @@ -347,7 +348,7 @@ describe('/me endpoints', () => { email: TEST_USER.email, firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, - password: hashSync(utils.randomValidPassword(), genSaltSync(10)), + password: hashSync(randomValidPassword(), genSaltSync(10)), globalRole: globalOwnerRole, }); @@ -434,9 +435,9 @@ describe('/me endpoints', () => { }); const TEST_USER = { - email: utils.randomEmail(), - firstName: utils.randomName(), - lastName: utils.randomName(), + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), }; const SURVEY = [ @@ -447,63 +448,63 @@ const SURVEY = [ 'otherWorkArea', 'workArea', ].reduce>((acc, cur) => { - return (acc[cur] = utils.randomString(1, 10)), acc; + return (acc[cur] = randomString(1, 10)), acc; }, {}); const VALID_PATCH_ME_PAYLOADS = [ { - email: utils.randomEmail(), - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }, { - email: utils.randomEmail(), - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }, ]; const INVALID_PATCH_ME_PAYLOADS = [ { email: 'invalid', - firstName: utils.randomName(), - lastName: utils.randomName(), + firstName: randomName(), + lastName: randomName(), }, { - email: utils.randomEmail(), + email: randomEmail(), firstName: '', - lastName: utils.randomName(), + lastName: randomName(), }, { - email: utils.randomEmail(), - firstName: utils.randomName(), + email: randomEmail(), + firstName: randomName(), lastName: '', }, { - email: utils.randomEmail(), + email: randomEmail(), firstName: 123, - lastName: utils.randomName(), + lastName: randomName(), }, { - firstName: utils.randomName(), - lastName: utils.randomName(), + firstName: randomName(), + lastName: randomName(), }, { - firstName: utils.randomName(), + firstName: randomName(), }, { - lastName: utils.randomName(), + lastName: randomName(), }, { - email: utils.randomEmail(), + email: randomEmail(), firstName: 'John { }); const TEST_USER = { - email: utils.randomEmail(), - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }; const INVALID_POST_OWNER_PAYLOADS = [ { email: '', - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }, { - email: utils.randomEmail(), + email: randomEmail(), firstName: '', - lastName: utils.randomName(), - password: utils.randomValidPassword(), + lastName: randomName(), + password: randomValidPassword(), }, { - email: utils.randomEmail(), - firstName: utils.randomName(), + email: randomEmail(), + firstName: randomName(), lastName: '', - password: utils.randomValidPassword(), + password: randomValidPassword(), }, { - email: utils.randomEmail(), - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomInvalidPassword(), + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomInvalidPassword(), }, { - firstName: utils.randomName(), - lastName: utils.randomName(), + firstName: randomName(), + lastName: randomName(), }, { - firstName: utils.randomName(), + firstName: randomName(), }, { - lastName: utils.randomName(), + lastName: randomName(), }, { - email: utils.randomEmail(), + email: randomEmail(), firstName: 'John (array: T[]) => array[Math.floor(Math.random() * array.length)]; + +export const randomValidPassword = () => randomString(MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH); + +export const randomInvalidPassword = () => + chooseRandomly([ + randomString(1, MIN_PASSWORD_LENGTH - 1), + randomString(MAX_PASSWORD_LENGTH + 1, 100), + ]); + +export const randomEmail = () => `${randomName()}@${randomName()}.${randomTopLevelDomain()}`; + +const POPULAR_TOP_LEVEL_DOMAINS = ['com', 'org', 'net', 'io', 'edu']; + +const randomTopLevelDomain = () => chooseRandomly(POPULAR_TOP_LEVEL_DOMAINS); + +export const randomName = () => randomString(3, 7); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 998519df3b413..3eaaeae90cb6a 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -1,4 +1,3 @@ -import { randomBytes } from 'crypto'; import express = require('express'); import * as superagent from 'superagent'; import * as request from 'supertest'; @@ -11,11 +10,7 @@ import config = require('../../../config'); import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes'; import { Db } from '../../../src'; -import { - MAX_PASSWORD_LENGTH, - MIN_PASSWORD_LENGTH, - User, -} from '../../../src/databases/entities/User'; +import { User } from '../../../src/databases/entities/User'; import { meNamespace as meEndpoints } from '../../../src/UserManagement/routes/me'; import { usersNamespace as usersEndpoints } from '../../../src/UserManagement/routes/users'; import { authenticationMethods as authEndpoints } from '../../../src/UserManagement/routes/auth'; @@ -32,8 +27,6 @@ export const getSmtpTestAccount = util.promisify(createTestAcco export const isTestRun = process.argv[1].split('/').includes('jest'); -const POPULAR_TOP_LEVEL_DOMAINS = ['com', 'org', 'net', 'io', 'edu']; - type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner'; /** @@ -42,15 +35,13 @@ type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner'; * @param namespaces Namespaces of endpoints to apply to the test server. * @param applyAuth Whether to apply auth middleware to the test server. */ -export function initTestServer( - { - applyAuth, - namespaces, - }: { - applyAuth: boolean; - namespaces?: EndpointNamespace[]; - }, -) { +export function initTestServer({ + applyAuth, + namespaces, +}: { + applyAuth: boolean; + namespaces?: EndpointNamespace[]; +}) { const testServer = { app: express(), restEndpoint: REST_PATH_SEGMENT, @@ -160,27 +151,3 @@ export function getAuthToken(response: request.Response, authCookieName = 'n8n-a return match.groups.token; } - -/** - * Create a random string of random length between two limits, both inclusive. - */ -export function randomString(min: number, max: number) { - const randomInteger = Math.floor(Math.random() * (max - min) + min) + 1; - return randomBytes(randomInteger / 2).toString('hex'); -} - -const chooseRandomly = (array: T[]) => array[Math.floor(Math.random() * array.length)]; - -export const randomValidPassword = () => randomString(MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH); - -export const randomInvalidPassword = () => - chooseRandomly([ - randomString(1, MIN_PASSWORD_LENGTH - 1), - randomString(MAX_PASSWORD_LENGTH + 1, 100), - ]); - -export const randomEmail = () => `${randomName()}@${randomName()}.${randomTopLevelDomain()}`; - -const randomTopLevelDomain = () => chooseRandomly(POPULAR_TOP_LEVEL_DOMAINS); - -export const randomName = () => randomString(3, 7); diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 9434afb9ed5d6..de1307da27c63 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -10,6 +10,7 @@ import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { getLogger } from '../../src/Logger'; import { LoggerProxy } from 'n8n-workflow'; import { Role } from '../../src/databases/entities/Role'; +import { randomEmail, randomValidPassword, randomName, randomInvalidPassword } from './shared/random'; let app: express.Application; let globalOwnerRole: Role; @@ -99,10 +100,10 @@ test('DELETE /users/:id should delete the user', async () => { const { id: idToDelete } = await Db.collections.User!.save({ id: uuid(), - email: utils.randomEmail(), - password: utils.randomValidPassword(), - firstName: utils.randomName(), - lastName: utils.randomName(), + email: randomEmail(), + password: randomValidPassword(), + firstName: randomName(), + lastName: randomName(), createdAt: new Date(), updatedAt: new Date(), globalRole: globalMemberRole, @@ -134,10 +135,10 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () = const { id: idToDelete } = await Db.collections.User!.save({ id: uuid(), - email: utils.randomEmail(), - password: utils.randomValidPassword(), - firstName: utils.randomName(), - lastName: utils.randomName(), + email: randomEmail(), + password: randomValidPassword(), + firstName: randomName(), + lastName: randomName(), createdAt: new Date(), updatedAt: new Date(), globalRole: globalMemberRole, @@ -161,17 +162,17 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { const userToDelete = await Db.collections.User!.save({ id: uuid(), - email: utils.randomEmail(), - password: utils.randomValidPassword(), - firstName: utils.randomName(), - lastName: utils.randomName(), + email: randomEmail(), + password: randomValidPassword(), + firstName: randomName(), + lastName: randomName(), createdAt: new Date(), updatedAt: new Date(), globalRole: workflowOwnerRole, }); const savedWorkflow = await Db.collections.Workflow!.save({ - name: utils.randomName(), + name: randomName(), active: false, connections: {}, }); @@ -204,10 +205,10 @@ test('GET /resolve-signup-token should validate invite token', async () => { const { id: inviteeId } = await Db.collections.User!.save({ id: uuid(), - email: utils.randomEmail(), - password: utils.randomValidPassword(), - firstName: utils.randomName(), - lastName: utils.randomName(), + email: randomEmail(), + password: randomValidPassword(), + firstName: randomName(), + lastName: randomName(), createdAt: new Date(), updatedAt: new Date(), globalRole: globalMemberRole, @@ -240,10 +241,10 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const { id: inviteeId } = await Db.collections.User!.save({ id: uuid(), - email: utils.randomEmail(), - password: utils.randomValidPassword(), - firstName: utils.randomName(), - lastName: utils.randomName(), + email: randomEmail(), + password: randomValidPassword(), + firstName: randomName(), + lastName: randomName(), createdAt: new Date(), updatedAt: new Date(), globalRole: globalMemberRole, @@ -280,15 +281,15 @@ test('POST /users/:id should fill out a user shell', async () => { }); const userToFillOut = await Db.collections.User!.save({ - email: utils.randomEmail(), + email: randomEmail(), globalRole: globalMemberRole, }); const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send({ inviterId: INITIAL_TEST_USER.id, - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }); const { @@ -324,7 +325,7 @@ test('POST /users/:id should fail with invalid inputs', async () => { }); const userToFillOut = await Db.collections.User!.save({ - email: utils.randomEmail(), + email: randomEmail(), globalRole: globalMemberRole, }); @@ -343,16 +344,16 @@ test('POST /users/:id should fail with already accepted invite', async () => { }); const userToFillOut = await Db.collections.User!.save({ - email: utils.randomEmail(), - password: utils.randomValidPassword(), // simulate accepted invite + email: randomEmail(), + password: randomValidPassword(), // simulate accepted invite globalRole: globalMemberRole, }); const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send({ inviterId: INITIAL_TEST_USER.id, - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }); expect(response.statusCode).toBe(400); @@ -362,7 +363,7 @@ test('POST /users should fail if emailing is not set up', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const response = await authOwnerAgent.post('/users').send([{ email: utils.randomEmail() }]); + const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]); expect(response.statusCode).toBe(500); }); @@ -412,11 +413,11 @@ test('POST /users should fail with invalid inputs', async () => { config.set('userManagement.emails.mode', 'smtp'); const invalidPayloads = [ - utils.randomEmail(), - [utils.randomEmail()], + randomEmail(), + [randomEmail()], {}, - [{ name: utils.randomName() }], - [{ email: utils.randomName() }], + [{ name: randomName() }], + [{ email: randomName() }], ]; for (const invalidPayload of invalidPayloads) { @@ -457,43 +458,43 @@ test('POST /users should ignore an empty payload', async () => { const INITIAL_TEST_USER = { id: uuid(), - email: utils.randomEmail(), - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }; const INVALID_FILL_OUT_USER_PAYLOADS = [ { - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }, { inviterId: INITIAL_TEST_USER.id, - firstName: utils.randomName(), - password: utils.randomValidPassword(), + firstName: randomName(), + password: randomValidPassword(), }, { inviterId: INITIAL_TEST_USER.id, - firstName: utils.randomName(), - password: utils.randomValidPassword(), + firstName: randomName(), + password: randomValidPassword(), }, { inviterId: INITIAL_TEST_USER.id, - firstName: utils.randomName(), - lastName: utils.randomName(), + firstName: randomName(), + lastName: randomName(), }, { inviterId: INITIAL_TEST_USER.id, - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomInvalidPassword(), + firstName: randomName(), + lastName: randomName(), + password: randomInvalidPassword(), }, ]; const TEST_EMAILS_TO_CREATE_USER_SHELLS = [ - utils.randomEmail(), - utils.randomEmail(), - utils.randomEmail(), + randomEmail(), + randomEmail(), + randomEmail(), ]; From ba2de0e4d06cdb1d2fc3d2e6b1af1935f301d506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:05:13 +0100 Subject: [PATCH 40/81] :truck: Move test types to own module --- packages/cli/test/integration/shared/types.d.ts | 6 ++++++ packages/cli/test/integration/shared/utils.ts | 7 ++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/cli/test/integration/shared/types.d.ts b/packages/cli/test/integration/shared/types.d.ts index cc6a1d43c4534..ad70b3eef01a2 100644 --- a/packages/cli/test/integration/shared/types.d.ts +++ b/packages/cli/test/integration/shared/types.d.ts @@ -1,3 +1,5 @@ +import type { N8nApp } from "../../../src/UserManagement/Interfaces"; + export type SmtpTestAccount = { user: string; pass: string; @@ -7,3 +9,7 @@ export type SmtpTestAccount = { secure: boolean; }; }; + +export type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner'; + +export type NamespacesMap = Readonly void>>; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 3eaaeae90cb6a..41fff72ad9152 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -17,8 +17,7 @@ import { authenticationMethods as authEndpoints } from '../../../src/UserManagem import { ownerNamespace as ownerEndpoints } from '../../../src/UserManagement/routes/owner'; import { getConnection } from 'typeorm'; import { issueJWT } from '../../../src/UserManagement/auth/jwt'; -import { N8nApp } from '../../../src/UserManagement/Interfaces'; -import type { SmtpTestAccount } from './types'; +import type { EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; /** * Get an SMTP test account from https://ethereal.email to test sending emails. @@ -27,8 +26,6 @@ export const getSmtpTestAccount = util.promisify(createTestAcco export const isTestRun = process.argv[1].split('/').includes('jest'); -type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner'; - /** * Initialize a test server to make requests to. * @@ -58,7 +55,7 @@ export function initTestServer({ } if (namespaces) { - const map: Readonly void>> = { + const map: NamespacesMap = { me: meEndpoints, users: usersEndpoints, auth: authEndpoints, From af204380ab492fafb6167ebe84ac461ccdbaf50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:07:02 +0100 Subject: [PATCH 41/81] :pencil2: Add dividers to utils --- packages/cli/test/integration/shared/utils.ts | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 41fff72ad9152..858b22e9cda29 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -19,12 +19,9 @@ import { getConnection } from 'typeorm'; import { issueJWT } from '../../../src/UserManagement/auth/jwt'; import type { EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; -/** - * Get an SMTP test account from https://ethereal.email to test sending emails. - */ -export const getSmtpTestAccount = util.promisify(createTestAccount); - -export const isTestRun = process.argv[1].split('/').includes('jest'); +// ---------------------------------- +// test server +// ---------------------------------- /** * Initialize a test server to make requests to. @@ -70,6 +67,10 @@ export function initTestServer({ return testServer.app; } +// ---------------------------------- +// test DB +// ---------------------------------- + export async function initTestDb() { await Db.init(); await getConnection().runMigrations({ transaction: 'none' }); @@ -81,6 +82,10 @@ export async function truncateUserTable() { await getConnection().query('PRAGMA foreign_keys=ON'); } +// ---------------------------------- +// request agent +// ---------------------------------- + export async function createAuthAgent(app: express.Application, user: User) { const agent = request.agent(app); agent.use(prefix(REST_PATH_SEGMENT)); @@ -101,8 +106,7 @@ export async function createAuthlessAgent(app: express.Application) { /** * Plugin to prefix a path segment into a request URL pathname. * - * Example: - * http://127.0.0.1:62100/me/password → http://127.0.0.1:62100/rest/me/password + * Example: http://127.0.0.1:62100/me/password → http://127.0.0.1:62100/rest/me/password */ export function prefix(pathSegment: string) { return function (request: superagent.SuperAgentRequest) { @@ -120,14 +124,6 @@ export function prefix(pathSegment: string) { }; } -export async function getHasOwnerSetting() { - const { value } = await Db.collections.Settings!.findOneOrFail({ - key: 'userManagement.hasOwner', - }); - - return Boolean(value); -} - /** * Extract the value (token) of the auth cookie in a response. */ @@ -148,3 +144,26 @@ export function getAuthToken(response: request.Response, authCookieName = 'n8n-a return match.groups.token; } + +// ---------------------------------- +// settings +// ---------------------------------- + +export async function getHasOwnerSetting() { + const { value } = await Db.collections.Settings!.findOneOrFail({ + key: 'userManagement.hasOwner', + }); + + return Boolean(value); +} + +// ---------------------------------- +// SMTP +// ---------------------------------- + +/** + * Get an SMTP test account from https://ethereal.email to test sending emails. + */ +export const getSmtpTestAccount = util.promisify(createTestAccount); + +export const isTestRun = process.argv[1].split('/').includes('jest'); From 403f1985b2d6493fdb67fadb8714ffcecb707ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:07:35 +0100 Subject: [PATCH 42/81] :pencil2: Reorder `initTestServer` param docstring --- packages/cli/test/integration/shared/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 858b22e9cda29..e13c8f9d09a9a 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -26,8 +26,8 @@ import type { EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types' /** * Initialize a test server to make requests to. * - * @param namespaces Namespaces of endpoints to apply to the test server. * @param applyAuth Whether to apply auth middleware to the test server. + * @param namespaces Namespaces of endpoints to apply to the test server. */ export function initTestServer({ applyAuth, From 6169636817bced0ab734c0495a610b8c8590bdce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:08:11 +0100 Subject: [PATCH 43/81] :pencil2: Add TODO comment --- packages/cli/test/integration/shared/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index e13c8f9d09a9a..58a66205699ea 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -166,4 +166,5 @@ export async function getHasOwnerSetting() { */ export const getSmtpTestAccount = util.promisify(createTestAccount); +// TODO: Phase out export const isTestRun = process.argv[1].split('/').includes('jest'); From 273a36b2e64523769d8bb943789c78182a84f820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:15:17 +0100 Subject: [PATCH 44/81] :zap: Dry up member creation --- packages/cli/test/integration/shared/utils.ts | 16 ++++ .../test/integration/users.endpoints.test.ts | 94 ++++--------------- 2 files changed, 34 insertions(+), 76 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 58a66205699ea..cb5b58551b843 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -5,6 +5,7 @@ import { URL } from 'url'; import bodyParser = require('body-parser'); import * as util from 'util'; import { createTestAccount } from 'nodemailer'; +import { v4 as uuid } from 'uuid'; import config = require('../../../config'); import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; @@ -17,7 +18,9 @@ import { authenticationMethods as authEndpoints } from '../../../src/UserManagem import { ownerNamespace as ownerEndpoints } from '../../../src/UserManagement/routes/owner'; import { getConnection } from 'typeorm'; import { issueJWT } from '../../../src/UserManagement/auth/jwt'; +import { randomEmail, randomValidPassword, randomName } from './random'; import type { EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; +import { Role } from '../../../src/databases/entities/Role'; // ---------------------------------- // test server @@ -82,6 +85,19 @@ export async function truncateUserTable() { await getConnection().query('PRAGMA foreign_keys=ON'); } +export async function createMember(globalMemberRole: Role) { + return await Db.collections.User!.save({ + id: uuid(), + email: randomEmail(), + password: randomValidPassword(), + firstName: randomName(), + lastName: randomName(), + createdAt: new Date(), + updatedAt: new Date(), + globalRole: globalMemberRole, + }); +} + // ---------------------------------- // request agent // ---------------------------------- diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index de1307da27c63..d4d5026dc838e 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -10,10 +10,17 @@ import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { getLogger } from '../../src/Logger'; import { LoggerProxy } from 'n8n-workflow'; import { Role } from '../../src/databases/entities/Role'; -import { randomEmail, randomValidPassword, randomName, randomInvalidPassword } from './shared/random'; +import { + randomEmail, + randomValidPassword, + randomName, + randomInvalidPassword, +} from './shared/random'; +import { createMember } from './shared/utils'; let app: express.Application; let globalOwnerRole: Role; +let globalMemberRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); @@ -25,6 +32,11 @@ beforeAll(async () => { scope: 'global', }); + globalMemberRole = await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); + config.set('logs.output', 'file'); // declutter console output const logger = getLogger(); LoggerProxy.init(logger); @@ -93,21 +105,7 @@ test('DELETE /users/:id should delete the user', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); - - const { id: idToDelete } = await Db.collections.User!.save({ - id: uuid(), - email: randomEmail(), - password: randomValidPassword(), - firstName: randomName(), - lastName: randomName(), - createdAt: new Date(), - updatedAt: new Date(), - globalRole: globalMemberRole, - }); + const { id: idToDelete } = await createMember(globalMemberRole); const response = await authOwnerAgent.delete(`/users/${idToDelete}`); @@ -128,21 +126,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () = const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); - - const { id: idToDelete } = await Db.collections.User!.save({ - id: uuid(), - email: randomEmail(), - password: randomValidPassword(), - firstName: randomName(), - lastName: randomName(), - createdAt: new Date(), - updatedAt: new Date(), - globalRole: globalMemberRole, - }); + const { id: idToDelete } = await createMember(globalMemberRole); const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({ transferId: idToDelete, @@ -198,21 +182,7 @@ test('GET /resolve-signup-token should validate invite token', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); - - const { id: inviteeId } = await Db.collections.User!.save({ - id: uuid(), - email: randomEmail(), - password: randomValidPassword(), - firstName: randomName(), - lastName: randomName(), - createdAt: new Date(), - updatedAt: new Date(), - globalRole: globalMemberRole, - }); + const { id: inviteeId } = await createMember(globalMemberRole); const response = await authOwnerAgent .get('/resolve-signup-token') @@ -234,21 +204,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); - - const { id: inviteeId } = await Db.collections.User!.save({ - id: uuid(), - email: randomEmail(), - password: randomValidPassword(), - firstName: randomName(), - lastName: randomName(), - createdAt: new Date(), - updatedAt: new Date(), - globalRole: globalMemberRole, - }); + const { id: inviteeId } = await createMember(globalMemberRole); const first = await authOwnerAgent .get('/resolve-signup-token') @@ -275,11 +231,6 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { test('POST /users/:id should fill out a user shell', async () => { const authlessAgent = await utils.createAuthlessAgent(app); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); - const userToFillOut = await Db.collections.User!.save({ email: randomEmail(), globalRole: globalMemberRole, @@ -319,11 +270,6 @@ test('POST /users/:id should fill out a user shell', async () => { test('POST /users/:id should fail with invalid inputs', async () => { const authlessAgent = await utils.createAuthlessAgent(app); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); - const userToFillOut = await Db.collections.User!.save({ email: randomEmail(), globalRole: globalMemberRole, @@ -493,8 +439,4 @@ const INVALID_FILL_OUT_USER_PAYLOADS = [ }, ]; -const TEST_EMAILS_TO_CREATE_USER_SHELLS = [ - randomEmail(), - randomEmail(), - randomEmail(), -]; +const TEST_EMAILS_TO_CREATE_USER_SHELLS = [randomEmail(), randomEmail(), randomEmail()]; From 771464b255e69028acc27867e6b0fb8fd4c09ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:21:23 +0100 Subject: [PATCH 45/81] :zap: Tighten search criteria --- packages/cli/test/integration/users.endpoints.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index d4d5026dc838e..79397bd6041e4 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -173,7 +173,10 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { expect(response.statusCode).toBe(200); - const shared = await Db.collections.SharedWorkflow!.findOneOrFail({ relations: ['user'] }); + const shared = await Db.collections.SharedWorkflow!.findOneOrFail({ + relations: ['user'], + where: { user: owner }, + }); expect(shared.user.id).toBe(owner.id); }); From fff3504ce480321d9e84906f6e50972569f4088f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:28:55 +0100 Subject: [PATCH 46/81] :test_tube: Add expectation to `GET /users` --- packages/cli/test/integration/users.endpoints.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 79397bd6041e4..62663f1a2d316 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -25,7 +25,7 @@ let globalMemberRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); await utils.initTestDb(); - await utils.truncateUserTable(); + globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', @@ -43,6 +43,8 @@ beforeAll(async () => { }); beforeEach(async () => { + await utils.truncateUserTable(); + jest.isolateModules(() => { jest.mock('../../config'); }); @@ -74,9 +76,12 @@ test('GET /users should return all users', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); + await createMember(globalMemberRole); + const response = await authOwnerAgent.get('/users'); expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(2); for (const user of response.body.data) { const { From d21d63d32c41d59f4bffe6dffc7fb7e413aa14a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:34:21 +0100 Subject: [PATCH 47/81] :zap: Create role fetcher utils --- .../test/integration/auth.endpoints.test.ts | 12 +++-------- .../cli/test/integration/me.endpoints.test.ts | 6 ++---- .../test/integration/owner.endpoints.test.ts | 6 ++---- packages/cli/test/integration/shared/utils.ts | 14 +++++++++++++ .../test/integration/users.endpoints.test.ts | 20 ++++++------------- 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 63804d840df33..4fd1089c2e49b 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -11,6 +11,7 @@ import { Db } from '../../src'; import { User } from '../../src/databases/entities/User'; import { Role } from '../../src/databases/entities/Role'; import { randomEmail, randomValidPassword, randomName } from './shared/random'; +import { getGlobalOwnerRole } from './shared/utils'; describe('auth endpoints', () => { describe('Owner requests', () => { @@ -24,14 +25,9 @@ describe('auth endpoints', () => { }); beforeEach(async () => { - globalOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); - - const newOwner = new User(); + globalOwnerRole = await getGlobalOwnerRole(); - Object.assign(newOwner, { + await Db.collections.User!.save({ id: uuid(), email: TEST_USER.email, firstName: TEST_USER.firstName, @@ -40,8 +36,6 @@ describe('auth endpoints', () => { globalRole: globalOwnerRole, }); - await Db.collections.User!.save(newOwner); - config.set('userManagement.hasOwner', true); await Db.collections.Settings!.update( diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 9eaed14ba476d..cfde49ee6f3e3 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -11,6 +11,7 @@ import { Db } from '../../src'; import { User } from '../../src/databases/entities/User'; import { Role } from '../../src/databases/entities/Role'; import { randomValidPassword, randomInvalidPassword, randomEmail, randomName, randomString } from './shared/random'; +import { getGlobalOwnerRole } from './shared/utils'; let globalOwnerRole: Role; @@ -23,10 +24,7 @@ describe('/me endpoints', () => { await utils.initTestDb(); await utils.truncateUserTable(); - globalOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); + globalOwnerRole = await getGlobalOwnerRole(); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index e3e96765f9cf5..5410d9193a8f4 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -8,6 +8,7 @@ import { Db } from '../../src'; import config = require('../../config'); import { Role } from '../../src/databases/entities/Role'; import { randomEmail, randomName, randomValidPassword, randomInvalidPassword } from './shared/random'; +import { getGlobalOwnerRole } from './shared/utils'; let globalOwnerRole: Role; @@ -20,10 +21,7 @@ describe('/owner endpoints', () => { await utils.initTestDb(); await utils.truncateUserTable(); - globalOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); + globalOwnerRole = await getGlobalOwnerRole(); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index cb5b58551b843..7e57363c3ad29 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -98,6 +98,20 @@ export async function createMember(globalMemberRole: Role) { }); } +export async function getGlobalOwnerRole() { + return await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); +} + +export async function getGlobalMemberRole() { + return await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); +} + // ---------------------------------- // request agent // ---------------------------------- diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 62663f1a2d316..6abf1b4b06a6b 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -16,7 +16,7 @@ import { randomName, randomInvalidPassword, } from './shared/random'; -import { createMember } from './shared/utils'; +import { createMember, getGlobalMemberRole, getGlobalOwnerRole } from './shared/utils'; let app: express.Application; let globalOwnerRole: Role; @@ -26,16 +26,8 @@ beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); await utils.initTestDb(); - - globalOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); - - globalMemberRole = await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); + globalOwnerRole = await getGlobalOwnerRole(); + globalMemberRole = await getGlobalMemberRole(); config.set('logs.output', 'file'); // declutter console output const logger = getLogger(); @@ -49,9 +41,6 @@ beforeEach(async () => { jest.mock('../../config'); }); - config.set('userManagement.hasOwner', true); - config.set('userManagement.emails.mode', ''); - await Db.collections.User!.save({ id: INITIAL_TEST_USER.id, email: INITIAL_TEST_USER.email, @@ -62,6 +51,9 @@ beforeEach(async () => { updatedAt: new Date(), globalRole: globalOwnerRole, }); + + config.set('userManagement.hasOwner', true); + config.set('userManagement.emails.mode', ''); }); afterEach(async () => { From ca46df3d3169a9018e2e1a908869202d8e793df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:36:41 +0100 Subject: [PATCH 48/81] :zap: Create one more role fetch util --- .../cli/test/integration/me.endpoints.test.ts | 19 +++++++++---------- packages/cli/test/integration/shared/utils.ts | 7 +++++++ .../test/integration/users.endpoints.test.ts | 7 ++----- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index cfde49ee6f3e3..9988537276006 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -10,7 +10,13 @@ import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { Db } from '../../src'; import { User } from '../../src/databases/entities/User'; import { Role } from '../../src/databases/entities/Role'; -import { randomValidPassword, randomInvalidPassword, randomEmail, randomName, randomString } from './shared/random'; +import { + randomValidPassword, + randomInvalidPassword, + randomEmail, + randomName, + randomString, +} from './shared/random'; import { getGlobalOwnerRole } from './shared/utils'; let globalOwnerRole: Role; @@ -334,14 +340,9 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); - - const newOwner = new User(); + const globalOwnerRole = await getGlobalOwnerRole(); - Object.assign(newOwner, { + await Db.collections.User!.save({ id: uuid(), email: TEST_USER.email, firstName: TEST_USER.firstName, @@ -350,8 +351,6 @@ describe('/me endpoints', () => { globalRole: globalOwnerRole, }); - await Db.collections.User!.save(newOwner); - config.set('userManagement.hasOwner', true); await Db.collections.Settings!.update( diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 7e57363c3ad29..24f9fafcff653 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -112,6 +112,13 @@ export async function getGlobalMemberRole() { }); } +export async function getWorkflowOwnerRole() { + return await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'workflow', + }); +} + // ---------------------------------- // request agent // ---------------------------------- diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 6abf1b4b06a6b..6dab87ad9dcde 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -16,7 +16,7 @@ import { randomName, randomInvalidPassword, } from './shared/random'; -import { createMember, getGlobalMemberRole, getGlobalOwnerRole } from './shared/utils'; +import { createMember, getGlobalMemberRole, getGlobalOwnerRole, getWorkflowOwnerRole } from './shared/utils'; let app: express.Application; let globalOwnerRole: Role; @@ -136,10 +136,7 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const workflowOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'workflow', - }); + const workflowOwnerRole = await getWorkflowOwnerRole(); const userToDelete = await Db.collections.User!.save({ id: uuid(), From 657a4aaeb5ec71d47071eadc28f67baf1a127701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:36:57 +0100 Subject: [PATCH 49/81] :fire: Remove unneeded DB query --- packages/cli/test/integration/me.endpoints.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 9988537276006..68be8195fc8a4 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -352,11 +352,6 @@ describe('/me endpoints', () => { }); config.set('userManagement.hasOwner', true); - - await Db.collections.Settings!.update( - { key: 'userManagement.hasOwner' }, - { value: JSON.stringify(true) }, - ); }); afterEach(async () => { From 4e58847290e0e2d37c3b331fe0b4ff4ad29e6c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:38:37 +0100 Subject: [PATCH 50/81] :test_tube: Add expectation to `POST /users` --- packages/cli/test/integration/users.endpoints.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 6dab87ad9dcde..54d5d7a169fdb 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -382,6 +382,9 @@ test('POST /users should ignore an empty payload', async () => { expect(response.statusCode).toBe(200); expect(Array.isArray(data)).toBe(true); expect(data.length).toBe(0); + + const users = await Db.collections.User!.find(); + expect(users.length).toBe(1); }); // TODO: UserManagementMailer is a singleton - cannot reinstantiate with wrong creds From 3519c0f49c21b0600528caefc5493aed3aa9c45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:51:54 +0100 Subject: [PATCH 51/81] :test_tube: Add expectation to `DELETE /users/:id` --- packages/cli/test/integration/users.endpoints.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 54d5d7a169fdb..344caecb0e6cc 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -130,6 +130,9 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () = }); expect(response.statusCode).toBe(400); + + const user = await Db.collections.User!.findOne(idToDelete); + expect(user).toBeDefined(); }); test('DELETE /users/:id with transferId should perform transfer', async () => { From 7c55e0373a890d79fa5a505b6b0168a338da6f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 11:53:22 +0100 Subject: [PATCH 52/81] :test_tube: Add another expectation to `DELETE /users/:id` --- packages/cli/test/integration/users.endpoints.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 344caecb0e6cc..8309629b3ddbb 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -117,6 +117,9 @@ test('DELETE /users/:id should fail to delete self', async () => { const response = await authOwnerAgent.delete(`/users/${owner.id}`); expect(response.statusCode).toBe(400); + + const user = await Db.collections.User!.findOne(owner.id); + expect(user).toBeDefined(); }); test('DELETE /users/:id should fail if user to delete is transferee', async () => { From 371e9ae5cc6c2cbc729fa0405117ee9c21b4135e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 12:24:34 +0100 Subject: [PATCH 53/81] :test_tube: Add expectations to `DELETE /users/:id` --- packages/cli/test/integration/shared/utils.ts | 7 +++ .../test/integration/users.endpoints.test.ts | 47 +++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 24f9fafcff653..018eb50f7f580 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -119,6 +119,13 @@ export async function getWorkflowOwnerRole() { }); } +export async function getCredentialOwnerRole() { + return await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'credential', + }); +} + // ---------------------------------- // request agent // ---------------------------------- diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 8309629b3ddbb..458f44c9c74aa 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -16,7 +16,15 @@ import { randomName, randomInvalidPassword, } from './shared/random'; -import { createMember, getGlobalMemberRole, getGlobalOwnerRole, getWorkflowOwnerRole } from './shared/utils'; +import { + createMember, + getCredentialOwnerRole, + getGlobalMemberRole, + getGlobalOwnerRole, + getWorkflowOwnerRole, +} from './shared/utils'; +import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; +import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; let app: express.Application; let globalOwnerRole: Role; @@ -143,6 +151,7 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { const authOwnerAgent = await utils.createAuthAgent(app, owner); const workflowOwnerRole = await getWorkflowOwnerRole(); + const credentialOwnerRole = await getCredentialOwnerRole(); const userToDelete = await Db.collections.User!.save({ id: uuid(), @@ -155,30 +164,60 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { globalRole: workflowOwnerRole, }); - const savedWorkflow = await Db.collections.Workflow!.save({ + const newWorkflow = new WorkflowEntity(); + + Object.assign(newWorkflow, { name: randomName(), active: false, connections: {}, }); + const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow); + await Db.collections.SharedWorkflow!.save({ role: workflowOwnerRole, user: userToDelete, workflow: savedWorkflow, }); + const newCredential = new CredentialsEntity(); + + Object.assign(newCredential, { + name: randomName(), + data: '', + type: '', + nodesAccess: [], + }); + + const savedCredential = await Db.collections.Credentials!.save(newCredential); + + await Db.collections.SharedCredentials!.save({ + role: credentialOwnerRole, + user: userToDelete, + credentials: savedCredential, + }); + const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`).query({ transferId: owner.id, }); expect(response.statusCode).toBe(200); - const shared = await Db.collections.SharedWorkflow!.findOneOrFail({ + const sharedWorkflow = await Db.collections.SharedWorkflow!.findOneOrFail({ + relations: ['user'], + where: { user: owner }, + }); + + const sharedCredential = await Db.collections.SharedCredentials!.findOneOrFail({ relations: ['user'], where: { user: owner }, }); - expect(shared.user.id).toBe(owner.id); + const deletedUser = await Db.collections.User!.findOne(userToDelete); + + expect(sharedWorkflow.user.id).toBe(owner.id); + expect(sharedCredential.user.id).toBe(owner.id); + expect(deletedUser).toBeUndefined(); }); test('GET /resolve-signup-token should validate invite token', async () => { From 856a96ce349e9fab72632401aa458204b546e35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 12:26:41 +0100 Subject: [PATCH 54/81] :test_tube: Adjust expectations in `POST /users/:id` --- packages/cli/test/integration/users.endpoints.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 458f44c9c74aa..09a7a36391f1c 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -280,8 +280,8 @@ test('POST /users/:id should fill out a user shell', async () => { const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send({ inviterId: INITIAL_TEST_USER.id, - firstName: randomName(), - lastName: randomName(), + firstName: INITIAL_TEST_USER.firstName, + lastName: INITIAL_TEST_USER.lastName, password: randomValidPassword(), }); @@ -298,8 +298,8 @@ test('POST /users/:id should fill out a user shell', async () => { expect(validator.isUUID(id)).toBe(true); expect(email).toBeDefined(); - expect(firstName).toBeDefined(); - expect(lastName).toBeDefined(); + expect(firstName).toBe(INITIAL_TEST_USER.firstName); + expect(lastName).toBe(INITIAL_TEST_USER.lastName); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(resetPasswordToken).toBeUndefined(); From e5fc78511d35a7ab4eca83c8c10243e909962678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 12:32:37 +0100 Subject: [PATCH 55/81] :test_tube: Add expectations to `DELETE /users/:id` --- .../test/integration/users.endpoints.test.ts | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 09a7a36391f1c..41e62f9c7014b 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -29,6 +29,8 @@ import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; let app: express.Application; let globalOwnerRole: Role; let globalMemberRole: Role; +let workflowOwnerRole: Role; +let credentialOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); @@ -36,6 +38,8 @@ beforeAll(async () => { globalOwnerRole = await getGlobalOwnerRole(); globalMemberRole = await getGlobalMemberRole(); + workflowOwnerRole = await getWorkflowOwnerRole(); + credentialOwnerRole = await getCredentialOwnerRole(); config.set('logs.output', 'file'); // declutter console output const logger = getLogger(); @@ -110,12 +114,66 @@ test('DELETE /users/:id should delete the user', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const { id: idToDelete } = await createMember(globalMemberRole); + const userToDelete = await createMember(globalMemberRole); + + const newWorkflow = new WorkflowEntity(); + + Object.assign(newWorkflow, { + name: randomName(), + active: false, + connections: {}, + }); + + const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow); + + await Db.collections.SharedWorkflow!.save({ + role: workflowOwnerRole, + user: userToDelete, + workflow: savedWorkflow, + }); + + const newCredential = new CredentialsEntity(); + + Object.assign(newCredential, { + name: randomName(), + data: '', + type: '', + nodesAccess: [], + }); + + const savedCredential = await Db.collections.Credentials!.save(newCredential); + + await Db.collections.SharedCredentials!.save({ + role: credentialOwnerRole, + user: userToDelete, + credentials: savedCredential, + }); - const response = await authOwnerAgent.delete(`/users/${idToDelete}`); + const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`); expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); + + const user = await Db.collections.User!.findOne(userToDelete.id); + expect(user).toBeUndefined(); + + const sharedWorkflow = await Db.collections.SharedWorkflow!.findOne({ + relations: ['user'], + where: { user: userToDelete }, + }); + expect(sharedWorkflow).toBeUndefined(); + + const sharedCredential = await Db.collections.SharedCredentials!.findOne({ + relations: ['user'], + where: { user: userToDelete }, + }); + expect(sharedCredential).toBeUndefined(); + + const workflow = await Db.collections.Workflow!.findOne(savedWorkflow.id); + expect(workflow).toBeUndefined(); + + const credential = await Db.collections.Credentials!.findOne(savedCredential.id); + expect(credential).toBeUndefined(); }); test('DELETE /users/:id should fail to delete self', async () => { @@ -150,9 +208,6 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const workflowOwnerRole = await getWorkflowOwnerRole(); - const credentialOwnerRole = await getCredentialOwnerRole(); - const userToDelete = await Db.collections.User!.save({ id: uuid(), email: randomEmail(), From d9cace9cbdf935ff5fe4b31f032044034c47855f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 12:53:11 +0100 Subject: [PATCH 56/81] :blue_book: Add namespace name to type --- packages/cli/test/integration/shared/types.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/shared/types.d.ts b/packages/cli/test/integration/shared/types.d.ts index ad70b3eef01a2..be577afde2895 100644 --- a/packages/cli/test/integration/shared/types.d.ts +++ b/packages/cli/test/integration/shared/types.d.ts @@ -10,6 +10,6 @@ export type SmtpTestAccount = { }; }; -export type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner'; +export type EndpointNamespace = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset'; export type NamespacesMap = Readonly void>>; From e90a71acf526a9dd9a56d4a8977ef0a47e053007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 12:55:49 +0100 Subject: [PATCH 57/81] :truck: Adjust imports --- .../passwordReset.endpoints.test.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index d6377fa7fd697..d1afafbd973e1 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -6,6 +6,7 @@ import * as utils from './shared/utils'; import { Db } from '../../src'; import config = require('../../config'); import { compare } from 'bcryptjs'; +import { randomEmail, randomInvalidPassword, randomName, randomValidPassword } from './shared/random'; let app: express.Application; @@ -91,11 +92,11 @@ test('POST /forgot-password should fail with invalid inputs', async () => { config.set('userManagement.emails.mode', 'smtp'); const invalidPayloads = [ - utils.randomEmail(), - [utils.randomEmail()], + randomEmail(), + [randomEmail()], {}, - [{ name: utils.randomName() }], - [{ email: utils.randomName() }], + [{ name: randomName() }], + [{ email: randomName() }], ]; for (const invalidPayload of invalidPayloads) { @@ -112,7 +113,7 @@ test('POST /forgot-password should fail if user is not found', async () => { const response = await authOwnerAgent .post('/forgot-password') - .send({ email: utils.randomEmail() }); + .send({ email: randomEmail() }); expect(response.statusCode).toBe(404); }); @@ -169,7 +170,7 @@ test('POST /change-password should succeed with valid inputs', async () => { const resetPasswordToken = uuid(); await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); - const passwordToSet = utils.randomValidPassword(); + const passwordToSet = randomValidPassword(); const response = await authOwnerAgent.post('/change-password').send({ token: resetPasswordToken, @@ -200,18 +201,18 @@ test('POST /change-password should fail with invalid inputs', async () => { const invalidPayloads = [ { token: uuid() }, { id: INITIAL_TEST_USER.id }, - { password: utils.randomValidPassword() }, + { password: randomValidPassword() }, { token: uuid(), id: INITIAL_TEST_USER.id }, - { token: uuid(), password: utils.randomValidPassword() }, - { id: INITIAL_TEST_USER.id, password: utils.randomValidPassword() }, + { token: uuid(), password: randomValidPassword() }, + { id: INITIAL_TEST_USER.id, password: randomValidPassword() }, { id: INITIAL_TEST_USER.id, - password: utils.randomInvalidPassword(), + password: randomInvalidPassword(), token: resetPasswordToken, }, { id: INITIAL_TEST_USER.id, - password: utils.randomValidPassword(), + password: randomValidPassword(), token: uuid(), }, ]; @@ -224,8 +225,8 @@ test('POST /change-password should fail with invalid inputs', async () => { const INITIAL_TEST_USER = { id: uuid(), - email: utils.randomEmail(), - firstName: utils.randomName(), - lastName: utils.randomName(), - password: utils.randomValidPassword(), + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), }; From e317143b6a302f9a4f9dfec7ce5bccb53a2ba7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 12:56:41 +0100 Subject: [PATCH 58/81] :zap: Optimize `globalOwnerRole` fetching --- .../passwordReset.endpoints.test.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index d1afafbd973e1..632f8285418fa 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -6,14 +6,26 @@ import * as utils from './shared/utils'; import { Db } from '../../src'; import config = require('../../config'); import { compare } from 'bcryptjs'; -import { randomEmail, randomInvalidPassword, randomName, randomValidPassword } from './shared/random'; +import { + randomEmail, + randomInvalidPassword, + randomName, + randomValidPassword, +} from './shared/random'; +import { Role } from '../../src/databases/entities/Role'; let app: express.Application; +let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); + + globalOwnerRole = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); }); beforeEach(async () => { @@ -24,11 +36,6 @@ beforeEach(async () => { config.set('userManagement.hasOwner', true); config.set('userManagement.emails.mode', ''); - const globalOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); - await Db.collections.User!.save({ id: INITIAL_TEST_USER.id, email: INITIAL_TEST_USER.email, @@ -111,9 +118,7 @@ test('POST /forgot-password should fail if user is not found', async () => { config.set('userManagement.emails.mode', 'smtp'); - const response = await authOwnerAgent - .post('/forgot-password') - .send({ email: randomEmail() }); + const response = await authOwnerAgent.post('/forgot-password').send({ email: randomEmail() }); expect(response.statusCode).toBe(404); }); From fea08842bd4c308c17030167bc72c9fac42f7a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 13:01:47 +0100 Subject: [PATCH 59/81] :test_tube: Add expectations --- .../integration/passwordReset.endpoints.test.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 632f8285418fa..d3272b9e36f92 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -79,6 +79,9 @@ test('POST /forgot-password should send password reset email', async () => { expect(response.statusCode).toBe(200); expect(response.body).toEqual({}); + + const shell = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(shell.resetPasswordToken).toBeDefined(); }); test('POST /forgot-password should fail if emailing is not set up', async () => { @@ -90,6 +93,9 @@ test('POST /forgot-password should fail if emailing is not set up', async () => .send({ email: INITIAL_TEST_USER.email }); expect(response.statusCode).toBe(500); + + const shell = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(shell.resetPasswordToken).toBeNull(); }); test('POST /forgot-password should fail with invalid inputs', async () => { @@ -109,6 +115,9 @@ test('POST /forgot-password should fail with invalid inputs', async () => { for (const invalidPayload of invalidPayloads) { const response = await authOwnerAgent.post('/forgot-password').send(invalidPayload); expect(response.statusCode).toBe(400); + + const shell = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(shell.resetPasswordToken).toBeNull(); } }); @@ -175,12 +184,12 @@ test('POST /change-password should succeed with valid inputs', async () => { const resetPasswordToken = uuid(); await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); - const passwordToSet = randomValidPassword(); + const passwordToSend = randomValidPassword(); const response = await authOwnerAgent.post('/change-password').send({ token: resetPasswordToken, id: INITIAL_TEST_USER.id, - password: passwordToSet, + password: passwordToSend, }); expect(response.statusCode).toBe(200); @@ -192,8 +201,9 @@ test('POST /change-password should succeed with valid inputs', async () => { INITIAL_TEST_USER.id, ); - const comparisonResult = await compare(passwordToSet, storedPassword!); + const comparisonResult = await compare(passwordToSend, storedPassword!); expect(comparisonResult).toBe(true); + expect(storedPassword).not.toBe(passwordToSend); }); test('POST /change-password should fail with invalid inputs', async () => { From cb460f75a8b4e6bc5d63a7ec0f75cddf7c7bad87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 13:03:42 +0100 Subject: [PATCH 60/81] :shirt: Fix build --- packages/cli/src/Server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 11b2406e50e76..4f55b7b4b877b 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -622,7 +622,7 @@ class App { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { // Allow access also from frontend when developing res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); - res.header('Access-Control-Allow-Credentials', true); + res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.header( 'Access-Control-Allow-Headers', From 579867d0a28b7230498361d344bc922f825d4743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 13:04:08 +0100 Subject: [PATCH 61/81] :shirt: Fix build --- packages/cli/src/Server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 11b2406e50e76..4f55b7b4b877b 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -622,7 +622,7 @@ class App { this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { // Allow access also from frontend when developing res.header('Access-Control-Allow-Origin', 'http://localhost:8080'); - res.header('Access-Control-Allow-Credentials', true); + res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.header( 'Access-Control-Allow-Headers', From a3813ffe68bc4a0f05869c54c8c3f120b9f1150d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 13:13:53 +0100 Subject: [PATCH 62/81] :zap: Update method --- packages/cli/test/integration/auth.endpoints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 43795aa5eb59e..66f911c7fcc36 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -127,7 +127,7 @@ describe('auth endpoints', () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const response = await authOwnerAgent.get('/logout'); + const response = await authOwnerAgent.post('/logout'); expect(response.statusCode).toBe(200); expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY); From f4820af7e9d94e8a2dc81e863ca7033026d0e9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 13:14:10 +0100 Subject: [PATCH 63/81] :zap: Update method --- packages/cli/test/integration/auth.endpoints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 43795aa5eb59e..66f911c7fcc36 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -127,7 +127,7 @@ describe('auth endpoints', () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAuthAgent(app, owner); - const response = await authOwnerAgent.get('/logout'); + const response = await authOwnerAgent.post('/logout'); expect(response.statusCode).toBe(200); expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY); From 9786175cbfacfa13214a312090c1caeebc7d575d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 14 Feb 2022 18:19:48 +0100 Subject: [PATCH 64/81] :test_tube: Fix `POST /change-password` test --- packages/cli/test/integration/passwordReset.endpoints.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index d3272b9e36f92..8b96498d3a21b 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -188,7 +188,7 @@ test('POST /change-password should succeed with valid inputs', async () => { const response = await authOwnerAgent.post('/change-password').send({ token: resetPasswordToken, - id: INITIAL_TEST_USER.id, + userId: INITIAL_TEST_USER.id, password: passwordToSend, }); From df7b4ca6eec52bc0cae0cf6b94390185da5b9af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 11:39:36 +0100 Subject: [PATCH 65/81] :blue_book: Fix `userToDelete` type --- packages/cli/src/UserManagement/routes/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index b5adde0dc1d68..b576c48e23340 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -252,7 +252,7 @@ export function usersNamespace(this: N8nApp): void { throw new ResponseHelper.ResponseError('Could not find user', undefined, 404); } - const userToDelete = users.find((user) => user.id === req.params.id)!; + const userToDelete = users.find((user) => user.id === req.params.id) as User; if (transferId) { const transferee = users.find((user) => user.id === transferId); From c32423f439dabe9282766b0073a8ee849997317f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 11:48:17 +0100 Subject: [PATCH 66/81] :zap: Refactor `createAgent()` --- .../test/integration/auth.endpoints.test.ts | 6 ++-- .../cli/test/integration/me.endpoints.test.ts | 28 ++++++++--------- .../test/integration/owner.endpoints.test.ts | 4 +-- packages/cli/test/integration/shared/utils.ts | 18 +++++------ .../test/integration/users.endpoints.test.ts | 30 +++++++++---------- 5 files changed, 42 insertions(+), 44 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 66f911c7fcc36..f23b91c437011 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -53,7 +53,7 @@ describe('auth endpoints', () => { }); test('POST /login should log user in', async () => { - const authlessAgent = await utils.createAuthlessAgent(app); + const authlessAgent = await utils.createAgent(app, { auth: false }); const response = await authlessAgent.post('/login').send({ email: TEST_USER.email, @@ -91,7 +91,7 @@ describe('auth endpoints', () => { test('GET /login should receive logged in user', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const response = await authOwnerAgent.get('/login'); @@ -125,7 +125,7 @@ describe('auth endpoints', () => { test('POST /logout should log user out', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const response = await authOwnerAgent.post('/logout'); diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 68be8195fc8a4..0e2a4175eb96d 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -52,7 +52,7 @@ describe('/me endpoints', () => { test('GET /me should return sanitized shell', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const authShellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAgent(app, { auth: true, user: shell }); const response = await authShellAgent.get('/me'); @@ -82,7 +82,7 @@ describe('/me endpoints', () => { test('PATCH /me should succeed with valid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const authShellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAgent(app, { auth: true, user: shell }); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { const response = await authShellAgent.patch('/me').send(validPayload); @@ -114,7 +114,7 @@ describe('/me endpoints', () => { test('PATCH /me should fail with invalid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const authShellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAgent(app, { auth: true, user: shell }); for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { const response = await authShellAgent.patch('/me').send(invalidPayload); @@ -124,7 +124,7 @@ describe('/me endpoints', () => { test('PATCH /me/password should succeed with valid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const authShellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAgent(app, { auth: true, user: shell }); const validPayloads = Array.from({ length: 3 }, () => ({ password: randomValidPassword(), @@ -139,7 +139,7 @@ describe('/me endpoints', () => { test('PATCH /me/password should fail with invalid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const authShellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAgent(app, { auth: true, user: shell }); const invalidPayloads = [ ...Array.from({ length: 3 }, () => ({ password: randomInvalidPassword() })), @@ -156,7 +156,7 @@ describe('/me endpoints', () => { test('POST /me/survey should succeed with valid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const authShellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAgent(app, { auth: true, user: shell }); const validPayloads = [SURVEY, {}]; @@ -214,7 +214,7 @@ describe('/me endpoints', () => { test('GET /me should return sanitized member', async () => { const member = await Db.collections.User!.findOneOrFail(); - const authMemberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAgent(app, { auth: true, user: member }); const response = await authMemberAgent.get('/me'); @@ -244,7 +244,7 @@ describe('/me endpoints', () => { test('PATCH /me should succeed with valid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const authMemberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAgent(app, { auth: true, user: member }); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { const response = await authMemberAgent.patch('/me').send(validPayload); @@ -276,7 +276,7 @@ describe('/me endpoints', () => { test('PATCH /me should fail with invalid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const authMemberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAgent(app, { auth: true, user: member }); for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { const response = await authMemberAgent.patch('/me').send(invalidPayload); @@ -286,7 +286,7 @@ describe('/me endpoints', () => { test('PATCH /me/password should succeed with valid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const authMemberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAgent(app, { auth: true, user: member }); const validPayloads = Array.from({ length: 3 }, () => ({ password: randomValidPassword(), @@ -301,7 +301,7 @@ describe('/me endpoints', () => { test('PATCH /me/password should fail with invalid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const authMemberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAgent(app, { auth: true, user: member }); const invalidPayloads = [ ...Array.from({ length: 3 }, () => ({ password: randomInvalidPassword() })), @@ -318,7 +318,7 @@ describe('/me endpoints', () => { test('POST /me/survey should succeed with valid inputs', async () => { const member = await Db.collections.User!.findOneOrFail(); - const authMemberAgent = await utils.createAuthAgent(app, member); + const authMemberAgent = await utils.createAgent(app, { auth: true, user: member }); const validPayloads = [SURVEY, {}]; @@ -364,7 +364,7 @@ describe('/me endpoints', () => { test('GET /me should return sanitized owner', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const response = await authOwnerAgent.get('/me'); @@ -394,7 +394,7 @@ describe('/me endpoints', () => { test('PATCH /me should succeed with valid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { const response = await authOwnerAgent.patch('/me').send(validPayload); diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 5410d9193a8f4..42c7c1bd385ea 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -43,7 +43,7 @@ describe('/owner endpoints', () => { test('POST /owner should create owner and enable hasOwner setting', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const authShellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAgent(app, { auth: true, user: shell }); const response = await authShellAgent.post('/owner').send(TEST_USER); @@ -82,7 +82,7 @@ describe('/owner endpoints', () => { test('POST /owner should fail with invalid inputs', async () => { const shell = await Db.collections.User!.findOneOrFail(); - const authShellAgent = await utils.createAuthAgent(app, shell); + const authShellAgent = await utils.createAgent(app, { auth: true, user: shell }); for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) { const response = await authShellAgent.post('/owner').send(invalidPayload); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 018eb50f7f580..4fb67a1b70479 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -130,19 +130,17 @@ export async function getCredentialOwnerRole() { // request agent // ---------------------------------- -export async function createAuthAgent(app: express.Application, user: User) { +export async function createAgent( + app: express.Application, + { auth, user }: { auth: boolean; user?: User } = { auth: false }, +) { const agent = request.agent(app); agent.use(prefix(REST_PATH_SEGMENT)); - const { token } = await issueJWT(user); - agent.jar.setCookie(`n8n-auth=${token}`); - - return agent; -} - -export async function createAuthlessAgent(app: express.Application) { - const agent = request.agent(app); - agent.use(prefix(REST_PATH_SEGMENT)); + if (auth && user) { + const { token } = await issueJWT(user); + agent.jar.setCookie(`n8n-auth=${token}`); + } return agent; } diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index b6037ada22851..9469748375758 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -78,7 +78,7 @@ afterAll(() => { test('GET /users should return all users', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); await createMember(globalMemberRole); @@ -112,7 +112,7 @@ test('GET /users should return all users', async () => { test('DELETE /users/:id should delete the user', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const userToDelete = await createMember(globalMemberRole); @@ -178,7 +178,7 @@ test('DELETE /users/:id should delete the user', async () => { test('DELETE /users/:id should fail to delete self', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const response = await authOwnerAgent.delete(`/users/${owner.id}`); @@ -190,7 +190,7 @@ test('DELETE /users/:id should fail to delete self', async () => { test('DELETE /users/:id should fail if user to delete is transferee', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const { id: idToDelete } = await createMember(globalMemberRole); @@ -206,7 +206,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () = test('DELETE /users/:id with transferId should perform transfer', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const userToDelete = await Db.collections.User!.save({ id: uuid(), @@ -277,7 +277,7 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { test('GET /resolve-signup-token should validate invite token', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const { id: inviteeId } = await createMember(globalMemberRole); @@ -299,7 +299,7 @@ test('GET /resolve-signup-token should validate invite token', async () => { test('GET /resolve-signup-token should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const { id: inviteeId } = await createMember(globalMemberRole); @@ -326,7 +326,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { }); test('POST /users/:id should fill out a user shell', async () => { - const authlessAgent = await utils.createAuthlessAgent(app); + const authlessAgent = await utils.createAgent(app, { auth: false }); const userToFillOut = await Db.collections.User!.save({ email: randomEmail(), @@ -365,7 +365,7 @@ test('POST /users/:id should fill out a user shell', async () => { }); test('POST /users/:id should fail with invalid inputs', async () => { - const authlessAgent = await utils.createAuthlessAgent(app); + const authlessAgent = await utils.createAgent(app, { auth: false }); const userToFillOut = await Db.collections.User!.save({ email: randomEmail(), @@ -379,7 +379,7 @@ test('POST /users/:id should fail with invalid inputs', async () => { }); test('POST /users/:id should fail with already accepted invite', async () => { - const authlessAgent = await utils.createAuthlessAgent(app); + const authlessAgent = await utils.createAgent(app, { auth: false }); const globalMemberRole = await Db.collections.Role!.findOneOrFail({ name: 'member', @@ -404,7 +404,7 @@ test('POST /users/:id should fail with already accepted invite', async () => { test('POST /users should fail if emailing is not set up', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]); @@ -413,7 +413,7 @@ test('POST /users should fail if emailing is not set up', async () => { test('POST /users should email invites and create user shells', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); const { user, @@ -451,7 +451,7 @@ test('POST /users should email invites and create user shells', async () => { test('POST /users should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); config.set('userManagement.emails.mode', 'smtp'); @@ -471,7 +471,7 @@ test('POST /users should fail with invalid inputs', async () => { test('POST /users should ignore an empty payload', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); config.set('userManagement.emails.mode', 'smtp'); @@ -490,7 +490,7 @@ test('POST /users should ignore an empty payload', async () => { // TODO: UserManagementMailer is a singleton - cannot reinstantiate with wrong creds // test('POST /users should error for wrong SMTP config', async () => { // const owner = await Db.collections.User!.findOneOrFail(); -// const authOwnerAgent = await utils.createAuthAgent(app, owner); +// const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); // config.set('userManagement.emails.mode', 'smtp'); // config.set('userManagement.emails.smtp.host', 'XYZ'); // break SMTP config From 2dc483855d9b26cca3c09df99db9069c0b69f4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 11:52:02 +0100 Subject: [PATCH 67/81] :zap: Make role fetching global --- packages/cli/test/integration/auth.endpoints.test.ts | 7 ++++--- packages/cli/test/integration/me.endpoints.test.ts | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index f23b91c437011..a6f6fbe5c87a7 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -13,20 +13,21 @@ import { Role } from '../../src/databases/entities/Role'; import { randomEmail, randomValidPassword, randomName } from './shared/random'; import { getGlobalOwnerRole } from './shared/utils'; +let globalOwnerRole: Role; + describe('auth endpoints', () => { describe('Owner requests', () => { let app: express.Application; - let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['auth'], applyAuth: true }); await utils.initTestDb(); await utils.truncateUserTable(); - }); - beforeEach(async () => { globalOwnerRole = await getGlobalOwnerRole(); + }); + beforeEach(async () => { await Db.collections.User!.save({ id: uuid(), email: TEST_USER.email, diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 0e2a4175eb96d..9d250b274c2b5 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -340,8 +340,6 @@ describe('/me endpoints', () => { }); beforeEach(async () => { - const globalOwnerRole = await getGlobalOwnerRole(); - await Db.collections.User!.save({ id: uuid(), email: TEST_USER.email, From 2c4579bde47059474e3d142f8342a2f0a1677669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 12:02:17 +0100 Subject: [PATCH 68/81] :zap: Optimize roles fetching --- packages/cli/test/integration/shared/utils.ts | 24 +++++++++++++++++++ .../test/integration/users.endpoints.test.ts | 23 +++++++++--------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 4fb67a1b70479..7383a9981bc35 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -126,6 +126,30 @@ export async function getCredentialOwnerRole() { }); } +export async function getAllRoles() { + const roles = await Promise.all([ + getGlobalOwnerRole(), + getGlobalMemberRole(), + getWorkflowOwnerRole(), + getCredentialOwnerRole(), + ]); + + return { + globalOwnerRole: roles.find( + ({ scope, name }) => scope === 'global' && name === 'owner', + ) as Role, + globalMemberRole: roles.find( + ({ scope, name }) => scope === 'global' && name === 'member', + ) as Role, + workflowOwnerRole: roles.find( + ({ scope, name }) => scope === 'workflow' && name === 'owner', + ) as Role, + credentialOwnerRole: roles.find( + ({ scope, name }) => scope === 'credential' && name === 'owner', + ) as Role, + }; +} + // ---------------------------------- // request agent // ---------------------------------- diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 9469748375758..d15687b7a073e 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -16,13 +16,7 @@ import { randomName, randomInvalidPassword, } from './shared/random'; -import { - createMember, - getCredentialOwnerRole, - getGlobalMemberRole, - getGlobalOwnerRole, - getWorkflowOwnerRole, -} from './shared/utils'; +import { createMember } from './shared/utils'; import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; @@ -36,10 +30,17 @@ beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); await utils.initTestDb(); - globalOwnerRole = await getGlobalOwnerRole(); - globalMemberRole = await getGlobalMemberRole(); - workflowOwnerRole = await getWorkflowOwnerRole(); - credentialOwnerRole = await getCredentialOwnerRole(); + const { + globalOwnerRole: fetchedGlobalOwnerRole, + globalMemberRole: fetchedGlobalMemberRole, + workflowOwnerRole: fetchedWorkflowOwnerRole, + credentialOwnerRole: fetchedCredentialOwnerRole, + } = await utils.getAllRoles(); + + globalOwnerRole = fetchedGlobalOwnerRole; + globalMemberRole = fetchedGlobalMemberRole; + workflowOwnerRole = fetchedWorkflowOwnerRole; + credentialOwnerRole = fetchedCredentialOwnerRole; config.set('logs.output', 'file'); // declutter console output const logger = getLogger(); From 9e69323f2a35927190732ac7ea1482114fb4de36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 13:44:45 +0100 Subject: [PATCH 69/81] :zap: Centralize member creation --- .../test/integration/auth.endpoints.test.ts | 4 +-- .../cli/test/integration/me.endpoints.test.ts | 8 +---- .../test/integration/owner.endpoints.test.ts | 8 +---- packages/cli/test/integration/shared/utils.ts | 33 +++++++++++++++++-- .../test/integration/users.endpoints.test.ts | 18 +++++----- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index a6f6fbe5c87a7..477c366b3bf08 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -28,13 +28,13 @@ describe('auth endpoints', () => { }); beforeEach(async () => { - await Db.collections.User!.save({ + await utils.createUser({ id: uuid(), email: TEST_USER.email, firstName: TEST_USER.firstName, lastName: TEST_USER.lastName, password: hashSync(TEST_USER.password, genSaltSync(10)), - globalRole: globalOwnerRole, + role: globalOwnerRole, }); config.set('userManagement.hasOwner', true); diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 9d250b274c2b5..774b2064b22a6 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -28,18 +28,12 @@ describe('/me endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); - await utils.truncateUserTable(); globalOwnerRole = await getGlobalOwnerRole(); }); beforeEach(async () => { - await Db.collections.User!.save({ - id: uuid(), - createdAt: new Date(), - updatedAt: new Date(), - globalRole: globalOwnerRole, - }); + await utils.createOwnerShell(); }); afterEach(async () => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 42c7c1bd385ea..0fa2a25e0fb9d 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -19,18 +19,12 @@ describe('/owner endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['owner'], applyAuth: true }); await utils.initTestDb(); - await utils.truncateUserTable(); globalOwnerRole = await getGlobalOwnerRole(); }); beforeEach(async () => { - await Db.collections.User!.save({ - id: uuid(), - createdAt: new Date(), - updatedAt: new Date(), - globalRole: globalOwnerRole, - }); + await utils.createOwnerShell(); }); afterEach(async () => { diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 7383a9981bc35..77524ea83dc57 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -85,16 +85,43 @@ export async function truncateUserTable() { await getConnection().query('PRAGMA foreign_keys=ON'); } -export async function createMember(globalMemberRole: Role) { - return await Db.collections.User!.save({ +/** + * Store a user in the DB, defaulting to a `member`. + */ +export async function createUser( + { + id, + email, + password, + firstName, + lastName, + role, + }: { id: string; email: string; password: string; firstName: string; lastName: string; role?: Role } = { id: uuid(), email: randomEmail(), password: randomValidPassword(), firstName: randomName(), lastName: randomName(), + }, +) { + return await Db.collections.User!.save({ + id, + email, + password, + firstName, + lastName, + createdAt: new Date(), + updatedAt: new Date(), + globalRole: role ?? (await getGlobalMemberRole()), + }); +} + +export async function createOwnerShell() { + await Db.collections.User!.save({ + id: uuid(), createdAt: new Date(), updatedAt: new Date(), - globalRole: globalMemberRole, + globalRole: await getGlobalOwnerRole(), }); } diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index d15687b7a073e..7d56ccd8cfdb5 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -16,7 +16,7 @@ import { randomName, randomInvalidPassword, } from './shared/random'; -import { createMember } from './shared/utils'; +import { createUser } from './shared/utils'; import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; @@ -54,15 +54,13 @@ beforeEach(async () => { jest.mock('../../config'); }); - await Db.collections.User!.save({ + await 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, - createdAt: new Date(), - updatedAt: new Date(), - globalRole: globalOwnerRole, + role: globalOwnerRole, }); config.set('userManagement.hasOwner', true); @@ -81,7 +79,7 @@ test('GET /users should return all users', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); - await createMember(globalMemberRole); + await createUser(); const response = await authOwnerAgent.get('/users'); @@ -115,7 +113,7 @@ test('DELETE /users/:id should delete the user', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); - const userToDelete = await createMember(globalMemberRole); + const userToDelete = await createUser(); const newWorkflow = new WorkflowEntity(); @@ -193,7 +191,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () = const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); - const { id: idToDelete } = await createMember(globalMemberRole); + const { id: idToDelete } = await createUser(); const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({ transferId: idToDelete, @@ -280,7 +278,7 @@ test('GET /resolve-signup-token should validate invite token', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); - const { id: inviteeId } = await createMember(globalMemberRole); + const { id: inviteeId } = await createUser(); const response = await authOwnerAgent .get('/resolve-signup-token') @@ -302,7 +300,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); - const { id: inviteeId } = await createMember(globalMemberRole); + const { id: inviteeId } = await createUser(); const first = await authOwnerAgent .get('/resolve-signup-token') From ded666369821c6a8361ae315d9d3fa9d31bdddfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 13:49:04 +0100 Subject: [PATCH 70/81] :zap: Refactor truncation helper --- packages/cli/test/integration/auth.endpoints.test.ts | 4 ++-- packages/cli/test/integration/me.endpoints.test.ts | 10 +++++----- packages/cli/test/integration/owner.endpoints.test.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 6 +++--- packages/cli/test/integration/users.endpoints.test.ts | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 477c366b3bf08..bc25de5179293 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -22,7 +22,7 @@ describe('auth endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['auth'], applyAuth: true }); await utils.initTestDb(); - await utils.truncateUserTable(); + await utils.truncate('User'); globalOwnerRole = await getGlobalOwnerRole(); }); @@ -46,7 +46,7 @@ describe('auth endpoints', () => { }); afterEach(async () => { - await utils.truncateUserTable(); + await utils.truncate('User'); }); afterAll(() => { diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 774b2064b22a6..398d2374a4680 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -37,7 +37,7 @@ describe('/me endpoints', () => { }); afterEach(async () => { - await utils.truncateUserTable(); + await utils.truncate('User'); }); afterAll(() => { @@ -168,7 +168,7 @@ describe('/me endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); - await utils.truncateUserTable(); + await utils.truncate('User'); }); beforeEach(async () => { @@ -199,7 +199,7 @@ describe('/me endpoints', () => { }); afterEach(async () => { - await utils.truncateUserTable(); + await utils.truncate('User'); }); afterAll(() => { @@ -330,7 +330,7 @@ describe('/me endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); - await utils.truncateUserTable(); + await utils.truncate('User'); }); beforeEach(async () => { @@ -347,7 +347,7 @@ describe('/me endpoints', () => { }); afterEach(async () => { - await utils.truncateUserTable(); + await utils.truncate('User'); }); afterAll(() => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 0fa2a25e0fb9d..0eede54695cda 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -28,7 +28,7 @@ describe('/owner endpoints', () => { }); afterEach(async () => { - await utils.truncateUserTable(); + await utils.truncate('User'); }); afterAll(() => { diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 77524ea83dc57..ac960d8b1c344 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -10,7 +10,7 @@ import { v4 as uuid } from 'uuid'; import config = require('../../../config'); import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes'; -import { Db } from '../../../src'; +import { Db, IDatabaseCollections } from '../../../src'; import { User } from '../../../src/databases/entities/User'; import { meNamespace as meEndpoints } from '../../../src/UserManagement/routes/me'; import { usersNamespace as usersEndpoints } from '../../../src/UserManagement/routes/users'; @@ -79,9 +79,9 @@ export async function initTestDb() { await getConnection().runMigrations({ transaction: 'none' }); } -export async function truncateUserTable() { +export async function truncate(entity: keyof IDatabaseCollections) { await getConnection().query('PRAGMA foreign_keys=OFF'); - await Db.collections.User!.clear(); + await Db.collections[entity]!.clear(); await getConnection().query('PRAGMA foreign_keys=ON'); } diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 7d56ccd8cfdb5..629151b500270 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -48,7 +48,7 @@ beforeAll(async () => { }); beforeEach(async () => { - await utils.truncateUserTable(); + await utils.truncate('User'); jest.isolateModules(() => { jest.mock('../../config'); @@ -68,7 +68,7 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncateUserTable(); + await utils.truncate('User'); }); afterAll(() => { From 9136d6735b01ecf35d6198b5f65b39e7db8e4882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 14:14:49 +0100 Subject: [PATCH 71/81] :test_tube: Add teardown to `DELETE /users/:id` --- packages/cli/test/integration/users.endpoints.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 629151b500270..307045f6b2f73 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -272,6 +272,8 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { expect(sharedWorkflow.user.id).toBe(owner.id); expect(sharedCredential.user.id).toBe(owner.id); expect(deletedUser).toBeUndefined(); + + await Promise.all([utils.truncate('Credentials'), utils.truncate('Workflow')]); }); test('GET /resolve-signup-token should validate invite token', async () => { From b17e9d155e710b3be0f6f2ec41b88d5d5eecb080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 14:57:13 +0100 Subject: [PATCH 72/81] :test_tube: Add DB expectations to users tests --- .../test/integration/users.endpoints.test.ts | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 307045f6b2f73..ce05b45d76291 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -19,6 +19,7 @@ import { import { createUser } from './shared/utils'; import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; +import { compare } from 'bcryptjs'; let app: express.Application; let globalOwnerRole: Role; @@ -334,11 +335,13 @@ test('POST /users/:id should fill out a user shell', async () => { 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: randomValidPassword(), + password: newPassword, }); const { @@ -363,19 +366,31 @@ test('POST /users/:id should fill out a user shell', async () => { const authToken = utils.getAuthToken(response); expect(authToken).toBeDefined(); + + const filledOutUser = await Db.collections.User!.findOneOrFail(userToFillOut.id); + expect(filledOutUser.firstName).toBe(INITIAL_TEST_USER.firstName); + expect(filledOutUser.lastName).toBe(INITIAL_TEST_USER.lastName); + expect(filledOutUser.password).not.toBe(newPassword); }); test('POST /users/:id should fail with invalid inputs', async () => { const authlessAgent = await utils.createAgent(app, { auth: false }); + const emailToStore = randomEmail(); + const userToFillOut = await Db.collections.User!.save({ - email: randomEmail(), + email: emailToStore, globalRole: globalMemberRole, }); for (const invalidPayload of INVALID_FILL_OUT_USER_PAYLOADS) { 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(); } }); @@ -387,20 +402,30 @@ test('POST /users/:id should fail with already accepted invite', async () => { scope: 'global', }); - const userToFillOut = await Db.collections.User!.save({ + const shell = await Db.collections.User!.save({ email: randomEmail(), password: randomValidPassword(), // simulate accepted invite globalRole: globalMemberRole, }); - const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send({ + const newPassword = randomValidPassword(); + + const response = await authlessAgent.post(`/users/${shell.id}`).send({ inviterId: INITIAL_TEST_USER.id, firstName: randomName(), lastName: randomName(), - password: randomValidPassword(), + password: newPassword, }); expect(response.statusCode).toBe(400); + + const fetchedShell = await Db.collections.User!.findOneOrFail({ where: { email: shell.email } }); + expect(fetchedShell.firstName).toBeNull(); + expect(fetchedShell.lastName).toBeNull(); + + const comparisonResult = await compare(shell.password, newPassword); + expect(comparisonResult).toBe(false); + expect(newPassword).not.toBe(fetchedShell.password); }); test('POST /users should fail if emailing is not set up', async () => { @@ -467,6 +492,9 @@ test('POST /users should fail with invalid inputs', async () => { for (const invalidPayload of invalidPayloads) { const response = await authOwnerAgent.post('/users').send(invalidPayload); expect(response.statusCode).toBe(400); + + const users = await Db.collections.User!.find(); + expect(users.length).toBe(1); // DB unaffected } }); From ad2deef1c9d77ce2f08bbdb4705b45c2692165c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 15:04:25 +0100 Subject: [PATCH 73/81] :zap: Refactor as in users namespace --- .../passwordReset.endpoints.test.ts | 77 ++++++++----------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 8b96498d3a21b..08106bb712645 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -20,7 +20,7 @@ let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true }); await utils.initTestDb(); - await utils.truncateUserTable(); + await utils.truncate('User'); globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', @@ -36,20 +36,18 @@ beforeEach(async () => { config.set('userManagement.hasOwner', true); config.set('userManagement.emails.mode', ''); - await Db.collections.User!.save({ + await utils.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, - createdAt: new Date(), - updatedAt: new Date(), - globalRole: globalOwnerRole, + role: globalOwnerRole, }); }); afterEach(async () => { - await utils.truncateUserTable(); + await utils.truncate('User'); }); afterAll(() => { @@ -57,8 +55,7 @@ afterAll(() => { }); test('POST /forgot-password should send password reset email', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); const { user, @@ -73,34 +70,32 @@ test('POST /forgot-password should send password reset email', async () => { config.set('userManagement.emails.smtp.auth.user', user); config.set('userManagement.emails.smtp.auth.pass', pass); - const response = await authOwnerAgent + const response = await authlessAgent .post('/forgot-password') .send({ email: INITIAL_TEST_USER.email }); expect(response.statusCode).toBe(200); expect(response.body).toEqual({}); - const shell = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); - expect(shell.resetPasswordToken).toBeDefined(); + const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(owner.resetPasswordToken).toBeDefined(); }); test('POST /forgot-password should fail if emailing is not set up', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); - const response = await authOwnerAgent + const response = await authlessAgent .post('/forgot-password') .send({ email: INITIAL_TEST_USER.email }); expect(response.statusCode).toBe(500); - const shell = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); - expect(shell.resetPasswordToken).toBeNull(); + const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(owner.resetPasswordToken).toBeNull(); }); test('POST /forgot-password should fail with invalid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); config.set('userManagement.emails.mode', 'smtp'); @@ -113,34 +108,32 @@ test('POST /forgot-password should fail with invalid inputs', async () => { ]; for (const invalidPayload of invalidPayloads) { - const response = await authOwnerAgent.post('/forgot-password').send(invalidPayload); + const response = await authlessAgent.post('/forgot-password').send(invalidPayload); expect(response.statusCode).toBe(400); - const shell = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); - expect(shell.resetPasswordToken).toBeNull(); + const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); + expect(owner.resetPasswordToken).toBeNull(); } }); test('POST /forgot-password should fail if user is not found', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); config.set('userManagement.emails.mode', 'smtp'); - const response = await authOwnerAgent.post('/forgot-password').send({ email: randomEmail() }); + const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() }); expect(response.statusCode).toBe(404); }); test('GET /resolve-password-token should succeed with valid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); const resetPasswordToken = uuid(); await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); - const response = await authOwnerAgent + const response = await authlessAgent .get('/resolve-password-token') .query({ userId: INITIAL_TEST_USER.id, token: resetPasswordToken }); @@ -148,14 +141,13 @@ test('GET /resolve-password-token should succeed with valid inputs', async () => }); test('GET /resolve-password-token should fail with invalid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); config.set('userManagement.emails.mode', 'smtp'); - const first = await authOwnerAgent.get('/resolve-password-token').query({ token: uuid() }); + const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() }); - const second = await authOwnerAgent + const second = await authlessAgent .get('/resolve-password-token') .query({ userId: INITIAL_TEST_USER.id }); @@ -165,12 +157,11 @@ test('GET /resolve-password-token should fail with invalid inputs', async () => }); test('GET /resolve-password-token should fail if user is not found', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); config.set('userManagement.emails.mode', 'smtp'); - const response = await authOwnerAgent + const response = await authlessAgent .get('/resolve-password-token') .query({ userId: INITIAL_TEST_USER.id, token: uuid() }); @@ -178,18 +169,17 @@ test('GET /resolve-password-token should fail if user is not found', async () => }); test('POST /change-password should succeed with valid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); const resetPasswordToken = uuid(); await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); - const passwordToSend = randomValidPassword(); + const passwordToStore = randomValidPassword(); - const response = await authOwnerAgent.post('/change-password').send({ + const response = await authlessAgent.post('/change-password').send({ token: resetPasswordToken, userId: INITIAL_TEST_USER.id, - password: passwordToSend, + password: passwordToStore, }); expect(response.statusCode).toBe(200); @@ -201,14 +191,13 @@ test('POST /change-password should succeed with valid inputs', async () => { INITIAL_TEST_USER.id, ); - const comparisonResult = await compare(passwordToSend, storedPassword!); + const comparisonResult = await compare(passwordToStore, storedPassword!); expect(comparisonResult).toBe(true); - expect(storedPassword).not.toBe(passwordToSend); + expect(storedPassword).not.toBe(passwordToStore); }); test('POST /change-password should fail with invalid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = await utils.createAuthAgent(app, owner); + const authlessAgent = await utils.createAgent(app); const resetPasswordToken = uuid(); await Db.collections.User!.update(INITIAL_TEST_USER.id, { resetPasswordToken }); @@ -233,7 +222,7 @@ test('POST /change-password should fail with invalid inputs', async () => { ]; for (const invalidPayload of invalidPayloads) { - const response = await authOwnerAgent.post('/change-password').query(invalidPayload); + const response = await authlessAgent.post('/change-password').query(invalidPayload); expect(response.statusCode).toBe(400); } }); From 753f1a30af125c0f37fd663943016e6bbf78168e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 15:07:16 +0100 Subject: [PATCH 74/81] :test_tube: Add expectation to `POST /change-password` --- .../cli/test/integration/passwordReset.endpoints.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 08106bb712645..7029dd1a0e574 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -221,9 +221,14 @@ test('POST /change-password should fail with invalid inputs', async () => { }, ]; + const { password: originalHashedPassword } = await Db.collections.User!.findOneOrFail(); + for (const invalidPayload of invalidPayloads) { const response = await authlessAgent.post('/change-password').query(invalidPayload); expect(response.statusCode).toBe(400); + + const { password: fetchedHashedPassword } = await Db.collections.User!.findOneOrFail(); + expect(originalHashedPassword).toBe(fetchedHashedPassword); } }); From aab2cd9031146ec14ce8322eb79ce682be74324d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 16:23:51 +0100 Subject: [PATCH 75/81] :fire: Remove pass validation due to hash --- packages/cli/src/databases/entities/User.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index 2efbabd9a9244..3dbed1ad817ab 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -78,9 +78,6 @@ export class User { @Column({ nullable: true }) @IsString({ message: 'Password must be of type string.' }) - @Length(MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, { - message: 'Password does not comply to security standards.', - }) password?: string; @Column({ type: String, nullable: true }) From 45bfd1223e562d56d0a8ba580ec66b495a0684aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 16:24:16 +0100 Subject: [PATCH 76/81] :pencil2: Improve pass validation error message --- packages/cli/src/UserManagement/UserManagementHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index b8e69accfe940..8bafde341b3a7 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -30,7 +30,7 @@ export function validatePassword(password?: string): string { if (password.length < MIN_PASSWORD_LENGTH || password.length > MAX_PASSWORD_LENGTH) { throw new ResponseHelper.ResponseError( - 'Password must be 8 to 64 characters long', + `Password must be ${MIN_PASSWORD_LENGTH} to ${MAX_PASSWORD_LENGTH} characters long`, undefined, 400, ); From 44e0fdc22cc685ea4fece498e54356548f308600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 16:24:36 +0100 Subject: [PATCH 77/81] :zap: Improve owner pass validation --- packages/cli/src/UserManagement/routes/owner.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/UserManagement/routes/owner.ts b/packages/cli/src/UserManagement/routes/owner.ts index 00d5323c6386a..7bb66076054f1 100644 --- a/packages/cli/src/UserManagement/routes/owner.ts +++ b/packages/cli/src/UserManagement/routes/owner.ts @@ -11,7 +11,7 @@ import { validateEntity } from '../../GenericHelpers'; import { OwnerRequest } from '../../requests'; import { issueCookie } from '../auth/jwt'; import { N8nApp } from '../Interfaces'; -import { sanitizeUser } from '../UserManagementHelper'; +import { sanitizeUser, validatePassword } from '../UserManagementHelper'; export function ownerNamespace(this: N8nApp): void { /** @@ -32,13 +32,7 @@ export function ownerNamespace(this: N8nApp): void { throw new ResponseHelper.ResponseError('Invalid email address', undefined, 400); } - if (!password) { - throw new ResponseHelper.ResponseError( - 'Password does not comply to security standards', - undefined, - 400, - ); - } + const validPassword = validatePassword(password); if (!firstName || !lastName) { throw new ResponseHelper.ResponseError( @@ -59,7 +53,7 @@ export function ownerNamespace(this: N8nApp): void { email, firstName, lastName, - password: hashSync(password, genSaltSync(10)), + password: hashSync(validPassword, genSaltSync(10)), globalRole, id: userId, }); From ab7753f901316715293b275107eae8eefbcfb289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 17:28:31 +0100 Subject: [PATCH 78/81] :zap: Create logger initialization helper --- packages/cli/test/integration/shared/utils.ts | 7 +++++++ packages/cli/test/integration/users.endpoints.test.ts | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index ac960d8b1c344..b8afa57da6cac 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -6,6 +6,7 @@ import bodyParser = require('body-parser'); import * as util from 'util'; import { createTestAccount } from 'nodemailer'; import { v4 as uuid } from 'uuid'; +import { LoggerProxy } from 'n8n-workflow'; import config = require('../../../config'); import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; @@ -21,11 +22,17 @@ import { issueJWT } from '../../../src/UserManagement/auth/jwt'; import { randomEmail, randomValidPassword, randomName } from './random'; import type { EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; import { Role } from '../../../src/databases/entities/Role'; +import { getLogger } from '../../../src/Logger'; // ---------------------------------- // test server // ---------------------------------- +export const initLogger = () => { + config.set('logs.output', 'file'); // declutter console output during tests + LoggerProxy.init(getLogger()); +}; + /** * Initialize a test server to make requests to. * diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index ce05b45d76291..fcce0df9b6f3a 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -44,8 +44,7 @@ beforeAll(async () => { credentialOwnerRole = fetchedCredentialOwnerRole; config.set('logs.output', 'file'); // declutter console output - const logger = getLogger(); - LoggerProxy.init(logger); + utils.initLogger(); }); beforeEach(async () => { From c60226eb175c88e1f961763e3a2f964ad40d0799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 17:43:12 +0100 Subject: [PATCH 79/81] :zap: Optimize helpers --- .../cli/test/integration/auth.endpoints.test.ts | 4 ++-- .../cli/test/integration/me.endpoints.test.ts | 10 +++++----- .../test/integration/owner.endpoints.test.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 17 ++++++++++++++--- .../test/integration/users.endpoints.test.ts | 6 +++--- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index bc25de5179293..784a1d2a16127 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -22,7 +22,7 @@ describe('auth endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['auth'], applyAuth: true }); await utils.initTestDb(); - await utils.truncate('User'); + await utils.truncate(['User']); globalOwnerRole = await getGlobalOwnerRole(); }); @@ -46,7 +46,7 @@ describe('auth endpoints', () => { }); afterEach(async () => { - await utils.truncate('User'); + await utils.truncate(['User']); }); afterAll(() => { diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 398d2374a4680..3b7640be65d70 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -37,7 +37,7 @@ describe('/me endpoints', () => { }); afterEach(async () => { - await utils.truncate('User'); + await utils.truncate(['User']); }); afterAll(() => { @@ -168,7 +168,7 @@ describe('/me endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); - await utils.truncate('User'); + await utils.truncate(['User']); }); beforeEach(async () => { @@ -199,7 +199,7 @@ describe('/me endpoints', () => { }); afterEach(async () => { - await utils.truncate('User'); + await utils.truncate(['User']); }); afterAll(() => { @@ -330,7 +330,7 @@ describe('/me endpoints', () => { beforeAll(async () => { app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); await utils.initTestDb(); - await utils.truncate('User'); + await utils.truncate(['User']); }); beforeEach(async () => { @@ -347,7 +347,7 @@ describe('/me endpoints', () => { }); afterEach(async () => { - await utils.truncate('User'); + await utils.truncate(['User']); }); afterAll(() => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 0eede54695cda..ec99f219874b8 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -28,7 +28,7 @@ describe('/owner endpoints', () => { }); afterEach(async () => { - await utils.truncate('User'); + await utils.truncate(['User']); }); afterAll(() => { diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index b8afa57da6cac..7eb244482f776 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -86,9 +86,9 @@ export async function initTestDb() { await getConnection().runMigrations({ transaction: 'none' }); } -export async function truncate(entity: keyof IDatabaseCollections) { +export async function truncate(entities: Array) { await getConnection().query('PRAGMA foreign_keys=OFF'); - await Db.collections[entity]!.clear(); + await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); await getConnection().query('PRAGMA foreign_keys=ON'); } @@ -103,7 +103,14 @@ export async function createUser( firstName, lastName, role, - }: { id: string; email: string; password: string; firstName: string; lastName: string; role?: Role } = { + }: { + id: string; + email: string; + password: string; + firstName: string; + lastName: string; + role?: Role; + } = { id: uuid(), email: randomEmail(), password: randomValidPassword(), @@ -195,6 +202,10 @@ export async function createAgent( const agent = request.agent(app); agent.use(prefix(REST_PATH_SEGMENT)); + if (auth && !user) { + throw new Error('User required for auth agent creation') + } + if (auth && user) { const { token } = await issueJWT(user); agent.jar.setCookie(`n8n-auth=${token}`); diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index fcce0df9b6f3a..0ae5971daec11 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -48,7 +48,7 @@ beforeAll(async () => { }); beforeEach(async () => { - await utils.truncate('User'); + await utils.truncate(['User']); jest.isolateModules(() => { jest.mock('../../config'); @@ -68,7 +68,7 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate('User'); + await utils.truncate(['User']); }); afterAll(() => { @@ -273,7 +273,7 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { expect(sharedCredential.user.id).toBe(owner.id); expect(deletedUser).toBeUndefined(); - await Promise.all([utils.truncate('Credentials'), utils.truncate('Workflow')]); + await utils.truncate(['Credentials', 'Workflow']); }); test('GET /resolve-signup-token should validate invite token', async () => { From 89cabd5fba14b0568006af2c35d7dfbb442002de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 17:53:13 +0100 Subject: [PATCH 80/81] :zap: Restructure `getAllRoles` helper --- packages/cli/test/integration/shared/utils.ts | 21 +++---------------- .../test/integration/users.endpoints.test.ts | 12 +++++------ 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 7eb244482f776..65f6eeaa58769 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -167,28 +167,13 @@ export async function getCredentialOwnerRole() { }); } -export async function getAllRoles() { - const roles = await Promise.all([ +export function getAllRoles() { + return Promise.all([ getGlobalOwnerRole(), getGlobalMemberRole(), getWorkflowOwnerRole(), getCredentialOwnerRole(), ]); - - return { - globalOwnerRole: roles.find( - ({ scope, name }) => scope === 'global' && name === 'owner', - ) as Role, - globalMemberRole: roles.find( - ({ scope, name }) => scope === 'global' && name === 'member', - ) as Role, - workflowOwnerRole: roles.find( - ({ scope, name }) => scope === 'workflow' && name === 'owner', - ) as Role, - credentialOwnerRole: roles.find( - ({ scope, name }) => scope === 'credential' && name === 'owner', - ) as Role, - }; } // ---------------------------------- @@ -203,7 +188,7 @@ export async function createAgent( agent.use(prefix(REST_PATH_SEGMENT)); if (auth && !user) { - throw new Error('User required for auth agent creation') + throw new Error('User required for auth agent creation'); } if (auth && user) { diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 0ae5971daec11..1f46d55497e72 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -31,12 +31,12 @@ beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); await utils.initTestDb(); - const { - globalOwnerRole: fetchedGlobalOwnerRole, - globalMemberRole: fetchedGlobalMemberRole, - workflowOwnerRole: fetchedWorkflowOwnerRole, - credentialOwnerRole: fetchedCredentialOwnerRole, - } = await utils.getAllRoles(); + const [ + fetchedGlobalOwnerRole, + fetchedGlobalMemberRole, + fetchedWorkflowOwnerRole, + fetchedCredentialOwnerRole, + ] = await utils.getAllRoles(); globalOwnerRole = fetchedGlobalOwnerRole; globalMemberRole = fetchedGlobalMemberRole; From 1baa54e4434c7d348adb476d59eb0607c896b2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 15 Feb 2022 17:57:24 +0100 Subject: [PATCH 81/81] :zap: Update `truncate` calls --- packages/cli/test/integration/passwordReset.endpoints.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 7029dd1a0e574..748eb5c54770d 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -20,7 +20,7 @@ let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true }); await utils.initTestDb(); - await utils.truncate('User'); + await utils.truncate(['User']); globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', @@ -47,7 +47,7 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate('User'); + await utils.truncate(['User']); }); afterAll(() => {