From 1e2d6daaa3e933409f5fbd3ff656b1286072fae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 8 Apr 2022 18:37:07 +0200 Subject: [PATCH] :rotating_light: Optimize UM tests (#3066) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :zap: Declutter test logs * :bug: Fix random passwords length * :bug: Fix password hashing in test user creation * :bug: Hash leftover password * :zap: Improve error message for `compare` * :zap: Restore `randomInvalidPassword` contant * :zap: Mock Telemetry module to prevent `--forceExit` * :fire: Remove unused imports * :fire: Remove unused import * :zap: Add util for configuring test SMTP * :zap: Isolate user creation * :fire: De-duplicate `createFullUser` * :zap: Centralize hashing * :fire: Remove superfluous arg * :fire: Remove outdated comment * :zap: Prioritize shared tables during trucation * :test_tube: Add login tests * :zap: Use token helper * :pencil2: Improve naming * :zap: Make `createMemberShell` consistent * :fire: Remove unneeded helper * :fire: De-duplicate `beforeEach` * :pencil2: Improve naming * :truck: Move `categorize` to utils * :pencil2: Update comment * :test_tube: Simplify test * :blue_book: Improve `User.password` type * :zap: Silence logger * :zap: Simplify condition * :zap: Unhash password in payload * :bug: Fix comparison against unhashed password * :zap: Increase timeout for fake SMTP service * :fire: Remove unneeded import * :zap: Use `isNull()` * :test_tube: Use `Promise.all()` in creds tests * :test_tube: Use `Promise.all()` in me tests * :test_tube: Use `Promise.all()` in owner tests * :test_tube: Use `Promise.all()` in password tests * :test_tube: Use `Promise.all()` in users tests * :zap: Re-set cookie if UM disabled * :fire: Remove repeated line * :zap: Refactor out shared owner data * :fire: Remove unneeded import * :fire: Remove repeated lines * :zap: Organize imports * :zap: Reuse helper * :truck: Rename tests to match routers * :truck: Rename `createFullUser()` to `createUser()` * :zap: Consolidate user shell creation * :zap: Make hashing async * :zap: Add email to user shell * :zap: Optimize array building * 🛠 refactor user shell factory * :bug: Fix MySQL tests * :zap: Silence logger in other DBs Co-authored-by: Ben Hesseldieck --- packages/cli/package.json | 4 +- .../UserManagement/UserManagementHelper.ts | 14 +- .../cli/src/UserManagement/routes/auth.ts | 10 +- packages/cli/src/UserManagement/routes/me.ts | 7 +- .../cli/src/UserManagement/routes/owner.ts | 5 +- .../UserManagement/routes/passwordReset.ts | 5 +- .../cli/src/UserManagement/routes/users.ts | 4 +- packages/cli/src/databases/entities/User.ts | 4 +- .../cli/test/integration/auth.api.test.ts | 283 ++++++++++++++ .../test/integration/auth.endpoints.test.ts | 149 ------- ...uth.middleware.test.ts => auth.mw.test.ts} | 10 +- .../test/integration/credentials.api.test.ts | 192 ++++----- .../{me.endpoints.test.ts => me.api.test.ts} | 65 ++-- ...er.endpoints.test.ts => owner.api.test.ts} | 59 +-- ...ints.test.ts => passwordReset.api.test.ts} | 162 ++++---- .../cli/test/integration/shared/testDb.ts | 123 +++--- .../cli/test/integration/shared/types.d.ts | 4 +- packages/cli/test/integration/shared/utils.ts | 40 +- ...rs.endpoints.test.ts => users.api.test.ts} | 365 ++++++++---------- 19 files changed, 831 insertions(+), 674 deletions(-) create mode 100644 packages/cli/test/integration/auth.api.test.ts delete mode 100644 packages/cli/test/integration/auth.endpoints.test.ts rename packages/cli/test/integration/{auth.middleware.test.ts => auth.mw.test.ts} (87%) rename packages/cli/test/integration/{me.endpoints.test.ts => me.api.test.ts} (88%) rename packages/cli/test/integration/{owner.endpoints.test.ts => owner.api.test.ts} (72%) rename packages/cli/test/integration/{passwordReset.endpoints.test.ts => passwordReset.api.test.ts} (60%) rename packages/cli/test/integration/{users.endpoints.test.ts => users.api.test.ts} (61%) diff --git a/packages/cli/package.json b/packages/cli/package.json index fbd2d93b89..c2f46fc221 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -31,8 +31,8 @@ "start:windows": "cd bin && n8n", "test": "npm run test:sqlite", "test:sqlite": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=sqlite; jest", - "test:postgres": "export DB_TYPE=postgresdb && jest", - "test:mysql": "export DB_TYPE=mysqldb && jest", + "test:postgres": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=postgresdb && jest", + "test:mysql": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=mysqldb && jest", "watch": "tsc --watch", "typeorm": "ts-node ../../node_modules/typeorm/cli.js" }, diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 7b33640ae3..650de0f264 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -4,7 +4,7 @@ import { Workflow } from 'n8n-workflow'; import { In, IsNull, Not } from 'typeorm'; import express = require('express'); -import { compare } from 'bcryptjs'; +import { compare, genSaltSync, hash } from 'bcryptjs'; import { PublicUser } from './Interfaces'; import { Db, ResponseHelper } from '..'; @@ -63,11 +63,6 @@ export function getInstanceBaseUrl(): string { return n8nBaseUrl.endsWith('/') ? n8nBaseUrl.slice(0, n8nBaseUrl.length - 1) : n8nBaseUrl; } -export async function isInstanceOwnerSetup(): Promise { - const users = await Db.collections.User!.find({ email: Not(IsNull()) }); - return users.length !== 0; -} - // TODO: Enforce at model level export function validatePassword(password?: string): string { if (!password) { @@ -223,9 +218,12 @@ export function isAuthenticatedRequest(request: express.Request): request is Aut // hashing // ---------------------------------- -export async function compareHash(str: string, hash: string): Promise { +export const hashPassword = async (validPassword: string): Promise => + hash(validPassword, genSaltSync(10)); + +export async function compareHash(plaintext: string, hashed: string): Promise { try { - return await compare(str, hash); + return await compare(plaintext, hashed); } catch (error) { if (error instanceof Error && error.message.includes('Invalid salt version')) { error.message += diff --git a/packages/cli/src/UserManagement/routes/auth.ts b/packages/cli/src/UserManagement/routes/auth.ts index 3d778545dd..628bade65d 100644 --- a/packages/cli/src/UserManagement/routes/auth.ts +++ b/packages/cli/src/UserManagement/routes/auth.ts @@ -8,9 +8,10 @@ import { Db, ResponseHelper } from '../..'; import { AUTH_COOKIE_NAME } from '../../constants'; import { issueCookie, resolveJwt } from '../auth/jwt'; import { N8nApp, PublicUser } from '../Interfaces'; -import { compareHash, isInstanceOwnerSetup, sanitizeUser } from '../UserManagementHelper'; +import { compareHash, sanitizeUser } from '../UserManagementHelper'; import { User } from '../../databases/entities/User'; import type { LoginRequest } from '../../requests'; +import config = require('../../../config'); export function authenticationMethods(this: N8nApp): void { /** @@ -71,13 +72,18 @@ export function authenticationMethods(this: N8nApp): void { // If logged in, return user try { user = await resolveJwt(cookieContents); + + if (!config.get('userManagement.isInstanceOwnerSetUp')) { + res.cookie(AUTH_COOKIE_NAME, cookieContents); + } + return sanitizeUser(user); } catch (error) { res.clearCookie(AUTH_COOKIE_NAME); } } - if (await isInstanceOwnerSetup()) { + if (config.get('userManagement.isInstanceOwnerSetUp')) { const error = new Error('Not logged in'); // @ts-ignore error.httpStatusCode = 401; diff --git a/packages/cli/src/UserManagement/routes/me.ts b/packages/cli/src/UserManagement/routes/me.ts index 1c4ed9f427..f4cab563dc 100644 --- a/packages/cli/src/UserManagement/routes/me.ts +++ b/packages/cli/src/UserManagement/routes/me.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable import/no-cycle */ -import { compare, genSaltSync, hashSync } from 'bcryptjs'; import express = require('express'); import validator from 'validator'; import { LoggerProxy as Logger } from 'n8n-workflow'; @@ -9,7 +8,7 @@ import { LoggerProxy as Logger } from 'n8n-workflow'; import { Db, InternalHooksManager, ResponseHelper } from '../..'; import { issueCookie } from '../auth/jwt'; import { N8nApp, PublicUser } from '../Interfaces'; -import { validatePassword, sanitizeUser } from '../UserManagementHelper'; +import { validatePassword, sanitizeUser, compareHash, hashPassword } from '../UserManagementHelper'; import type { AuthenticatedRequest, MeRequest } from '../../requests'; import { validateEntity } from '../../GenericHelpers'; import { User } from '../../databases/entities/User'; @@ -87,7 +86,7 @@ export function meNamespace(this: N8nApp): void { throw new ResponseHelper.ResponseError('Requesting user not set up.'); } - const isCurrentPwCorrect = await compare(currentPassword, req.user.password); + const isCurrentPwCorrect = await compareHash(currentPassword, req.user.password); if (!isCurrentPwCorrect) { throw new ResponseHelper.ResponseError( 'Provided current password is incorrect.', @@ -98,7 +97,7 @@ export function meNamespace(this: N8nApp): void { const validPassword = validatePassword(newPassword); - req.user.password = hashSync(validPassword, genSaltSync(10)); + req.user.password = await hashPassword(validPassword); const user = await Db.collections.User!.save(req.user); Logger.info('Password updated successfully', { userId: user.id }); diff --git a/packages/cli/src/UserManagement/routes/owner.ts b/packages/cli/src/UserManagement/routes/owner.ts index a526afc948..915bb39946 100644 --- a/packages/cli/src/UserManagement/routes/owner.ts +++ b/packages/cli/src/UserManagement/routes/owner.ts @@ -1,6 +1,5 @@ /* eslint-disable import/no-cycle */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { hashSync, genSaltSync } from 'bcryptjs'; import * as express from 'express'; import validator from 'validator'; import { LoggerProxy as Logger } from 'n8n-workflow'; @@ -11,7 +10,7 @@ import { validateEntity } from '../../GenericHelpers'; import { AuthenticatedRequest, OwnerRequest } from '../../requests'; import { issueCookie } from '../auth/jwt'; import { N8nApp } from '../Interfaces'; -import { sanitizeUser, validatePassword } from '../UserManagementHelper'; +import { hashPassword, sanitizeUser, validatePassword } from '../UserManagementHelper'; export function ownerNamespace(this: N8nApp): void { /** @@ -74,7 +73,7 @@ export function ownerNamespace(this: N8nApp): void { email, firstName, lastName, - password: hashSync(validPassword, genSaltSync(10)), + password: await hashPassword(validPassword), }); await validateEntity(owner); diff --git a/packages/cli/src/UserManagement/routes/passwordReset.ts b/packages/cli/src/UserManagement/routes/passwordReset.ts index 574551e142..6a9fb68cca 100644 --- a/packages/cli/src/UserManagement/routes/passwordReset.ts +++ b/packages/cli/src/UserManagement/routes/passwordReset.ts @@ -4,14 +4,13 @@ import express = require('express'); import { v4 as uuid } from 'uuid'; import { URL } from 'url'; -import { genSaltSync, hashSync } from 'bcryptjs'; import validator from 'validator'; import { IsNull, MoreThanOrEqual, Not } from 'typeorm'; import { LoggerProxy as Logger } from 'n8n-workflow'; import { Db, InternalHooksManager, ResponseHelper } from '../..'; import { N8nApp } from '../Interfaces'; -import { getInstanceBaseUrl, validatePassword } from '../UserManagementHelper'; +import { getInstanceBaseUrl, hashPassword, validatePassword } from '../UserManagementHelper'; import * as UserManagementMailer from '../email'; import type { PasswordResetRequest } from '../../requests'; import { issueCookie } from '../auth/jwt'; @@ -206,7 +205,7 @@ export function passwordResetNamespace(this: N8nApp): void { } await Db.collections.User!.update(userId, { - password: hashSync(validPassword, genSaltSync(10)), + password: await hashPassword(validPassword), resetPasswordToken: null, resetPasswordTokenExpiration: null, }); diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 7711150c4b..ef9ebe889e 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -3,7 +3,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Response } from 'express'; import { In } from 'typeorm'; -import { genSaltSync, hashSync } from 'bcryptjs'; import validator from 'validator'; import { LoggerProxy as Logger } from 'n8n-workflow'; @@ -12,6 +11,7 @@ import { N8nApp, PublicUser } from '../Interfaces'; import { UserRequest } from '../../requests'; import { getInstanceBaseUrl, + hashPassword, isEmailSetUp, sanitizeUser, validatePassword, @@ -349,7 +349,7 @@ export function usersNamespace(this: N8nApp): void { invitee.firstName = firstName; invitee.lastName = lastName; - invitee.password = hashSync(validPassword, genSaltSync(10)); + invitee.password = await hashPassword(validPassword); const updatedUser = await Db.collections.User!.save(invitee); diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index b345b26534..648e1e4c83 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -62,7 +62,7 @@ export class User { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ length: 254 }) + @Column({ length: 254, nullable: true }) @Index({ unique: true }) @IsEmail() email: string; @@ -81,7 +81,7 @@ export class User { @Column({ nullable: true }) @IsString({ message: 'Password must be of type string.' }) - password?: string; + password: string; @Column({ type: String, nullable: true }) resetPasswordToken?: string | null; diff --git a/packages/cli/test/integration/auth.api.test.ts b/packages/cli/test/integration/auth.api.test.ts new file mode 100644 index 0000000000..ed49c63edb --- /dev/null +++ b/packages/cli/test/integration/auth.api.test.ts @@ -0,0 +1,283 @@ +import express = require('express'); +import validator from 'validator'; + +import config = require('../../config'); +import * as utils from './shared/utils'; +import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; +import { Db } from '../../src'; +import type { Role } from '../../src/databases/entities/Role'; +import { randomValidPassword } from './shared/random'; +import * as testDb from './shared/testDb'; +import { AUTH_COOKIE_NAME } from '../../src/constants'; + +jest.mock('../../src/telemetry'); + +let app: express.Application; +let testDbName = ''; +let globalOwnerRole: Role; +let globalMemberRole: Role; + +beforeAll(async () => { + app = utils.initTestServer({ endpointGroups: ['auth'], applyAuth: true }); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; + + globalOwnerRole = await testDb.getGlobalOwnerRole(); + globalMemberRole = await testDb.getGlobalMemberRole(); + utils.initTestLogger(); + utils.initTestTelemetry(); +}); + +beforeEach(async () => { + await testDb.truncate(['User'], testDbName); + + config.set('userManagement.isInstanceOwnerSetUp', true); + + await Db.collections.Settings!.update( + { key: 'userManagement.isInstanceOwnerSetUp' }, + { value: JSON.stringify(true) }, + ); +}); + +afterAll(async () => { + await testDb.terminate(testDbName); +}); + +test('POST /login should log user in', async () => { + const ownerPassword = randomValidPassword(); + const owner = await testDb.createUser({ + password: ownerPassword, + globalRole: globalOwnerRole, + }); + + const authlessAgent = utils.createAgent(app); + + const response = await authlessAgent.post('/login').send({ + email: owner.email, + password: ownerPassword, + }); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + } = response.body.data; + + expect(validator.isUUID(id)).toBe(true); + expect(email).toBe(owner.email); + expect(firstName).toBe(owner.firstName); + expect(lastName).toBe(owner.lastName); + expect(password).toBeUndefined(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(globalRole).toBeDefined(); + expect(globalRole.name).toBe('owner'); + expect(globalRole.scope).toBe('global'); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); +}); + +test('GET /login should return 401 Unauthorized if no cookie', async () => { + const authlessAgent = utils.createAgent(app); + + const response = await authlessAgent.get('/login'); + + expect(response.statusCode).toBe(401); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); +}); + +test('GET /login should return cookie if UM is disabled', async () => { + const ownerShell = await testDb.createUserShell(globalOwnerRole); + + config.set('userManagement.isInstanceOwnerSetUp', false); + + await Db.collections.Settings!.update( + { key: 'userManagement.isInstanceOwnerSetUp' }, + { value: JSON.stringify(false) }, + ); + + const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + + const response = await authOwnerShellAgent.get('/login'); + + expect(response.statusCode).toBe(200); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); +}); + +test('GET /login should return 401 Unauthorized if invalid cookie', async () => { + const invalidAuthAgent = utils.createAgent(app); + invalidAuthAgent.jar.setCookie(`${AUTH_COOKIE_NAME}=invalid`); + + const response = await invalidAuthAgent.get('/login'); + + expect(response.statusCode).toBe(401); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); +}); + +test('GET /login should return logged-in owner shell', async () => { + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authMemberAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + + const response = await authMemberAgent.get('/login'); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + } = response.body.data; + + expect(validator.isUUID(id)).toBe(true); + expect(email).toBeDefined(); + expect(firstName).toBeNull(); + expect(lastName).toBeNull(); + expect(password).toBeUndefined(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(globalRole).toBeDefined(); + expect(globalRole.name).toBe('owner'); + expect(globalRole.scope).toBe('global'); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); +}); + +test('GET /login should return logged-in member shell', async () => { + const memberShell = await testDb.createUserShell(globalMemberRole); + const authMemberAgent = utils.createAgent(app, { auth: true, user: memberShell }); + + const response = await authMemberAgent.get('/login'); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + } = response.body.data; + + expect(validator.isUUID(id)).toBe(true); + expect(email).toBeDefined(); + expect(firstName).toBeNull(); + expect(lastName).toBeNull(); + expect(password).toBeUndefined(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(globalRole).toBeDefined(); + expect(globalRole.name).toBe('member'); + expect(globalRole.scope).toBe('global'); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); +}); + +test('GET /login should return logged-in owner', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + + const response = await authOwnerAgent.get('/login'); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + } = response.body.data; + + expect(validator.isUUID(id)).toBe(true); + expect(email).toBe(owner.email); + expect(firstName).toBe(owner.firstName); + expect(lastName).toBe(owner.lastName); + expect(password).toBeUndefined(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(globalRole).toBeDefined(); + expect(globalRole.name).toBe('owner'); + expect(globalRole.scope).toBe('global'); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); +}); + +test('GET /login should return logged-in member', async () => { + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); + + const response = await authMemberAgent.get('/login'); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + } = response.body.data; + + expect(validator.isUUID(id)).toBe(true); + expect(email).toBe(member.email); + expect(firstName).toBe(member.firstName); + expect(lastName).toBe(member.lastName); + expect(password).toBeUndefined(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(globalRole).toBeDefined(); + expect(globalRole.name).toBe('member'); + expect(globalRole.scope).toBe('global'); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); +}); + +test('POST /logout should log user out', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + + const response = await authOwnerAgent.post('/logout'); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); +}); diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts deleted file mode 100644 index 187856c2b6..0000000000 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { hashSync, genSaltSync } from 'bcryptjs'; -import express = require('express'); -import validator from 'validator'; -import { v4 as uuid } from 'uuid'; - -import config = require('../../config'); -import * as utils from './shared/utils'; -import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; -import { Db } from '../../src'; -import { Role } from '../../src/databases/entities/Role'; -import { randomEmail, randomValidPassword, randomName } from './shared/random'; -import { getGlobalOwnerRole } from './shared/testDb'; -import * as testDb from './shared/testDb'; - -jest.mock('../../src/telemetry'); - -let globalOwnerRole: Role; - -let app: express.Application; -let testDbName = ''; - -beforeAll(async () => { - app = utils.initTestServer({ endpointGroups: ['auth'], applyAuth: true }); - const initResult = await testDb.init(); - testDbName = initResult.testDbName; - - await testDb.truncate(['User'], testDbName); - - globalOwnerRole = await getGlobalOwnerRole(); - utils.initTestLogger(); - utils.initTestTelemetry(); -}); - -beforeEach(async () => { - await testDb.createUser({ - id: uuid(), - email: TEST_USER.email, - firstName: TEST_USER.firstName, - lastName: TEST_USER.lastName, - password: TEST_USER.password, - globalRole: globalOwnerRole, - }); - - config.set('userManagement.isInstanceOwnerSetUp', true); - - await Db.collections.Settings!.update( - { key: 'userManagement.isInstanceOwnerSetUp' }, - { value: JSON.stringify(true) }, - ); -}); - -afterEach(async () => { - await testDb.truncate(['User'], testDbName); -}); - -afterAll(async () => { - await testDb.terminate(testDbName); -}); - -test('POST /login should log user in', async () => { - const authlessAgent = utils.createAgent(app); - - const response = await authlessAgent.post('/login').send({ - email: TEST_USER.email, - password: TEST_USER.password, - }); - - expect(response.statusCode).toBe(200); - - const { - id, - email, - firstName, - lastName, - password, - personalizationAnswers, - globalRole, - resetPasswordToken, - } = response.body.data; - - expect(validator.isUUID(id)).toBe(true); - expect(email).toBe(TEST_USER.email); - expect(firstName).toBe(TEST_USER.firstName); - expect(lastName).toBe(TEST_USER.lastName); - expect(password).toBeUndefined(); - expect(personalizationAnswers).toBeNull(); - expect(password).toBeUndefined(); - expect(resetPasswordToken).toBeUndefined(); - expect(globalRole).toBeDefined(); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); - - const authToken = utils.getAuthToken(response); - expect(authToken).toBeDefined(); -}); - -test('GET /login should receive logged in user', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - - const response = await authOwnerAgent.get('/login'); - - expect(response.statusCode).toBe(200); - - const { - id, - email, - firstName, - lastName, - password, - personalizationAnswers, - globalRole, - resetPasswordToken, - } = response.body.data; - - expect(validator.isUUID(id)).toBe(true); - expect(email).toBe(TEST_USER.email); - expect(firstName).toBe(TEST_USER.firstName); - expect(lastName).toBe(TEST_USER.lastName); - expect(password).toBeUndefined(); - expect(personalizationAnswers).toBeNull(); - expect(password).toBeUndefined(); - expect(resetPasswordToken).toBeUndefined(); - expect(globalRole).toBeDefined(); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); - - expect(response.headers['set-cookie']).toBeUndefined(); -}); - -test('POST /logout should log user out', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - - const response = await authOwnerAgent.post('/logout'); - - expect(response.statusCode).toBe(200); - expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY); - - const authToken = utils.getAuthToken(response); - expect(authToken).toBeUndefined(); -}); - -const TEST_USER = { - email: randomEmail(), - password: randomValidPassword(), - firstName: randomName(), - lastName: randomName(), -}; diff --git a/packages/cli/test/integration/auth.middleware.test.ts b/packages/cli/test/integration/auth.mw.test.ts similarity index 87% rename from packages/cli/test/integration/auth.middleware.test.ts rename to packages/cli/test/integration/auth.mw.test.ts index 49b520125f..ab9db88fcc 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.mw.test.ts @@ -8,11 +8,13 @@ import { } from './shared/constants'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; +import type { Role } from '../../src/databases/entities/Role'; jest.mock('../../src/telemetry'); let app: express.Application; let testDbName = ''; +let globalMemberRole: Role; beforeAll(async () => { app = utils.initTestServer({ @@ -21,6 +23,9 @@ beforeAll(async () => { }); const initResult = await testDb.init(); testDbName = initResult.testDbName; + + globalMemberRole = await testDb.getGlobalMemberRole(); + utils.initTestLogger(); utils.initTestTelemetry(); }); @@ -43,12 +48,9 @@ ROUTES_REQUIRING_AUTHORIZATION.forEach(async (route) => { const [method, endpoint] = getMethodAndEndpoint(route); test(`${route} should return 403 Forbidden for member`, async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const response = await authMemberAgent[method](endpoint); - if (response.statusCode === 500) { - console.log(response); - } expect(response.statusCode).toBe(403); }); diff --git a/packages/cli/test/integration/credentials.api.test.ts b/packages/cli/test/integration/credentials.api.test.ts index fb331d08cf..dde6cf342b 100644 --- a/packages/cli/test/integration/credentials.api.test.ts +++ b/packages/cli/test/integration/credentials.api.test.ts @@ -4,14 +4,17 @@ import { Db } from '../../src'; import { randomName, randomString } from './shared/random'; import * as utils from './shared/utils'; import type { CredentialPayload, SaveCredentialFunction } from './shared/types'; -import { Role } from '../../src/databases/entities/Role'; -import { User } from '../../src/databases/entities/User'; +import type { Role } from '../../src/databases/entities/Role'; +import type { User } from '../../src/databases/entities/User'; import * as testDb from './shared/testDb'; +import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; jest.mock('../../src/telemetry'); let app: express.Application; let testDbName = ''; +let globalOwnerRole: Role; +let globalMemberRole: Role; let saveCredential: SaveCredentialFunction; beforeAll(async () => { @@ -24,19 +27,17 @@ beforeAll(async () => { utils.initConfigFile(); + globalOwnerRole = await testDb.getGlobalOwnerRole(); + globalMemberRole = await testDb.getGlobalMemberRole(); const credentialOwnerRole = await testDb.getCredentialOwnerRole(); saveCredential = affixRoleToSaveCredential(credentialOwnerRole); + + utils.initTestLogger(); utils.initTestTelemetry(); }); beforeEach(async () => { - await testDb.createOwnerShell(); -}); - -afterEach(async () => { - // do not combine calls - shared table must be cleared first and separately - await testDb.truncate(['SharedCredentials'], testDbName); - await testDb.truncate(['User', 'Credentials'], testDbName); + await testDb.truncate(['User', 'SharedCredentials', 'Credentials'], testDbName); }); afterAll(async () => { @@ -44,8 +45,9 @@ afterAll(async () => { }); test('POST /credentials should create cred', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const payload = credentialPayload(); const response = await authOwnerAgent.post('/credentials').send(payload); @@ -71,26 +73,28 @@ test('POST /credentials should create cred', async () => { where: { credentials: credential }, }); - expect(sharedCredential.user.id).toBe(owner.id); + expect(sharedCredential.user.id).toBe(ownerShell.id); expect(sharedCredential.credentials.name).toBe(payload.name); }); test('POST /credentials should fail with invalid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); - for (const invalidPayload of INVALID_PAYLOADS) { - const response = await authOwnerAgent.post('/credentials').send(invalidPayload); - expect(response.statusCode).toBe(400); - } + await Promise.all( + INVALID_PAYLOADS.map(async (invalidPayload) => { + const response = await authOwnerAgent.post('/credentials').send(invalidPayload); + expect(response.statusCode).toBe(400); + }), + ); }); test('POST /credentials should fail with missing encryption key', async () => { const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); mock.mockResolvedValue(undefined); - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const response = await authOwnerAgent.post('/credentials').send(credentialPayload()); @@ -100,8 +104,8 @@ test('POST /credentials should fail with missing encryption key', async () => { }); test('POST /credentials should ignore ID in payload', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const firstResponse = await authOwnerAgent .post('/credentials') @@ -117,9 +121,9 @@ test('POST /credentials should ignore ID in payload', async () => { }); test('DELETE /credentials/:id should delete owned cred for owner', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const savedCredential = await saveCredential(credentialPayload(), { user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell }); const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); @@ -136,9 +140,9 @@ test('DELETE /credentials/:id should delete owned cred for owner', async () => { }); test('DELETE /credentials/:id should delete non-owned cred for owner', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const member = await testDb.createUser(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); @@ -156,7 +160,7 @@ test('DELETE /credentials/:id should delete non-owned cred for owner', async () }); test('DELETE /credentials/:id should delete owned cred for member', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); @@ -175,10 +179,10 @@ test('DELETE /credentials/:id should delete owned cred for member', async () => }); test('DELETE /credentials/:id should not delete non-owned cred for member', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const member = await testDb.createUser(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); - const savedCredential = await saveCredential(credentialPayload(), { user: owner }); + const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell }); const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); @@ -194,8 +198,8 @@ test('DELETE /credentials/:id should not delete non-owned cred for member', asyn }); test('DELETE /credentials/:id should fail if cred not found', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const response = await authOwnerAgent.delete('/credentials/123'); @@ -203,9 +207,9 @@ test('DELETE /credentials/:id should fail if cred not found', async () => { }); test('PATCH /credentials/:id should update owned cred for owner', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const savedCredential = await saveCredential(credentialPayload(), { user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell }); const patchPayload = credentialPayload(); const response = await authOwnerAgent @@ -237,9 +241,9 @@ test('PATCH /credentials/:id should update owned cred for owner', async () => { }); test('PATCH /credentials/:id should update non-owned cred for owner', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const member = await testDb.createUser(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); const patchPayload = credentialPayload(); @@ -272,7 +276,7 @@ test('PATCH /credentials/:id should update non-owned cred for owner', async () = }); test('PATCH /credentials/:id should update owned cred for member', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); const patchPayload = credentialPayload(); @@ -306,10 +310,10 @@ test('PATCH /credentials/:id should update owned cred for member', async () => { }); test('PATCH /credentials/:id should not update non-owned cred for member', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const member = await testDb.createUser(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); - const savedCredential = await saveCredential(credentialPayload(), { user: owner }); + const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell }); const patchPayload = credentialPayload(); const response = await authMemberAgent @@ -324,22 +328,24 @@ test('PATCH /credentials/:id should not update non-owned cred for member', async }); test('PATCH /credentials/:id should fail with invalid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const savedCredential = await saveCredential(credentialPayload(), { user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell }); - for (const invalidPayload of INVALID_PAYLOADS) { - const response = await authOwnerAgent - .patch(`/credentials/${savedCredential.id}`) - .send(invalidPayload); + await Promise.all( + INVALID_PAYLOADS.map(async (invalidPayload) => { + const response = await authOwnerAgent + .patch(`/credentials/${savedCredential.id}`) + .send(invalidPayload); - expect(response.statusCode).toBe(400); - } + expect(response.statusCode).toBe(400); + }), + ); }); test('PATCH /credentials/:id should fail if cred not found', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const response = await authOwnerAgent.patch('/credentials/123').send(credentialPayload()); @@ -350,8 +356,8 @@ test('PATCH /credentials/:id should fail with missing encryption key', async () const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); mock.mockResolvedValue(undefined); - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const response = await authOwnerAgent.post('/credentials').send(credentialPayload()); @@ -361,14 +367,14 @@ test('PATCH /credentials/:id should fail with missing encryption key', async () }); test('GET /credentials should retrieve all creds for owner', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); for (let i = 0; i < 3; i++) { - await saveCredential(credentialPayload(), { user: owner }); + await saveCredential(credentialPayload(), { user: ownerShell }); } - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); await saveCredential(credentialPayload(), { user: member }); @@ -377,18 +383,20 @@ test('GET /credentials should retrieve all creds for owner', async () => { expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(4); // 3 owner + 1 member - for (const credential of response.body.data) { - const { name, type, nodesAccess, data: encryptedData } = credential; + await Promise.all( + response.body.data.map(async (credential: CredentialsEntity) => { + const { name, type, nodesAccess, data: encryptedData } = credential; - expect(typeof name).toBe('string'); - expect(typeof type).toBe('string'); - expect(typeof nodesAccess[0].nodeType).toBe('string'); - expect(encryptedData).toBeUndefined(); - } + expect(typeof name).toBe('string'); + expect(typeof type).toBe('string'); + expect(typeof nodesAccess[0].nodeType).toBe('string'); + expect(encryptedData).toBeUndefined(); + }), + ); }); test('GET /credentials should retrieve owned creds for member', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (let i = 0; i < 3; i++) { @@ -400,23 +408,25 @@ test('GET /credentials should retrieve owned creds for member', async () => { expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(3); - for (const credential of response.body.data) { - const { name, type, nodesAccess, data: encryptedData } = credential; + await Promise.all( + response.body.data.map(async (credential: CredentialsEntity) => { + const { name, type, nodesAccess, data: encryptedData } = credential; - expect(typeof name).toBe('string'); - expect(typeof type).toBe('string'); - expect(typeof nodesAccess[0].nodeType).toBe('string'); - expect(encryptedData).toBeUndefined(); - } + expect(typeof name).toBe('string'); + expect(typeof type).toBe('string'); + expect(typeof nodesAccess[0].nodeType).toBe('string'); + expect(encryptedData).toBeUndefined(); + }), + ); }); test('GET /credentials should not retrieve non-owned creds for member', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const member = await testDb.createUser(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (let i = 0; i < 3; i++) { - await saveCredential(credentialPayload(), { user: owner }); + await saveCredential(credentialPayload(), { user: ownerShell }); } const response = await authMemberAgent.get('/credentials'); @@ -426,9 +436,9 @@ test('GET /credentials should not retrieve non-owned creds for member', async () }); test('GET /credentials/:id should retrieve owned cred for owner', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const savedCredential = await saveCredential(credentialPayload(), { user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell }); const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); @@ -451,7 +461,7 @@ test('GET /credentials/:id should retrieve owned cred for owner', async () => { }); test('GET /credentials/:id should retrieve owned cred for member', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); @@ -477,10 +487,10 @@ test('GET /credentials/:id should retrieve owned cred for member', async () => { }); test('GET /credentials/:id should not retrieve non-owned cred for member', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const member = await testDb.createUser(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); - const savedCredential = await saveCredential(credentialPayload(), { user: owner }); + const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell }); const response = await authMemberAgent.get(`/credentials/${savedCredential.id}`); @@ -489,9 +499,9 @@ test('GET /credentials/:id should not retrieve non-owned cred for member', async }); test('GET /credentials/:id should fail with missing encryption key', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const savedCredential = await saveCredential(credentialPayload(), { user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); + const savedCredential = await saveCredential(credentialPayload(), { user: ownerShell }); const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); mock.mockResolvedValue(undefined); @@ -506,8 +516,8 @@ test('GET /credentials/:id should fail with missing encryption key', async () => }); test('GET /credentials/:id should return 404 if cred not found', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authMemberAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authMemberAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const response = await authMemberAgent.get('/credentials/789'); diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.api.test.ts similarity index 88% rename from packages/cli/test/integration/me.endpoints.test.ts rename to packages/cli/test/integration/me.api.test.ts index 1718f51052..182ed22f5c 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.api.test.ts @@ -1,12 +1,12 @@ -import { hashSync, genSaltSync } from 'bcryptjs'; import express = require('express'); import validator from 'validator'; +import { IsNull } from 'typeorm'; import config = require('../../config'); import * as utils from './shared/utils'; import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { Db } from '../../src'; -import { Role } from '../../src/databases/entities/Role'; +import type { Role } from '../../src/databases/entities/Role'; import { randomValidPassword, randomEmail, randomName, randomString } from './shared/random'; import * as testDb from './shared/testDb'; @@ -15,6 +15,7 @@ jest.mock('../../src/telemetry'); let app: express.Application; let testDbName = ''; let globalOwnerRole: Role; +let globalMemberRole: Role; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['me'], applyAuth: true }); @@ -22,6 +23,7 @@ beforeAll(async () => { testDbName = initResult.testDbName; globalOwnerRole = await testDb.getGlobalOwnerRole(); + globalMemberRole = await testDb.getGlobalMemberRole(); utils.initTestLogger(); utils.initTestTelemetry(); }); @@ -32,15 +34,11 @@ afterAll(async () => { describe('Owner shell', () => { beforeEach(async () => { - await testDb.createOwnerShell(); - }); - - afterEach(async () => { await testDb.truncate(['User'], testDbName); }); test('GET /me should return sanitized owner shell', async () => { - const ownerShell = await Db.collections.User!.findOneOrFail(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const response = await authOwnerShellAgent.get('/me'); @@ -72,7 +70,7 @@ describe('Owner shell', () => { }); test('PATCH /me should succeed with valid inputs', async () => { - const ownerShell = await Db.collections.User!.findOneOrFail(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { @@ -112,7 +110,7 @@ describe('Owner shell', () => { }); test('PATCH /me should fail with invalid inputs', async () => { - const ownerShell = await Db.collections.User!.findOneOrFail(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { @@ -127,7 +125,7 @@ describe('Owner shell', () => { }); test('PATCH /me/password should fail for shell', async () => { - const ownerShell = await Db.collections.User!.findOneOrFail(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const validPasswordPayload = { @@ -135,40 +133,46 @@ describe('Owner shell', () => { newPassword: randomValidPassword(), }; - const payloads = [validPasswordPayload, ...INVALID_PASSWORD_PAYLOADS]; + const validPayloads = [validPasswordPayload, ...INVALID_PASSWORD_PAYLOADS]; - for (const payload of payloads) { - const response = await authOwnerShellAgent.patch('/me/password').send(payload); - expect([400, 500].includes(response.statusCode)).toBe(true); + await Promise.all( + validPayloads.map(async (payload) => { + const response = await authOwnerShellAgent.patch('/me/password').send(payload); + expect([400, 500].includes(response.statusCode)).toBe(true); - const storedMember = await Db.collections.User!.findOneOrFail(); + const storedMember = await Db.collections.User!.findOneOrFail(); - if (payload.newPassword) { - expect(storedMember.password).not.toBe(payload.newPassword); - } - if (payload.currentPassword) { - expect(storedMember.password).not.toBe(payload.currentPassword); - } - } + if (payload.newPassword) { + expect(storedMember.password).not.toBe(payload.newPassword); + } + + if (payload.currentPassword) { + expect(storedMember.password).not.toBe(payload.currentPassword); + } + }), + ); const storedOwnerShell = await Db.collections.User!.findOneOrFail(); expect(storedOwnerShell.password).toBeNull(); }); test('POST /me/survey should succeed with valid inputs', async () => { - const ownerShell = await Db.collections.User!.findOneOrFail(); + const ownerShell = await testDb.createUserShell(globalOwnerRole); const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const validPayloads = [SURVEY, {}]; for (const validPayload of validPayloads) { const response = await authOwnerShellAgent.post('/me/survey').send(validPayload); + expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); - const { personalizationAnswers: storedAnswers } = await Db.collections.User!.findOneOrFail(); + const storedShellOwner = await Db.collections.User!.findOneOrFail({ + where: { email: IsNull() }, + }); - expect(storedAnswers).toEqual(validPayload); + expect(storedShellOwner.personalizationAnswers).toEqual(validPayload); } }); }); @@ -188,7 +192,7 @@ describe('Member', () => { }); test('GET /me should return sanitized member', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const response = await authMemberAgent.get('/me'); @@ -220,7 +224,7 @@ describe('Member', () => { }); test('PATCH /me should succeed with valid inputs', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { @@ -260,7 +264,7 @@ describe('Member', () => { }); test('PATCH /me should fail with invalid inputs', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { @@ -278,6 +282,7 @@ describe('Member', () => { const memberPassword = randomValidPassword(); const member = await testDb.createUser({ password: memberPassword, + globalRole: globalMemberRole, }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); @@ -296,7 +301,7 @@ describe('Member', () => { }); test('PATCH /me/password should fail with invalid inputs', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (const payload of INVALID_PASSWORD_PAYLOADS) { @@ -315,7 +320,7 @@ describe('Member', () => { }); test('POST /me/survey should succeed with valid inputs', async () => { - const member = await testDb.createUser(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const validPayloads = [SURVEY, {}]; diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.api.test.ts similarity index 72% rename from packages/cli/test/integration/owner.endpoints.test.ts rename to packages/cli/test/integration/owner.api.test.ts index ec78f7a198..d5ef0375f4 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.api.test.ts @@ -11,26 +11,25 @@ import { randomValidPassword, randomInvalidPassword, } from './shared/random'; +import type { Role } from '../../src/databases/entities/Role'; jest.mock('../../src/telemetry'); let app: express.Application; let testDbName = ''; +let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['owner'], applyAuth: true }); const initResult = await testDb.init(); testDbName = initResult.testDbName; + globalOwnerRole = await testDb.getGlobalOwnerRole(); utils.initTestLogger(); utils.initTestTelemetry(); }); beforeEach(async () => { - await testDb.createOwnerShell(); -}); - -afterEach(async () => { await testDb.truncate(['User'], testDbName); }); @@ -39,10 +38,17 @@ afterAll(async () => { }); test('POST /owner should create owner and enable isInstanceOwnerSetUp', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); - const response = await authOwnerAgent.post('/owner').send(TEST_USER); + const newOwnerData = { + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }; + + const response = await authOwnerAgent.post('/owner').send(newOwnerData); expect(response.statusCode).toBe(200); @@ -59,9 +65,9 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async () } = response.body.data; expect(validator.isUUID(id)).toBe(true); - expect(email).toBe(TEST_USER.email); - expect(firstName).toBe(TEST_USER.firstName); - expect(lastName).toBe(TEST_USER.lastName); + expect(email).toBe(newOwnerData.email); + expect(firstName).toBe(newOwnerData.firstName); + expect(lastName).toBe(newOwnerData.lastName); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(isPending).toBe(false); @@ -70,10 +76,10 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async () expect(globalRole.scope).toBe('global'); const storedOwner = await Db.collections.User!.findOneOrFail(id); - expect(storedOwner.password).not.toBe(TEST_USER.password); - expect(storedOwner.email).toBe(TEST_USER.email); - expect(storedOwner.firstName).toBe(TEST_USER.firstName); - expect(storedOwner.lastName).toBe(TEST_USER.lastName); + expect(storedOwner.password).not.toBe(newOwnerData.password); + expect(storedOwner.email).toBe(newOwnerData.email); + expect(storedOwner.firstName).toBe(newOwnerData.firstName); + expect(storedOwner.lastName).toBe(newOwnerData.lastName); const isInstanceOwnerSetUpConfig = config.get('userManagement.isInstanceOwnerSetUp'); expect(isInstanceOwnerSetUpConfig).toBe(true); @@ -83,18 +89,20 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async () }); test('POST /owner should fail with invalid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); - for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) { - const response = await authOwnerAgent.post('/owner').send(invalidPayload); - expect(response.statusCode).toBe(400); - } + await Promise.all( + INVALID_POST_OWNER_PAYLOADS.map(async (invalidPayload) => { + const response = await authOwnerAgent.post('/owner').send(invalidPayload); + expect(response.statusCode).toBe(400); + }), + ); }); test('POST /owner/skip-setup should persist skipping setup to the DB', async () => { - const owner = await Db.collections.User!.findOneOrFail(); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + const ownerShell = await testDb.createUserShell(globalOwnerRole); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const response = await authOwnerAgent.post('/owner/skip-setup').send(); @@ -109,13 +117,6 @@ test('POST /owner/skip-setup should persist skipping setup to the DB', async () expect(value).toBe('true'); }); -const TEST_USER = { - email: randomEmail(), - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), -}; - const INVALID_POST_OWNER_PAYLOADS = [ { email: '', diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts similarity index 60% rename from packages/cli/test/integration/passwordReset.endpoints.test.ts rename to packages/cli/test/integration/passwordReset.api.test.ts index 56f58d99bf..4527f039a0 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -11,51 +11,35 @@ import { randomName, randomValidPassword, } from './shared/random'; -import { Role } from '../../src/databases/entities/Role'; import * as testDb from './shared/testDb'; +import type { Role } from '../../src/databases/entities/Role'; jest.mock('../../src/telemetry'); let app: express.Application; -let globalOwnerRole: Role; let testDbName = ''; +let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true }); const initResult = await testDb.init(); testDbName = initResult.testDbName; - await testDb.truncate(['User'], testDbName); - - globalOwnerRole = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); + globalOwnerRole = await testDb.getGlobalOwnerRole(); utils.initTestTelemetry(); utils.initTestLogger(); }); beforeEach(async () => { - jest.isolateModules(() => { - jest.mock('../../config'); - }); + await testDb.truncate(['User'], testDbName); + + jest.mock('../../config'); config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.emails.mode', ''); - await testDb.createUser({ - id: INITIAL_TEST_USER.id, - email: INITIAL_TEST_USER.email, - password: INITIAL_TEST_USER.password, - firstName: INITIAL_TEST_USER.firstName, - lastName: INITIAL_TEST_USER.lastName, - globalRole: globalOwnerRole, - }); -}); - -afterEach(async () => { - await testDb.truncate(['User'], testDbName); + jest.setTimeout(30000); // fake SMTP service might be slow }); afterAll(async () => { @@ -63,47 +47,38 @@ afterAll(async () => { }); test('POST /forgot-password should send password reset email', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); - const { - user, - pass, - smtp: { host, port, secure }, - } = await utils.getSmtpTestAccount(); + await utils.configureSmtp(); - 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 authlessAgent - .post('/forgot-password') - .send({ email: INITIAL_TEST_USER.email }); + const response = await authlessAgent.post('/forgot-password').send({ email: owner.email }); expect(response.statusCode).toBe(200); expect(response.body).toEqual({}); - const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); - expect(owner.resetPasswordToken).toBeDefined(); - expect(owner.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000)); + const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email }); + expect(storedOwner.resetPasswordToken).toBeDefined(); + expect(storedOwner.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000)); }); test('POST /forgot-password should fail if emailing is not set up', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); - const response = await authlessAgent - .post('/forgot-password') - .send({ email: INITIAL_TEST_USER.email }); + const response = await authlessAgent.post('/forgot-password').send({ email: owner.email }); expect(response.statusCode).toBe(500); - const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); - expect(owner.resetPasswordToken).toBeNull(); + const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email }); + expect(storedOwner.resetPasswordToken).toBeNull(); }); test('POST /forgot-password should fail with invalid inputs', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); config.set('userManagement.emails.mode', 'smtp'); @@ -116,13 +91,15 @@ test('POST /forgot-password should fail with invalid inputs', async () => { [{ email: randomName() }], ]; - for (const invalidPayload of invalidPayloads) { - const response = await authlessAgent.post('/forgot-password').send(invalidPayload); - expect(response.statusCode).toBe(400); + await Promise.all( + invalidPayloads.map(async (invalidPayload) => { + const response = await authlessAgent.post('/forgot-password').send(invalidPayload); + expect(response.statusCode).toBe(400); - const owner = await Db.collections.User!.findOneOrFail({ email: INITIAL_TEST_USER.email }); - expect(owner.resetPasswordToken).toBeNull(); - } + const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email }); + expect(storedOwner.resetPasswordToken).toBeNull(); + }), + ); }); test('POST /forgot-password should fail if user is not found', async () => { @@ -132,38 +109,39 @@ test('POST /forgot-password should fail if user is not found', async () => { const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() }); - // response should have 200 to not provide any information to the requester - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200); // expect 200 to remain vague }); test('GET /resolve-password-token should succeed with valid inputs', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); const resetPasswordToken = uuid(); const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; - await Db.collections.User!.update(INITIAL_TEST_USER.id, { + await Db.collections.User!.update(owner.id, { resetPasswordToken, resetPasswordTokenExpiration, }); const response = await authlessAgent .get('/resolve-password-token') - .query({ userId: INITIAL_TEST_USER.id, token: resetPasswordToken }); + .query({ userId: owner.id, token: resetPasswordToken }); expect(response.statusCode).toBe(200); }); test('GET /resolve-password-token should fail with invalid inputs', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); config.set('userManagement.emails.mode', 'smtp'); const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() }); - const second = await authlessAgent - .get('/resolve-password-token') - .query({ userId: INITIAL_TEST_USER.id }); + const second = await authlessAgent.get('/resolve-password-token').query({ userId: owner.id }); for (const response of [first, second]) { expect(response.statusCode).toBe(400); @@ -171,24 +149,28 @@ test('GET /resolve-password-token should fail with invalid inputs', async () => }); test('GET /resolve-password-token should fail if user is not found', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); config.set('userManagement.emails.mode', 'smtp'); const response = await authlessAgent .get('/resolve-password-token') - .query({ userId: INITIAL_TEST_USER.id, token: uuid() }); + .query({ userId: owner.id, token: uuid() }); expect(response.statusCode).toBe(404); }); test('GET /resolve-password-token should fail if token is expired', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); const resetPasswordToken = uuid(); const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1; - await Db.collections.User!.update(INITIAL_TEST_USER.id, { + await Db.collections.User!.update(owner.id, { resetPasswordToken, resetPasswordTokenExpiration, }); @@ -197,18 +179,20 @@ test('GET /resolve-password-token should fail if token is expired', async () => const response = await authlessAgent .get('/resolve-password-token') - .query({ userId: INITIAL_TEST_USER.id, token: resetPasswordToken }); + .query({ userId: owner.id, token: resetPasswordToken }); expect(response.statusCode).toBe(404); }); test('POST /change-password should succeed with valid inputs', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); const resetPasswordToken = uuid(); const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; - await Db.collections.User!.update(INITIAL_TEST_USER.id, { + await Db.collections.User!.update(owner.id, { resetPasswordToken, resetPasswordTokenExpiration, }); @@ -217,7 +201,7 @@ test('POST /change-password should succeed with valid inputs', async () => { const response = await authlessAgent.post('/change-password').send({ token: resetPasswordToken, - userId: INITIAL_TEST_USER.id, + userId: owner.id, password: passwordToStore, }); @@ -226,63 +210,65 @@ test('POST /change-password should succeed with valid inputs', async () => { const authToken = utils.getAuthToken(response); expect(authToken).toBeDefined(); - const { password: storedPassword } = await Db.collections.User!.findOneOrFail( - INITIAL_TEST_USER.id, - ); + const { password: storedPassword } = await Db.collections.User!.findOneOrFail(owner.id); - const comparisonResult = await compare(passwordToStore, storedPassword!); + const comparisonResult = await compare(passwordToStore, storedPassword); expect(comparisonResult).toBe(true); expect(storedPassword).not.toBe(passwordToStore); }); test('POST /change-password should fail with invalid inputs', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); const resetPasswordToken = uuid(); const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; - await Db.collections.User!.update(INITIAL_TEST_USER.id, { + await Db.collections.User!.update(owner.id, { resetPasswordToken, resetPasswordTokenExpiration, }); const invalidPayloads = [ { token: uuid() }, - { id: INITIAL_TEST_USER.id }, + { id: owner.id }, { password: randomValidPassword() }, - { token: uuid(), id: INITIAL_TEST_USER.id }, + { token: uuid(), id: owner.id }, { token: uuid(), password: randomValidPassword() }, - { id: INITIAL_TEST_USER.id, password: randomValidPassword() }, + { id: owner.id, password: randomValidPassword() }, { - id: INITIAL_TEST_USER.id, + id: owner.id, password: randomInvalidPassword(), token: resetPasswordToken, }, { - id: INITIAL_TEST_USER.id, + id: owner.id, password: randomValidPassword(), token: uuid(), }, ]; - const { password: originalHashedPassword } = await Db.collections.User!.findOneOrFail(); + await Promise.all( + invalidPayloads.map(async (invalidPayload) => { + const response = await authlessAgent.post('/change-password').query(invalidPayload); + expect(response.statusCode).toBe(400); - 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); - } + const { password: storedPassword } = await Db.collections.User!.findOneOrFail(); + expect(owner.password).toBe(storedPassword); + }), + ); }); test('POST /change-password should fail when token has expired', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); const resetPasswordToken = uuid(); const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1; - await Db.collections.User!.update(INITIAL_TEST_USER.id, { + await Db.collections.User!.update(owner.id, { resetPasswordToken, resetPasswordTokenExpiration, }); @@ -291,17 +277,9 @@ test('POST /change-password should fail when token has expired', async () => { const response = await authlessAgent.post('/change-password').send({ token: resetPasswordToken, - userId: INITIAL_TEST_USER.id, + userId: owner.id, password: passwordToStore, }); expect(response.statusCode).toBe(404); }); - -const INITIAL_TEST_USER = { - id: uuid(), - email: randomEmail(), - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), -}; diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index e7102d7028..34b2df51ce 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -1,4 +1,4 @@ -import { createConnection, getConnection, ConnectionOptions } from 'typeorm'; +import { createConnection, getConnection, ConnectionOptions, Connection } from 'typeorm'; import { Credentials, UserSettings } from 'n8n-core'; import config = require('../../../config'); @@ -6,17 +6,17 @@ import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } f import { DatabaseType, Db, ICredentialsDb, IDatabaseCollections } from '../../../src'; import { randomEmail, randomName, randomString, randomValidPassword } from './random'; import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity'; - +import { hashPassword } from '../../../src/UserManagement/UserManagementHelper'; import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants'; import { entities } from '../../../src/databases/entities'; import { mysqlMigrations } from '../../../src/databases/mysqldb/migrations'; import { postgresMigrations } from '../../../src/databases/postgresdb/migrations'; import { sqliteMigrations } from '../../../src/databases/sqlite/migrations'; +import { categorize } from './utils'; import type { Role } from '../../../src/databases/entities/Role'; import type { User } from '../../../src/databases/entities/User'; -import type { CredentialPayload } from './types'; -import { genSaltSync, hashSync } from 'bcryptjs'; +import type { CollectionName, CredentialPayload } from './types'; /** * Initialize one test DB per suite run, with bootstrap connection if needed. @@ -97,22 +97,49 @@ export async function terminate(testDbName: string) { } /** - * Truncate DB tables for specified entities. + * Truncate DB tables for collections. * - * @param entities Array of entity names whose tables to truncate. + * @param collections Array of entity names whose tables to truncate. * @param testDbName Name of the test DB to truncate tables in. */ -export async function truncate(entities: Array, testDbName: string) { +export async function truncate(collections: CollectionName[], testDbName: string) { const dbType = config.get('database.type'); + const testDb = getConnection(testDbName); + if (dbType === 'sqlite') { - const testDb = getConnection(testDbName); await testDb.query('PRAGMA foreign_keys=OFF'); - await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); + await Promise.all(collections.map((collection) => Db.collections[collection]!.clear())); return testDb.query('PRAGMA foreign_keys=ON'); } - const map: { [K in keyof IDatabaseCollections]: string } = { + if (dbType === 'postgresdb') { + return Promise.all( + collections.map((collection) => { + const tableName = toTableName(collection); + testDb.query(`TRUNCATE TABLE "${tableName}" RESTART IDENTITY CASCADE;`); + }), + ); + } + + /** + * MySQL `TRUNCATE` requires enabling and disabling the global variable `foreign_key_checks`, + * which cannot be safely manipulated by parallel tests, so use `DELETE` and `AUTO_INCREMENT`. + * Clear shared tables first to avoid deadlock: https://stackoverflow.com/a/41174997 + */ + if (dbType === 'mysqldb') { + const { pass: isShared, fail: isNotShared } = categorize( + collections, + (collectionName: CollectionName) => collectionName.toLowerCase().startsWith('shared'), + ); + + await truncateMySql(testDb, isShared); + await truncateMySql(testDb, isNotShared); + } +} + +function toTableName(collectionName: CollectionName) { + return { Credentials: 'credentials_entity', Workflow: 'workflow_entity', Execution: 'execution_entity', @@ -123,27 +150,17 @@ export async function truncate(entities: Array, test SharedCredentials: 'shared_credentials', SharedWorkflow: 'shared_workflow', Settings: 'settings', - }; + }[collectionName]; +} - if (dbType === 'postgresdb') { - return Promise.all( - entities.map((entity) => - getConnection(testDbName).query( - `TRUNCATE TABLE "${map[entity]}" RESTART IDENTITY CASCADE;`, - ), - ), - ); - } - - // MySQL truncation requires globals, which cannot be safely manipulated by parallel tests - if (dbType === 'mysqldb') { - await Promise.all( - entities.map(async (entity) => { - await Db.collections[entity]!.delete({}); - await getConnection(testDbName).query(`ALTER TABLE ${map[entity]} AUTO_INCREMENT = 1;`); - }), - ); - } +function truncateMySql(connection: Connection, collections: Array) { + return Promise.all( + collections.map(async (collection) => { + const tableName = toTableName(collection); + await connection.query(`DELETE FROM ${tableName};`); + await connection.query(`ALTER TABLE ${tableName} AUTO_INCREMENT = 1;`); + }), + ); } // ---------------------------------- @@ -179,63 +196,65 @@ export async function saveCredential( } // ---------------------------------- -// user creation +// user creation // ---------------------------------- -/** - * Store a user in the DB, defaulting to a `member`. - */ -export async function createUser(attributes: Partial = {}): Promise { +export async function createUser(attributes: Partial & { globalRole: Role }): Promise { const { email, password, firstName, lastName, globalRole, ...rest } = attributes; + const user = { email: email ?? randomEmail(), - password: hashSync(password ?? randomValidPassword(), genSaltSync(10)), + password: await hashPassword(password ?? randomValidPassword()), firstName: firstName ?? randomName(), lastName: lastName ?? randomName(), - globalRole: globalRole ?? (await getGlobalMemberRole()), + globalRole, ...rest, }; return Db.collections.User!.save(user); } -export async function createOwnerShell() { - const globalRole = await getGlobalOwnerRole(); - return Db.collections.User!.save({ globalRole }); -} +export function createUserShell(globalRole: Role): Promise { + if (globalRole.scope !== 'global') { + throw new Error(`Invalid role received: ${JSON.stringify(globalRole)}`); + } -export async function createMemberShell() { - const globalRole = await getGlobalMemberRole(); - return Db.collections.User!.save({ globalRole }); + const shell: Partial = { globalRole }; + + if (globalRole.name !== 'owner') { + shell.email = randomEmail(); + } + + return Db.collections.User!.save(shell); } // ---------------------------------- // role fetchers // ---------------------------------- -export async function getGlobalOwnerRole() { - return await Db.collections.Role!.findOneOrFail({ +export function getGlobalOwnerRole() { + return Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global', }); } -export async function getGlobalMemberRole() { - return await Db.collections.Role!.findOneOrFail({ +export function getGlobalMemberRole() { + return Db.collections.Role!.findOneOrFail({ name: 'member', scope: 'global', }); } -export async function getWorkflowOwnerRole() { - return await Db.collections.Role!.findOneOrFail({ +export function getWorkflowOwnerRole() { + return Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'workflow', }); } -export async function getCredentialOwnerRole() { - return await Db.collections.Role!.findOneOrFail({ +export function getCredentialOwnerRole() { + return Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'credential', }); diff --git a/packages/cli/test/integration/shared/types.d.ts b/packages/cli/test/integration/shared/types.d.ts index b160faea01..bca1c86fbc 100644 --- a/packages/cli/test/integration/shared/types.d.ts +++ b/packages/cli/test/integration/shared/types.d.ts @@ -1,8 +1,10 @@ import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-workflow'; -import type { ICredentialsDb } from '../../../src'; +import type { ICredentialsDb, IDatabaseCollections } from '../../../src'; import type { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity'; import type { User } from '../../../src/databases/entities/User'; +export type CollectionName = keyof IDatabaseCollections; + export type SmtpTestAccount = { user: string; pass: string; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 285c214d29..14d17a21f1 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -23,9 +23,7 @@ import { passwordResetNamespace as passwordResetEndpoints } from '../../../src/U import { issueJWT } from '../../../src/UserManagement/auth/jwt'; import { getLogger } from '../../../src/Logger'; import { credentialsController } from '../../../src/api/credentials.api'; - import type { User } from '../../../src/databases/entities/User'; -import { Telemetry } from '../../../src/telemetry'; import type { EndpointGroup, SmtpTestAccount } from './types'; import type { N8nApp } from '../../../src/UserManagement/Interfaces'; @@ -182,9 +180,7 @@ export function prefix(pathSegment: string) { export function getAuthToken(response: request.Response, authCookieName = AUTH_COOKIE_NAME) { const cookies: string[] = response.headers['set-cookie']; - if (!cookies) { - throw new Error("No 'set-cookie' header found in response"); - } + if (!cookies) return undefined; const authCookie = cookies.find((c) => c.startsWith(`${authCookieName}=`)); @@ -216,5 +212,37 @@ export async function isInstanceOwnerSetUp() { /** * Get an SMTP test account from https://ethereal.email to test sending emails. */ -export const getSmtpTestAccount = util.promisify(createTestAccount); +const getSmtpTestAccount = util.promisify(createTestAccount); +export async function configureSmtp() { + const { + user, + pass, + smtp: { host, port, secure }, + } = await getSmtpTestAccount(); + + config.set('userManagement.emails.mode', 'smtp'); + config.set('userManagement.emails.smtp.host', host); + config.set('userManagement.emails.smtp.port', port); + config.set('userManagement.emails.smtp.secure', secure); + config.set('userManagement.emails.smtp.auth.user', user); + config.set('userManagement.emails.smtp.auth.pass', pass); +} + +// ---------------------------------- +// misc +// ---------------------------------- + +/** + * Categorize array items into two groups based on whether they pass a test. + */ +export const categorize = (arr: T[], test: (str: T) => boolean) => { + return arr.reduce<{ pass: T[]; fail: T[] }>( + (acc, cur) => { + test(cur) ? acc.pass.push(cur) : acc.fail.push(cur); + + return acc; + }, + { pass: [], fail: [] }, + ); +}; diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.api.test.ts similarity index 61% rename from packages/cli/test/integration/users.endpoints.test.ts rename to packages/cli/test/integration/users.api.test.ts index d11633282f..651bb4a454 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -1,12 +1,10 @@ import express = require('express'); import validator from 'validator'; import { v4 as uuid } from 'uuid'; -import { compare, genSaltSync, hashSync } from 'bcryptjs'; import { Db } from '../../src'; import config = require('../../config'); import { SUCCESS_RESPONSE_BODY } from './shared/constants'; -import { Role } from '../../src/databases/entities/Role'; import { randomEmail, randomValidPassword, @@ -15,15 +13,18 @@ import { } from './shared/random'; import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; +import type { Role } from '../../src/databases/entities/Role'; +import type { User } from '../../src/databases/entities/User'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; +import { compareHash } from '../../src/UserManagement/UserManagementHelper'; jest.mock('../../src/telemetry'); let app: express.Application; let testDbName = ''; -let globalOwnerRole: Role; let globalMemberRole: Role; +let globalOwnerRole: Role; let workflowOwnerRole: Role; let credentialOwnerRole: Role; @@ -46,25 +47,17 @@ beforeAll(async () => { utils.initTestTelemetry(); utils.initTestLogger(); + + jest.setTimeout(30000); // fake SMTP service might be slow }); beforeEach(async () => { - // do not combine calls - shared tables must be cleared first and separately - await testDb.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName); - await testDb.truncate(['User', 'Workflow', 'Credentials'], testDbName); + await testDb.truncate( + ['User', 'SharedCredentials', 'SharedWorkflow', 'Workflow', 'Credentials'], + testDbName, + ); - jest.isolateModules(() => { - jest.mock('../../config'); - }); - - await testDb.createUser({ - id: INITIAL_TEST_USER.id, - email: INITIAL_TEST_USER.email, - password: INITIAL_TEST_USER.password, - firstName: INITIAL_TEST_USER.firstName, - lastName: INITIAL_TEST_USER.lastName, - globalRole: globalOwnerRole, - }); + jest.mock('../../config'); config.set('userManagement.disabled', false); config.set('userManagement.isInstanceOwnerSetUp', true); @@ -76,46 +69,48 @@ afterAll(async () => { }); test('GET /users should return all users', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - await testDb.createUser(); + await testDb.createUser({ globalRole: 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 { - id, - email, - firstName, - lastName, - personalizationAnswers, - globalRole, - password, - resetPasswordToken, - isPending, - } = user; + await Promise.all( + response.body.data.map(async (user: User) => { + const { + id, + email, + firstName, + lastName, + personalizationAnswers, + globalRole, + password, + resetPasswordToken, + isPending, + } = user; - expect(validator.isUUID(id)).toBe(true); - expect(email).toBeDefined(); - expect(firstName).toBeDefined(); - expect(lastName).toBeDefined(); - expect(personalizationAnswers).toBeUndefined(); - expect(password).toBeUndefined(); - expect(resetPasswordToken).toBeUndefined(); - expect(isPending).toBe(false); - expect(globalRole).toBeDefined(); - } + expect(validator.isUUID(id)).toBe(true); + expect(email).toBeDefined(); + expect(firstName).toBeDefined(); + expect(lastName).toBeDefined(); + expect(personalizationAnswers).toBeUndefined(); + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(isPending).toBe(false); + expect(globalRole).toBeDefined(); + }), + ); }); test('DELETE /users/:id should delete the user', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const userToDelete = await testDb.createUser(); + const userToDelete = await testDb.createUser({ globalRole: globalMemberRole }); const newWorkflow = new WorkflowEntity(); @@ -181,7 +176,7 @@ test('DELETE /users/:id should delete the user', async () => { }); test('DELETE /users/:id should fail to delete self', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const response = await authOwnerAgent.delete(`/users/${owner.id}`); @@ -193,10 +188,10 @@ test('DELETE /users/:id should fail to delete self', async () => { }); test('DELETE /users/:id should fail if user to delete is transferee', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const { id: idToDelete } = await testDb.createUser(); + const { id: idToDelete } = await testDb.createUser({ globalRole: globalMemberRole }); const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({ transferId: idToDelete, @@ -209,7 +204,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () = }); test('DELETE /users/:id with transferId should perform transfer', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const userToDelete = await Db.collections.User!.save({ @@ -281,36 +276,34 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { }); test('GET /resolve-signup-token should validate invite token', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const { id: inviteeId } = await testDb.createMemberShell(); + const memberShell = await testDb.createUserShell(globalMemberRole); const response = await authOwnerAgent .get('/resolve-signup-token') - .query({ inviterId: INITIAL_TEST_USER.id }) - .query({ inviteeId }); + .query({ inviterId: owner.id }) + .query({ inviteeId: memberShell.id }); expect(response.statusCode).toBe(200); expect(response.body).toEqual({ data: { inviter: { - firstName: INITIAL_TEST_USER.firstName, - lastName: INITIAL_TEST_USER.lastName, + firstName: owner.firstName, + lastName: owner.lastName, }, }, }); }); test('GET /resolve-signup-token should fail with invalid inputs', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const { id: inviteeId } = await testDb.createUser(); + const { id: inviteeId } = await testDb.createUser({ globalRole: globalMemberRole }); - const first = await authOwnerAgent - .get('/resolve-signup-token') - .query({ inviterId: INITIAL_TEST_USER.id }); + const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id }); const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId }); @@ -322,14 +315,14 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { // user is already set up, so call should error const fourth = await authOwnerAgent .get('/resolve-signup-token') - .query({ inviterId: INITIAL_TEST_USER.id }) + .query({ inviterId: owner.id }) .query({ inviteeId }); // cause inconsistent DB state await Db.collections.User!.update(owner.id, { email: '' }); const fifth = await authOwnerAgent .get('/resolve-signup-token') - .query({ inviterId: INITIAL_TEST_USER.id }) + .query({ inviterId: owner.id }) .query({ inviteeId }); for (const response of [first, second, third, fourth, fifth]) { @@ -338,21 +331,20 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { }); test('POST /users/:id should fill out a user shell', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + + const memberShell = await testDb.createUserShell(globalMemberRole); + + const memberData = { + inviterId: owner.id, + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }; + const authlessAgent = utils.createAgent(app); - const userToFillOut = await Db.collections.User!.save({ - email: randomEmail(), - globalRole: globalMemberRole, - }); - - const newPassword = randomValidPassword(); - - const response = await authlessAgent.post(`/users/${userToFillOut.id}`).send({ - inviterId: INITIAL_TEST_USER.id, - firstName: INITIAL_TEST_USER.firstName, - lastName: INITIAL_TEST_USER.lastName, - password: newPassword, - }); + const response = await authlessAgent.post(`/users/${memberShell.id}`).send(memberData); const { id, @@ -368,8 +360,8 @@ test('POST /users/:id should fill out a user shell', async () => { expect(validator.isUUID(id)).toBe(true); expect(email).toBeDefined(); - expect(firstName).toBe(INITIAL_TEST_USER.firstName); - expect(lastName).toBe(INITIAL_TEST_USER.lastName); + expect(firstName).toBe(memberData.firstName); + expect(lastName).toBe(memberData.lastName); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(resetPasswordToken).toBeUndefined(); @@ -379,69 +371,98 @@ 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); + const member = await Db.collections.User!.findOneOrFail(memberShell.id); + expect(member.firstName).toBe(memberData.firstName); + expect(member.lastName).toBe(memberData.lastName); + expect(member.password).not.toBe(memberData.password); }); test('POST /users/:id should fail with invalid inputs', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authlessAgent = utils.createAgent(app); - const emailToStore = randomEmail(); + const memberShellEmail = randomEmail(); - const userToFillOut = await Db.collections.User!.save({ - email: emailToStore, + const memberShell = await Db.collections.User!.save({ + email: memberShellEmail, 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 invalidPayloads = [ + { + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }, + { + inviterId: owner.id, + firstName: randomName(), + password: randomValidPassword(), + }, + { + inviterId: owner.id, + firstName: randomName(), + password: randomValidPassword(), + }, + { + inviterId: owner.id, + firstName: randomName(), + lastName: randomName(), + }, + { + inviterId: owner.id, + firstName: randomName(), + lastName: randomName(), + password: randomInvalidPassword(), + }, + ]; - const user = await Db.collections.User!.findOneOrFail({ where: { email: emailToStore } }); - expect(user.firstName).toBeNull(); - expect(user.lastName).toBeNull(); - expect(user.password).toBeNull(); - } + await Promise.all( + invalidPayloads.map(async (invalidPayload) => { + const response = await authlessAgent.post(`/users/${memberShell.id}`).send(invalidPayload); + expect(response.statusCode).toBe(400); + + const storedUser = await Db.collections.User!.findOneOrFail({ + where: { email: memberShellEmail }, + }); + expect(storedUser.firstName).toBeNull(); + expect(storedUser.lastName).toBeNull(); + expect(storedUser.password).toBeNull(); + }), + ); }); test('POST /users/:id should fail with already accepted invite', async () => { - const authlessAgent = utils.createAgent(app); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const member = await testDb.createUser({ globalRole: globalMemberRole }); - const globalMemberRole = await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); - - const shell = await Db.collections.User!.save({ - email: randomEmail(), - password: hashSync(randomValidPassword(), genSaltSync(10)), // simulate accepted invite - globalRole: globalMemberRole, - }); - - const newPassword = randomValidPassword(); - - const response = await authlessAgent.post(`/users/${shell.id}`).send({ - inviterId: INITIAL_TEST_USER.id, + const newMemberData = { + inviterId: owner.id, firstName: randomName(), lastName: randomName(), password: randomValidPassword(), - }); + }; + + const authlessAgent = utils.createAgent(app); + + const response = await authlessAgent.post(`/users/${member.id}`).send(newMemberData); expect(response.statusCode).toBe(400); - const fetchedShell = await Db.collections.User!.findOneOrFail({ where: { email: shell.email } }); - expect(fetchedShell.firstName).toBeNull(); - expect(fetchedShell.lastName).toBeNull(); + const storedMember = await Db.collections.User!.findOneOrFail({ + where: { email: member.email }, + }); + expect(storedMember.firstName).not.toBe(newMemberData.firstName); + expect(storedMember.lastName).not.toBe(newMemberData.lastName); - const comparisonResult = await compare(shell.password, newPassword); + const comparisonResult = await compareHash(member.password, storedMember.password); expect(comparisonResult).toBe(false); - expect(newPassword).not.toBe(fetchedShell.password); + expect(storedMember.password).not.toBe(newMemberData.password); }); test('POST /users should fail if emailing is not set up', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]); @@ -450,7 +471,7 @@ test('POST /users should fail if emailing is not set up', async () => { }); test('POST /users should fail if user management is disabled', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); config.set('userManagement.disabled', true); @@ -461,54 +482,47 @@ test('POST /users should fail if user management is disabled', async () => { }); test('POST /users should email invites and create user shells', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const { - user, - pass, - smtp: { host, port, secure }, - } = await utils.getSmtpTestAccount(); + await utils.configureSmtp(); - 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 testEmails = [randomEmail(), randomEmail(), randomEmail()]; - const payload = TEST_EMAILS_TO_CREATE_USER_SHELLS.map((e) => ({ email: e })); + const payload = testEmails.map((e) => ({ email: e })); const response = await authOwnerAgent.post('/users').send(payload); expect(response.statusCode).toBe(200); - for (const { - user: { id, email: receivedEmail }, - error, - } of response.body.data) { - expect(validator.isUUID(id)).toBe(true); - expect(TEST_EMAILS_TO_CREATE_USER_SHELLS.some((e) => e === receivedEmail)).toBe(true); - if (error) { - expect(error).toBe('Email could not be sent'); - } + await Promise.all( + response.body.data.map(async ({ user, error }: { user: User; error: Error }) => { + const { id, email: receivedEmail } = user; - const user = await Db.collections.User!.findOneOrFail(id); - const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } = user; + expect(validator.isUUID(id)).toBe(true); + expect(testEmails.some((e) => e === receivedEmail)).toBe(true); + if (error) { + expect(error).toBe('Email could not be sent'); + } - expect(firstName).toBeNull(); - expect(lastName).toBeNull(); - expect(personalizationAnswers).toBeNull(); - expect(password).toBeNull(); - expect(resetPasswordToken).toBeNull(); - } + const storedUser = await Db.collections.User!.findOneOrFail(id); + const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } = + storedUser; + + 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 owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - config.set('userManagement.emails.mode', 'smtp'); + await utils.configureSmtp(); const invalidPayloads = [ randomEmail(), @@ -518,20 +532,22 @@ test('POST /users should fail with invalid inputs', async () => { [{ email: randomName() }], ]; - for (const invalidPayload of invalidPayloads) { - const response = await authOwnerAgent.post('/users').send(invalidPayload); - expect(response.statusCode).toBe(400); + await Promise.all( + invalidPayloads.map(async (invalidPayload) => { + 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 - } + const users = await Db.collections.User!.find(); + expect(users.length).toBe(1); // DB unaffected + }), + ); }); test('POST /users should ignore an empty payload', async () => { - const owner = await Db.collections.User!.findOneOrFail(); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - config.set('userManagement.emails.mode', 'smtp'); + await utils.configureSmtp(); const response = await authOwnerAgent.post('/users').send([]); @@ -561,42 +577,3 @@ test('POST /users should ignore an empty payload', async () => { // expect(response.statusCode).toBe(500); // }); - -const INITIAL_TEST_USER = { - id: uuid(), - email: randomEmail(), - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), -}; - -const INVALID_FILL_OUT_USER_PAYLOADS = [ - { - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), - }, - { - inviterId: INITIAL_TEST_USER.id, - firstName: randomName(), - password: randomValidPassword(), - }, - { - inviterId: INITIAL_TEST_USER.id, - firstName: randomName(), - password: randomValidPassword(), - }, - { - inviterId: INITIAL_TEST_USER.id, - firstName: randomName(), - lastName: randomName(), - }, - { - inviterId: INITIAL_TEST_USER.id, - firstName: randomName(), - lastName: randomName(), - password: randomInvalidPassword(), - }, -]; - -const TEST_EMAILS_TO_CREATE_USER_SHELLS = [randomEmail(), randomEmail(), randomEmail()];