From 6242cac53baaed7e5c8be3ba477cd7f408fa1430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 17 Mar 2023 17:24:05 +0100 Subject: [PATCH] ci: Refactor cli tests to speed up CI (no-changelog) (#5718) * ci: Refactor cli tests to speed up CI (no-changelog) * upgrade jest to address memory leaks --- jest.config.js | 3 +- package.json | 10 +- packages/cli/src/index.ts | 31 +- .../cli/test/integration/auth.api.test.ts | 571 +++--- packages/cli/test/integration/auth.mw.test.ts | 32 +- .../test/integration/credentials.ee.test.ts | 809 ++++---- .../cli/test/integration/credentials.test.ts | 862 ++++----- .../cli/test/integration/eventbus.test.ts | 14 +- .../test/integration/ldap/ldap.api.test.ts | 12 +- .../cli/test/integration/license.api.test.ts | 173 +- packages/cli/test/integration/me.api.test.ts | 98 +- .../cli/test/integration/nodes.api.test.ts | 382 ++-- .../cli/test/integration/owner.api.test.ts | 290 ++- .../integration/passwordReset.api.test.ts | 393 ++-- .../integration/publicApi/credentials.test.ts | 520 +++-- .../integration/publicApi/executions.test.ts | 666 +++---- .../integration/publicApi/workflows.test.ts | 1716 ++++++++--------- .../test/integration/saml/saml.api.test.ts | 99 +- .../test/integration/shared/augmentation.d.ts | 1 - packages/cli/test/integration/shared/utils.ts | 18 +- .../cli/test/integration/users.api.test.ts | 806 ++++---- .../workflows.controller.ee.test.ts | 422 ++-- .../integration/workflows.controller.test.ts | 92 +- .../test/unit/ActiveWorkflowRunner.test.ts | 2 +- .../cli/test/unit/CredentialsHelper.test.ts | 59 +- packages/cli/test/unit/Events.test.ts | 50 +- packages/cli/test/unit/Helpers.ts | 106 +- .../cli/test/unit/PermissionChecker.test.ts | 57 +- pnpm-lock.yaml | 697 ++++--- 29 files changed, 4229 insertions(+), 4762 deletions(-) diff --git a/jest.config.js b/jest.config.js index f371b1de8d..e34e01fa22 100644 --- a/jest.config.js +++ b/jest.config.js @@ -28,8 +28,7 @@ const config = { }; if (process.env.CI === 'true') { - config.maxWorkers = 2; - config.workerIdleMemoryLimit = 2048; + config.workerIdleMemoryLimit = 1024; config.coverageReporters = ['cobertura']; } diff --git a/package.json b/package.json index 3aa0444287..e69c7a2986 100644 --- a/package.json +++ b/package.json @@ -39,15 +39,15 @@ "devDependencies": { "@n8n_io/eslint-config": "workspace:*", "@ngneat/falso": "^6.1.0", - "@types/jest": "^29.2.2", + "@types/jest": "^29.5.0", "@types/supertest": "^2.0.12", "cross-env": "^7.0.3", "cypress": "^12.7.0", "cypress-real-events": "^1.7.6", - "jest": "^29.4.2", - "jest-environment-jsdom": "^29.4.2", - "jest-mock": "^29.4.2", - "jest-mock-extended": "^3.0.1", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "jest-mock": "^29.5.0", + "jest-mock-extended": "^3.0.3", "nock": "^13.2.9", "node-fetch": "^2.6.7", "p-limit": "^3.1.0", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 6b336cf807..cb0ff5c3b5 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,30 +1 @@ -/* eslint-disable import/first */ -export * from './CredentialsHelper'; -export * from './CredentialTypes'; -export * from './CredentialsOverwrites'; -export * from './Interfaces'; -export * from './WaitingWebhooks'; -export * from './WorkflowCredentials'; -export * from './WorkflowRunner'; - -import * as Db from './Db'; -import * as GenericHelpers from './GenericHelpers'; -import * as ResponseHelper from './ResponseHelper'; -import * as Server from './Server'; -import * as TestWebhooks from './TestWebhooks'; -import * as WebhookHelpers from './WebhookHelpers'; -import * as WebhookServer from './WebhookServer'; -import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData'; -import * as WorkflowHelpers from './WorkflowHelpers'; - -export { - Db, - GenericHelpers, - ResponseHelper, - Server, - TestWebhooks, - WebhookHelpers, - WebhookServer, - WorkflowExecuteAdditionalData, - WorkflowHelpers, -}; +export {}; diff --git a/packages/cli/test/integration/auth.api.test.ts b/packages/cli/test/integration/auth.api.test.ts index cad139d0ce..e81cf06aa3 100644 --- a/packages/cli/test/integration/auth.api.test.ts +++ b/packages/cli/test/integration/auth.api.test.ts @@ -1,339 +1,338 @@ -import express from 'express'; +import type { Application } from 'express'; +import type { SuperAgentTest } from 'supertest'; import validator from 'validator'; import config from '@/config'; import * as Db from '@/Db'; import { AUTH_COOKIE_NAME } from '@/constants'; import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; import { randomValidPassword } from './shared/random'; import * as testDb from './shared/testDb'; import type { AuthAgent } from './shared/types'; import * as utils from './shared/utils'; -let app: express.Application; +let app: Application; let globalOwnerRole: Role; let globalMemberRole: Role; +let owner: User; let authAgent: AuthAgent; +let authlessAgent: SuperAgentTest; +let authOwnerAgent: SuperAgentTest; +const ownerPassword = randomValidPassword(); beforeAll(async () => { app = await utils.initTestServer({ endpointGroups: ['auth'] }); + authAgent = utils.createAuthAgent(app); globalOwnerRole = await testDb.getGlobalOwnerRole(); globalMemberRole = await testDb.getGlobalMemberRole(); - - authAgent = utils.createAuthAgent(app); }); beforeEach(async () => { await testDb.truncate(['User']); - + authlessAgent = utils.createAgent(app); config.set('ldap.disabled', true); - - config.set('userManagement.isInstanceOwnerSetUp', true); - - await Db.collections.Settings.update( - { key: 'userManagement.isInstanceOwnerSetUp' }, - { value: JSON.stringify(true) }, - ); + await utils.setInstanceOwnerSetUp(true); }); afterAll(async () => { await testDb.terminate(); }); -test('POST /login should log user in', async () => { - const ownerPassword = randomValidPassword(); - const owner = await testDb.createUser({ - password: ownerPassword, - globalRole: globalOwnerRole, +describe('POST /login', () => { + beforeEach(async () => { + owner = await testDb.createUser({ + password: ownerPassword, + globalRole: globalOwnerRole, + }); }); - const authlessAgent = utils.createAgent(app); + test('should log user in', async () => { + const response = await authlessAgent.post('/login').send({ + email: owner.email, + password: ownerPassword, + }); - 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, + apiKey, + } = 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'); + expect(apiKey).toBeUndefined(); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); + }); +}); + +describe('GET /login', () => { + test('should return 401 Unauthorized if no cookie', async () => { + const response = await authlessAgent.get('/login'); + + expect(response.statusCode).toBe(401); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); }); - expect(response.statusCode).toBe(200); + test('should return cookie if UM is disabled and no cookie is already set', async () => { + await testDb.createUserShell(globalOwnerRole); + await utils.setInstanceOwnerSetUp(false); - const { - id, - email, - firstName, - lastName, - password, - personalizationAnswers, - globalRole, - resetPasswordToken, - apiKey, - } = response.body.data; + const response = await authlessAgent.get('/login'); - 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'); - expect(apiKey).toBeUndefined(); + expect(response.statusCode).toBe(200); - const authToken = utils.getAuthToken(response); - expect(authToken).toBeDefined(); + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); + }); + + test('should return 401 Unauthorized if invalid cookie', async () => { + authlessAgent.jar.setCookie(`${AUTH_COOKIE_NAME}=invalid`); + + const response = await authlessAgent.get('/login'); + + expect(response.statusCode).toBe(401); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); + }); + + test('should return logged-in owner shell', async () => { + const ownerShell = await testDb.createUserShell(globalOwnerRole); + + const response = await authAgent(ownerShell).get('/login'); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + apiKey, + } = 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'); + expect(apiKey).toBeUndefined(); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); + }); + + test('should return logged-in member shell', async () => { + const memberShell = await testDb.createUserShell(globalMemberRole); + + const response = await authAgent(memberShell).get('/login'); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + apiKey, + } = 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'); + expect(apiKey).toBeUndefined(); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); + }); + + test('should return logged-in owner', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + + const response = await authAgent(owner).get('/login'); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + apiKey, + } = 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'); + expect(apiKey).toBeUndefined(); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); + }); + + test('should return logged-in member', async () => { + const member = await testDb.createUser({ globalRole: globalMemberRole }); + + const response = await authAgent(member).get('/login'); + + expect(response.statusCode).toBe(200); + + const { + id, + email, + firstName, + lastName, + password, + personalizationAnswers, + globalRole, + resetPasswordToken, + apiKey, + } = 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'); + expect(apiKey).toBeUndefined(); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); + }); }); -test('GET /login should return 401 Unauthorized if no cookie', async () => { - const authlessAgent = utils.createAgent(app); +describe('GET /resolve-signup-token', () => { + beforeEach(async () => { + owner = await testDb.createUser({ + password: ownerPassword, + globalRole: globalOwnerRole, + }); + authOwnerAgent = authAgent(owner); + }); - const response = await authlessAgent.get('/login'); + test('should validate invite token', async () => { + const memberShell = await testDb.createUserShell(globalMemberRole); - expect(response.statusCode).toBe(401); + const response = await authOwnerAgent + .get('/resolve-signup-token') + .query({ inviterId: owner.id }) + .query({ inviteeId: memberShell.id }); - const authToken = utils.getAuthToken(response); - expect(authToken).toBeUndefined(); -}); - -test('GET /login should return cookie if UM is disabled and no cookie is already set', async () => { - const authlessAgent = utils.createAgent(app); - await testDb.createUserShell(globalOwnerRole); - - config.set('userManagement.isInstanceOwnerSetUp', false); - - await Db.collections.Settings.update( - { key: 'userManagement.isInstanceOwnerSetUp' }, - { value: JSON.stringify(false) }, - ); - - const response = await authlessAgent.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 response = await authAgent(ownerShell).get('/login'); - - expect(response.statusCode).toBe(200); - - const { - id, - email, - firstName, - lastName, - password, - personalizationAnswers, - globalRole, - resetPasswordToken, - apiKey, - } = 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'); - expect(apiKey).toBeUndefined(); - - 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 response = await authAgent(memberShell).get('/login'); - - expect(response.statusCode).toBe(200); - - const { - id, - email, - firstName, - lastName, - password, - personalizationAnswers, - globalRole, - resetPasswordToken, - apiKey, - } = 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'); - expect(apiKey).toBeUndefined(); - - 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 response = await authAgent(owner).get('/login'); - - expect(response.statusCode).toBe(200); - - const { - id, - email, - firstName, - lastName, - password, - personalizationAnswers, - globalRole, - resetPasswordToken, - apiKey, - } = 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'); - expect(apiKey).toBeUndefined(); - - 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 response = await authAgent(member).get('/login'); - - expect(response.statusCode).toBe(200); - - const { - id, - email, - firstName, - lastName, - password, - personalizationAnswers, - globalRole, - resetPasswordToken, - apiKey, - } = 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'); - expect(apiKey).toBeUndefined(); - - const authToken = utils.getAuthToken(response); - expect(authToken).toBeUndefined(); -}); - -test('GET /resolve-signup-token should validate invite token', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const memberShell = await testDb.createUserShell(globalMemberRole); - - const response = await authAgent(owner) - .get('/resolve-signup-token') - .query({ inviterId: owner.id }) - .query({ inviteeId: memberShell.id }); - - expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ - data: { - inviter: { - firstName: owner.firstName, - lastName: owner.lastName, + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ + data: { + inviter: { + firstName: owner.firstName, + lastName: owner.lastName, + }, }, - }, + }); + }); + + test('should fail with invalid inputs', async () => { + const { id: inviteeId } = await testDb.createUser({ globalRole: globalMemberRole }); + + const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id }); + + const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId }); + + const third = await authOwnerAgent.get('/resolve-signup-token').query({ + inviterId: '5531199e-b7ae-425b-a326-a95ef8cca59d', + inviteeId: 'cb133beb-7729-4c34-8cd1-a06be8834d9d', + }); + + // user is already set up, so call should error + const fourth = await authOwnerAgent + .get('/resolve-signup-token') + .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: owner.id }) + .query({ inviteeId }); + + for (const response of [first, second, third, fourth, fifth]) { + expect(response.statusCode).toBe(400); + } }); }); -test('GET /resolve-signup-token should fail with invalid inputs', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const authOwnerAgent = authAgent(owner); +describe('POST /logout', () => { + test('should log user out', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const { id: inviteeId } = await testDb.createUser({ globalRole: globalMemberRole }); + const response = await authAgent(owner).post('/logout'); - const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id }); + expect(response.statusCode).toBe(200); + expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY); - const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId }); - - const third = await authOwnerAgent.get('/resolve-signup-token').query({ - inviterId: '5531199e-b7ae-425b-a326-a95ef8cca59d', - inviteeId: 'cb133beb-7729-4c34-8cd1-a06be8834d9d', + const authToken = utils.getAuthToken(response); + expect(authToken).toBeUndefined(); }); - - // user is already set up, so call should error - const fourth = await authOwnerAgent - .get('/resolve-signup-token') - .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: owner.id }) - .query({ inviteeId }); - - for (const response of [first, second, third, fourth, fifth]) { - expect(response.statusCode).toBe(400); - } -}); - -test('POST /logout should log user out', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const response = await authAgent(owner).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.mw.test.ts b/packages/cli/test/integration/auth.mw.test.ts index 94291ddfc8..1c4c5b72a6 100644 --- a/packages/cli/test/integration/auth.mw.test.ts +++ b/packages/cli/test/integration/auth.mw.test.ts @@ -1,26 +1,21 @@ -import express from 'express'; - -import request from 'supertest'; -import type { Role } from '@db/entities/Role'; +import type { SuperAgentTest } from 'supertest'; import { - REST_PATH_SEGMENT, ROUTES_REQUIRING_AUTHENTICATION, ROUTES_REQUIRING_AUTHORIZATION, } from './shared/constants'; import * as testDb from './shared/testDb'; -import type { AuthAgent } from './shared/types'; import * as utils from './shared/utils'; -let app: express.Application; -let globalMemberRole: Role; -let authAgent: AuthAgent; +let authlessAgent: SuperAgentTest; +let authMemberAgent: SuperAgentTest; beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['me', 'auth', 'owner', 'users'] }); + const app = await utils.initTestServer({ endpointGroups: ['me', 'auth', 'owner', 'users'] }); + const globalMemberRole = await testDb.getGlobalMemberRole(); + const member = await testDb.createUser({ globalRole: globalMemberRole }); - globalMemberRole = await testDb.getGlobalMemberRole(); - - authAgent = utils.createAuthAgent(app); + authlessAgent = utils.createAgent(app); + authMemberAgent = utils.createAuthAgent(app)(member); }); afterAll(async () => { @@ -31,9 +26,8 @@ ROUTES_REQUIRING_AUTHENTICATION.concat(ROUTES_REQUIRING_AUTHORIZATION).forEach(( const [method, endpoint] = getMethodAndEndpoint(route); test(`${route} should return 401 Unauthorized if no cookie`, async () => { - const response = await request(app)[method](endpoint).use(utils.prefix(REST_PATH_SEGMENT)); - - expect(response.statusCode).toBe(401); + const { statusCode } = await authlessAgent[method](endpoint); + expect(statusCode).toBe(401); }); }); @@ -41,10 +35,8 @@ 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({ globalRole: globalMemberRole }); - const response = await authAgent(member)[method](endpoint); - - expect(response.statusCode).toBe(403); + const { statusCode } = await authMemberAgent[method](endpoint); + expect(statusCode).toBe(403); }); }); diff --git a/packages/cli/test/integration/credentials.ee.test.ts b/packages/cli/test/integration/credentials.ee.test.ts index 81313739e4..139975eb5b 100644 --- a/packages/cli/test/integration/credentials.ee.test.ts +++ b/packages/cli/test/integration/credentials.ee.test.ts @@ -1,43 +1,48 @@ -import express from 'express'; -import { UserSettings } from 'n8n-core'; +import type { SuperAgentTest } from 'supertest'; import { In } from 'typeorm'; +import { UserSettings } from 'n8n-core'; +import type { IUser } from 'n8n-workflow'; import * as Db from '@/Db'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import type { CredentialWithSharings } from '@/credentials/credentials.types'; import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; import { randomCredentialPayload } from './shared/random'; import * as testDb from './shared/testDb'; import type { AuthAgent, SaveCredentialFunction } from './shared/types'; import * as utils from './shared/utils'; -import type { IUser } from 'n8n-workflow'; -let app: express.Application; -let globalOwnerRole: Role; let globalMemberRole: Role; -let credentialOwnerRole: Role; -let saveCredential: SaveCredentialFunction; +let owner: User; +let member: User; +let authOwnerAgent: SuperAgentTest; let authAgent: AuthAgent; +let saveCredential: SaveCredentialFunction; let sharingSpy: jest.SpyInstance; beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['credentials'] }); + const app = await utils.initTestServer({ endpointGroups: ['credentials'] }); utils.initConfigFile(); - globalOwnerRole = await testDb.getGlobalOwnerRole(); + const globalOwnerRole = await testDb.getGlobalOwnerRole(); globalMemberRole = await testDb.getGlobalMemberRole(); - credentialOwnerRole = await testDb.getCredentialOwnerRole(); + const credentialOwnerRole = await testDb.getCredentialOwnerRole(); + + owner = await testDb.createUser({ globalRole: globalOwnerRole }); + member = await testDb.createUser({ globalRole: globalMemberRole }); + + authAgent = utils.createAuthAgent(app); + authOwnerAgent = authAgent(owner); saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole); - authAgent = utils.createAuthAgent(app); - sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); }); beforeEach(async () => { - await testDb.truncate(['User', 'SharedCredentials', 'Credentials']); + await testDb.truncate(['SharedCredentials', 'Credentials']); }); afterAll(async () => { @@ -47,490 +52,452 @@ afterAll(async () => { // ---------------------------------------- // dynamic router switching // ---------------------------------------- +describe('router should switch based on flag', () => { + let savedCredentialId: string; -test('router should switch based on flag', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + beforeEach(async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + savedCredentialId = savedCredential.id; + }); - // free router - sharingSpy.mockReturnValueOnce(false); + test('when sharing is disabled', async () => { + sharingSpy.mockReturnValueOnce(false); - const freeShareResponse = authAgent(owner) - .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds: [member.id] }); + await authOwnerAgent + .put(`/credentials/${savedCredentialId}/share`) + .send({ shareWithIds: [member.id] }) + .expect(404); - const freeGetResponse = authAgent(owner).get(`/credentials/${savedCredential.id}`).send(); + await authOwnerAgent.get(`/credentials/${savedCredentialId}`).send().expect(200); + }); - const [{ statusCode: freeShareStatus }, { statusCode: freeGetStatus }] = await Promise.all([ - freeShareResponse, - freeGetResponse, - ]); + test('when sharing is enabled', async () => { + await authOwnerAgent + .put(`/credentials/${savedCredentialId}/share`) + .send({ shareWithIds: [member.id] }) + .expect(200); - expect(freeShareStatus).toBe(404); - expect(freeGetStatus).toBe(200); - - // EE router - - const eeShareResponse = authAgent(owner) - .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds: [member.id] }); - - const eeGetResponse = authAgent(owner).get(`/credentials/${savedCredential.id}`).send(); - - const [{ statusCode: eeShareStatus }, { statusCode: eeGetStatus }] = await Promise.all([ - eeShareResponse, - eeGetResponse, - ]); - - expect(eeShareStatus).toBe(200); - expect(eeGetStatus).toBe(200); + await authOwnerAgent.get(`/credentials/${savedCredentialId}`).send().expect(200); + }); }); // ---------------------------------------- // GET /credentials - fetch all credentials // ---------------------------------------- - -test('GET /credentials should return all creds for owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const [member1, member2, member3] = await testDb.createManyUsers(3, { - globalRole: globalMemberRole, - }); - - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - await saveCredential(randomCredentialPayload(), { user: member1 }); - - const sharedWith = [member1, member2, member3]; - await testDb.shareCredentialWithUsers(savedCredential, sharedWith); - - const response = await authAgent(owner).get('/credentials'); - - expect(response.statusCode).toBe(200); - expect(response.body.data).toHaveLength(2); // owner retrieved owner cred and member cred - - const [ownerCredential, memberCredential] = response.body.data as CredentialWithSharings[]; - - validateMainCredentialData(ownerCredential); - expect(ownerCredential.data).toBeUndefined(); - - validateMainCredentialData(memberCredential); - expect(memberCredential.data).toBeUndefined(); - - expect(ownerCredential.ownedBy).toMatchObject({ - id: owner.id, - email: owner.email, - firstName: owner.firstName, - lastName: owner.lastName, - }); - - expect(Array.isArray(ownerCredential.sharedWith)).toBe(true); - expect(ownerCredential.sharedWith).toHaveLength(3); - - // Fix order issue (MySQL might return items in any order) - const ownerCredentialsSharedWithOrdered = [...ownerCredential.sharedWith!].sort( - (a: IUser, b: IUser) => (a.email < b.email ? -1 : 1), - ); - const orderedSharedWith = [...sharedWith].sort((a, b) => (a.email < b.email ? -1 : 1)); - - ownerCredentialsSharedWithOrdered.forEach((sharee: IUser, idx: number) => { - expect(sharee).toMatchObject({ - id: orderedSharedWith[idx].id, - email: orderedSharedWith[idx].email, - firstName: orderedSharedWith[idx].firstName, - lastName: orderedSharedWith[idx].lastName, +describe('GET /credentials', () => { + test('should return all creds for owner', async () => { + const [member1, member2, member3] = await testDb.createManyUsers(3, { + globalRole: globalMemberRole, }); + + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + await saveCredential(randomCredentialPayload(), { user: member1 }); + + const sharedWith = [member1, member2, member3]; + await testDb.shareCredentialWithUsers(savedCredential, sharedWith); + + const response = await authOwnerAgent.get('/credentials'); + + expect(response.statusCode).toBe(200); + expect(response.body.data).toHaveLength(2); // owner retrieved owner cred and member cred + + const [ownerCredential, memberCredential] = response.body.data as CredentialWithSharings[]; + + validateMainCredentialData(ownerCredential); + expect(ownerCredential.data).toBeUndefined(); + + validateMainCredentialData(memberCredential); + expect(memberCredential.data).toBeUndefined(); + + expect(ownerCredential.ownedBy).toMatchObject({ + id: owner.id, + email: owner.email, + firstName: owner.firstName, + lastName: owner.lastName, + }); + + expect(Array.isArray(ownerCredential.sharedWith)).toBe(true); + expect(ownerCredential.sharedWith).toHaveLength(3); + + // Fix order issue (MySQL might return items in any order) + const ownerCredentialsSharedWithOrdered = [...ownerCredential.sharedWith!].sort( + (a: IUser, b: IUser) => (a.email < b.email ? -1 : 1), + ); + const orderedSharedWith = [...sharedWith].sort((a, b) => (a.email < b.email ? -1 : 1)); + + ownerCredentialsSharedWithOrdered.forEach((sharee: IUser, idx: number) => { + expect(sharee).toMatchObject({ + id: orderedSharedWith[idx].id, + email: orderedSharedWith[idx].email, + firstName: orderedSharedWith[idx].firstName, + lastName: orderedSharedWith[idx].lastName, + }); + }); + + expect(memberCredential.ownedBy).toMatchObject({ + id: member1.id, + email: member1.email, + firstName: member1.firstName, + lastName: member1.lastName, + }); + + expect(Array.isArray(memberCredential.sharedWith)).toBe(true); + expect(memberCredential.sharedWith).toHaveLength(0); }); - expect(memberCredential.ownedBy).toMatchObject({ - id: member1.id, - email: member1.email, - firstName: member1.firstName, - lastName: member1.lastName, - }); + test('should return only relevant creds for member', async () => { + const [member1, member2] = await testDb.createManyUsers(2, { + globalRole: globalMemberRole, + }); - expect(Array.isArray(memberCredential.sharedWith)).toBe(true); - expect(memberCredential.sharedWith).toHaveLength(0); -}); + await saveCredential(randomCredentialPayload(), { user: member2 }); + const savedMemberCredential = await saveCredential(randomCredentialPayload(), { + user: member1, + }); -test('GET /credentials should return only relevant creds for member', async () => { - const [member1, member2] = await testDb.createManyUsers(2, { - globalRole: globalMemberRole, - }); + await testDb.shareCredentialWithUsers(savedMemberCredential, [member2]); - await saveCredential(randomCredentialPayload(), { user: member2 }); - const savedMemberCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); + const response = await authAgent(member1).get('/credentials'); - await testDb.shareCredentialWithUsers(savedMemberCredential, [member2]); + expect(response.statusCode).toBe(200); + expect(response.body.data).toHaveLength(1); // member retrieved only member cred - const response = await authAgent(member1).get('/credentials'); + const [member1Credential] = response.body.data; - expect(response.statusCode).toBe(200); - expect(response.body.data).toHaveLength(1); // member retrieved only member cred + validateMainCredentialData(member1Credential); + expect(member1Credential.data).toBeUndefined(); - const [member1Credential] = response.body.data; + expect(member1Credential.ownedBy).toMatchObject({ + id: member1.id, + email: member1.email, + firstName: member1.firstName, + lastName: member1.lastName, + }); - validateMainCredentialData(member1Credential); - expect(member1Credential.data).toBeUndefined(); + expect(Array.isArray(member1Credential.sharedWith)).toBe(true); + expect(member1Credential.sharedWith).toHaveLength(1); - expect(member1Credential.ownedBy).toMatchObject({ - id: member1.id, - email: member1.email, - firstName: member1.firstName, - lastName: member1.lastName, - }); + const [sharee] = member1Credential.sharedWith; - expect(Array.isArray(member1Credential.sharedWith)).toBe(true); - expect(member1Credential.sharedWith).toHaveLength(1); - - const [sharee] = member1Credential.sharedWith; - - expect(sharee).toMatchObject({ - id: member2.id, - email: member2.email, - firstName: member2.firstName, - lastName: member2.lastName, + expect(sharee).toMatchObject({ + id: member2.id, + email: member2.email, + firstName: member2.firstName, + lastName: member2.lastName, + }); }); }); // ---------------------------------------- // GET /credentials/:id - fetch a certain credential // ---------------------------------------- +describe('GET /credentials/:id', () => { + test('should retrieve owned cred for owner', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); -test('GET /credentials/:id should retrieve owned cred for owner', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = authAgent(ownerShell); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); + const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); - const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); + expect(firstResponse.statusCode).toBe(200); - expect(firstResponse.statusCode).toBe(200); + const { data: firstCredential } = firstResponse.body; + validateMainCredentialData(firstCredential); + expect(firstCredential.data).toBeUndefined(); + expect(firstCredential.ownedBy).toMatchObject({ + id: owner.id, + email: owner.email, + firstName: owner.firstName, + lastName: owner.lastName, + }); + expect(firstCredential.sharedWith).toHaveLength(0); - const { data: firstCredential } = firstResponse.body; - validateMainCredentialData(firstCredential); - expect(firstCredential.data).toBeUndefined(); - expect(firstCredential.ownedBy).toMatchObject({ - id: ownerShell.id, - email: ownerShell.email, - firstName: ownerShell.firstName, - lastName: ownerShell.lastName, - }); - expect(firstCredential.sharedWith).toHaveLength(0); + const secondResponse = await authOwnerAgent + .get(`/credentials/${savedCredential.id}`) + .query({ includeData: true }); - const secondResponse = await authOwnerAgent - .get(`/credentials/${savedCredential.id}`) - .query({ includeData: true }); + expect(secondResponse.statusCode).toBe(200); - expect(secondResponse.statusCode).toBe(200); - - const { data: secondCredential } = secondResponse.body; - validateMainCredentialData(secondCredential); - expect(secondCredential.data).toBeDefined(); -}); - -test('GET /credentials/:id should retrieve non-owned cred for owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const authOwnerAgent = authAgent(owner); - const [member1, member2] = await testDb.createManyUsers(2, { - globalRole: globalMemberRole, + const { data: secondCredential } = secondResponse.body; + validateMainCredentialData(secondCredential); + expect(secondCredential.data).toBeDefined(); }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); - await testDb.shareCredentialWithUsers(savedCredential, [member2]); + test('should retrieve non-owned cred for owner', async () => { + const [member1, member2] = await testDb.createManyUsers(2, { + globalRole: globalMemberRole, + }); - const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); + await testDb.shareCredentialWithUsers(savedCredential, [member2]); - expect(response1.statusCode).toBe(200); + const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); - validateMainCredentialData(response1.body.data); - expect(response1.body.data.data).toBeUndefined(); - expect(response1.body.data.ownedBy).toMatchObject({ - id: member1.id, - email: member1.email, - firstName: member1.firstName, - lastName: member1.lastName, - }); - expect(response1.body.data.sharedWith).toHaveLength(1); - expect(response1.body.data.sharedWith[0]).toMatchObject({ - id: member2.id, - email: member2.email, - firstName: member2.firstName, - lastName: member2.lastName, + expect(response1.statusCode).toBe(200); + + validateMainCredentialData(response1.body.data); + expect(response1.body.data.data).toBeUndefined(); + expect(response1.body.data.ownedBy).toMatchObject({ + id: member1.id, + email: member1.email, + firstName: member1.firstName, + lastName: member1.lastName, + }); + expect(response1.body.data.sharedWith).toHaveLength(1); + expect(response1.body.data.sharedWith[0]).toMatchObject({ + id: member2.id, + email: member2.email, + firstName: member2.firstName, + lastName: member2.lastName, + }); + + const response2 = await authOwnerAgent + .get(`/credentials/${savedCredential.id}`) + .query({ includeData: true }); + + expect(response2.statusCode).toBe(200); + + validateMainCredentialData(response2.body.data); + expect(response2.body.data.data).toBeUndefined(); + expect(response2.body.data.sharedWith).toHaveLength(1); }); - const response2 = await authOwnerAgent - .get(`/credentials/${savedCredential.id}`) - .query({ includeData: true }); + test('should retrieve owned cred for member', async () => { + const [member1, member2, member3] = await testDb.createManyUsers(3, { + globalRole: globalMemberRole, + }); + const authMemberAgent = authAgent(member1); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); + await testDb.shareCredentialWithUsers(savedCredential, [member2, member3]); - expect(response2.statusCode).toBe(200); + const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`); - validateMainCredentialData(response2.body.data); - expect(response2.body.data.data).toBeUndefined(); - expect(response2.body.data.sharedWith).toHaveLength(1); -}); + expect(firstResponse.statusCode).toBe(200); -test('GET /credentials/:id should retrieve owned cred for member', async () => { - const [member1, member2, member3] = await testDb.createManyUsers(3, { - globalRole: globalMemberRole, - }); - const authMemberAgent = authAgent(member1); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); - await testDb.shareCredentialWithUsers(savedCredential, [member2, member3]); + const { data: firstCredential } = firstResponse.body; + validateMainCredentialData(firstCredential); + expect(firstCredential.data).toBeUndefined(); + expect(firstCredential.ownedBy).toMatchObject({ + id: member1.id, + email: member1.email, + firstName: member1.firstName, + lastName: member1.lastName, + }); + expect(firstCredential.sharedWith).toHaveLength(2); + firstCredential.sharedWith.forEach((sharee: IUser, idx: number) => { + expect([member2.id, member3.id]).toContain(sharee.id); + }); - const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`); + const secondResponse = await authMemberAgent + .get(`/credentials/${savedCredential.id}`) + .query({ includeData: true }); - expect(firstResponse.statusCode).toBe(200); + expect(secondResponse.statusCode).toBe(200); - const { data: firstCredential } = firstResponse.body; - validateMainCredentialData(firstCredential); - expect(firstCredential.data).toBeUndefined(); - expect(firstCredential.ownedBy).toMatchObject({ - id: member1.id, - email: member1.email, - firstName: member1.firstName, - lastName: member1.lastName, - }); - expect(firstCredential.sharedWith).toHaveLength(2); - firstCredential.sharedWith.forEach((sharee: IUser, idx: number) => { - expect([member2.id, member3.id]).toContain(sharee.id); + const { data: secondCredential } = secondResponse.body; + validateMainCredentialData(secondCredential); + expect(secondCredential.data).toBeDefined(); + expect(firstCredential.sharedWith).toHaveLength(2); }); - const secondResponse = await authMemberAgent - .get(`/credentials/${savedCredential.id}`) - .query({ includeData: true }); + test('should not retrieve non-owned cred for member', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - expect(secondResponse.statusCode).toBe(200); + const response = await authAgent(member).get(`/credentials/${savedCredential.id}`); - const { data: secondCredential } = secondResponse.body; - validateMainCredentialData(secondCredential); - expect(secondCredential.data).toBeDefined(); - expect(firstCredential.sharedWith).toHaveLength(2); -}); - -test('GET /credentials/:id should not retrieve non-owned cred for member', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); - - const response = await authAgent(member).get(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(403); - expect(response.body.data).toBeUndefined(); // owner's cred not returned -}); - -test('GET /credentials/:id should fail with missing encryption key', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); - - const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); - mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); - - const response = await authAgent(ownerShell) - .get(`/credentials/${savedCredential.id}`) - .query({ includeData: true }); - - expect(response.statusCode).toBe(500); - - mock.mockRestore(); -}); - -test('GET /credentials/:id should return 404 if cred not found', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell).get('/credentials/789'); - expect(response.statusCode).toBe(404); - - const responseAbc = await authAgent(ownerShell).get('/credentials/abc'); - expect(responseAbc.statusCode).toBe(404); - - // because EE router has precedence, check if forwards this route - const responseNew = await authAgent(ownerShell).get('/credentials/new'); - expect(responseNew.statusCode).toBe(200); -}); - -// ---------------------------------------- -// indempotent share/unshare -// ---------------------------------------- - -test('PUT /credentials/:id/share should share the credential with the provided userIds and unshare it for missing ones', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - - const [member1, member2, member3, member4, member5] = await testDb.createManyUsers(5, { - globalRole: globalMemberRole, - }); - const shareWithIds = [member1.id, member2.id, member3.id]; - - await testDb.shareCredentialWithUsers(savedCredential, [member4, member5]); - - const response = await authAgent(owner) - .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds }); - - expect(response.statusCode).toBe(200); - expect(response.body.data).toBeUndefined(); - - const sharedCredentials = await Db.collections.SharedCredentials.find({ - relations: ['role'], - where: { credentialsId: savedCredential.id }, + expect(response.statusCode).toBe(403); + expect(response.body.data).toBeUndefined(); // owner's cred not returned }); - // check that sharings have been removed/added correctly - expect(sharedCredentials.length).toBe(shareWithIds.length + 1); // +1 for the owner + test('should fail with missing encryption key', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - sharedCredentials.forEach((sharedCredential) => { - if (sharedCredential.userId === owner.id) { - expect(sharedCredential.role.name).toBe('owner'); - expect(sharedCredential.role.scope).toBe('credential'); - return; - } - expect(shareWithIds).toContain(sharedCredential.userId); - expect(sharedCredential.role.name).toBe('user'); - expect(sharedCredential.role.scope).toBe('credential'); + const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); + mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); + + const response = await authOwnerAgent + .get(`/credentials/${savedCredential.id}`) + .query({ includeData: true }); + + expect(response.statusCode).toBe(500); + + mock.mockRestore(); + }); + + test('should return 404 if cred not found', async () => { + const response = await authOwnerAgent.get('/credentials/789'); + expect(response.statusCode).toBe(404); + + const responseAbc = await authOwnerAgent.get('/credentials/abc'); + expect(responseAbc.statusCode).toBe(404); + + // because EE router has precedence, check if forwards this route + const responseNew = await authOwnerAgent.get('/credentials/new'); + expect(responseNew.statusCode).toBe(200); }); }); // ---------------------------------------- -// share +// idempotent share/unshare // ---------------------------------------- +describe('PUT /credentials/:id/share', () => { + test('should share the credential with the provided userIds and unshare it for missing ones', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); -test('PUT /credentials/:id/share should share the credential with the provided userIds', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const [member1, member2, member3] = await testDb.createManyUsers(3, { - globalRole: globalMemberRole, - }); - const memberIds = [member1.id, member2.id, member3.id]; - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const [member1, member2, member3, member4, member5] = await testDb.createManyUsers(5, { + globalRole: globalMemberRole, + }); + const shareWithIds = [member1.id, member2.id, member3.id]; - const response = await authAgent(owner) - .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds: memberIds }); + await testDb.shareCredentialWithUsers(savedCredential, [member4, member5]); - expect(response.statusCode).toBe(200); - expect(response.body.data).toBeUndefined(); - - // check that sharings got correctly set in DB - const sharedCredentials = await Db.collections.SharedCredentials.find({ - relations: ['role'], - where: { credentialsId: savedCredential.id, userId: In([...memberIds]) }, - }); - - expect(sharedCredentials.length).toBe(memberIds.length); - - sharedCredentials.forEach((sharedCredential) => { - expect(sharedCredential.role.name).toBe('user'); - expect(sharedCredential.role.scope).toBe('credential'); - }); - - // check that owner still exists - const ownerSharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ - relations: ['role'], - where: { credentialsId: savedCredential.id, userId: owner.id }, - }); - - expect(ownerSharedCredential.role.name).toBe('owner'); - expect(ownerSharedCredential.role.scope).toBe('credential'); -}); - -test('PUT /credentials/:id/share should respond 403 for non-existing credentials', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - - const response = await authAgent(owner) - .put(`/credentials/1234567/share`) - .send({ shareWithIds: [member.id] }); - - expect(response.statusCode).toBe(403); -}); - -test('PUT /credentials/:id/share should respond 403 for non-owned credentials', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); - - const response = await authAgent(owner) - .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds: [member.id] }); - - expect(response.statusCode).toBe(403); -}); - -test('PUT /credentials/:id/share should ignore pending sharee', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const memberShell = await testDb.createUserShell(globalMemberRole); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - - const response = await authAgent(owner) - .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds: [memberShell.id] }); - - expect(response.statusCode).toBe(200); - - const sharedCredentials = await Db.collections.SharedCredentials.find({ - where: { credentialsId: savedCredential.id }, - }); - - expect(sharedCredentials).toHaveLength(1); - expect(sharedCredentials[0].userId).toBe(owner.id); -}); - -test('PUT /credentials/:id/share should ignore non-existing sharee', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - - const response = await authAgent(owner) - .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds: ['bce38a11-5e45-4d1c-a9ee-36e4a20ab0fc'] }); - - expect(response.statusCode).toBe(200); - - const sharedCredentials = await Db.collections.SharedCredentials.find({ - where: { credentialsId: savedCredential.id }, - }); - - expect(sharedCredentials).toHaveLength(1); - expect(sharedCredentials[0].userId).toBe(owner.id); -}); - -test('PUT /credentials/:id/share should respond 400 if invalid payload is provided', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - - const responses = await Promise.all([ - authAgent(owner).put(`/credentials/${savedCredential.id}/share`).send(), - authAgent(owner) + const response = await authOwnerAgent .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds: [1] }), - ]); + .send({ shareWithIds }); - responses.forEach((response) => expect(response.statusCode).toBe(400)); -}); + expect(response.statusCode).toBe(200); + expect(response.body.data).toBeUndefined(); -// ---------------------------------------- -// unshare -// ---------------------------------------- + const sharedCredentials = await Db.collections.SharedCredentials.find({ + relations: ['role'], + where: { credentialsId: savedCredential.id }, + }); -test('PUT /credentials/:id/share should unshare the credential', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + // check that sharings have been removed/added correctly + expect(sharedCredentials.length).toBe(shareWithIds.length + 1); // +1 for the owner - const [member1, member2] = await testDb.createManyUsers(2, { - globalRole: globalMemberRole, + sharedCredentials.forEach((sharedCredential) => { + if (sharedCredential.userId === owner.id) { + expect(sharedCredential.role.name).toBe('owner'); + expect(sharedCredential.role.scope).toBe('credential'); + return; + } + expect(shareWithIds).toContain(sharedCredential.userId); + expect(sharedCredential.role.name).toBe('user'); + expect(sharedCredential.role.scope).toBe('credential'); + }); }); - await testDb.shareCredentialWithUsers(savedCredential, [member1, member2]); + test('should share the credential with the provided userIds', async () => { + const [member1, member2, member3] = await testDb.createManyUsers(3, { + globalRole: globalMemberRole, + }); + const memberIds = [member1.id, member2.id, member3.id]; + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - const response = await authAgent(owner) - .put(`/credentials/${savedCredential.id}/share`) - .send({ shareWithIds: [] }); + const response = await authOwnerAgent + .put(`/credentials/${savedCredential.id}/share`) + .send({ shareWithIds: memberIds }); - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200); + expect(response.body.data).toBeUndefined(); - const sharedCredentials = await Db.collections.SharedCredentials.find({ - where: { credentialsId: savedCredential.id }, + // check that sharings got correctly set in DB + const sharedCredentials = await Db.collections.SharedCredentials.find({ + relations: ['role'], + where: { credentialsId: savedCredential.id, userId: In([...memberIds]) }, + }); + + expect(sharedCredentials.length).toBe(memberIds.length); + + sharedCredentials.forEach((sharedCredential) => { + expect(sharedCredential.role.name).toBe('user'); + expect(sharedCredential.role.scope).toBe('credential'); + }); + + // check that owner still exists + const ownerSharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ + relations: ['role'], + where: { credentialsId: savedCredential.id, userId: owner.id }, + }); + + expect(ownerSharedCredential.role.name).toBe('owner'); + expect(ownerSharedCredential.role.scope).toBe('credential'); }); - expect(sharedCredentials).toHaveLength(1); - expect(sharedCredentials[0].userId).toBe(owner.id); + test('should respond 403 for non-existing credentials', async () => { + const response = await authOwnerAgent + .put(`/credentials/1234567/share`) + .send({ shareWithIds: [member.id] }); + + expect(response.statusCode).toBe(403); + }); + + test('should respond 403 for non-owned credentials', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + + const response = await authOwnerAgent + .put(`/credentials/${savedCredential.id}/share`) + .send({ shareWithIds: [member.id] }); + + expect(response.statusCode).toBe(403); + }); + + test('should ignore pending sharee', async () => { + const memberShell = await testDb.createUserShell(globalMemberRole); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + + const response = await authOwnerAgent + .put(`/credentials/${savedCredential.id}/share`) + .send({ shareWithIds: [memberShell.id] }); + + expect(response.statusCode).toBe(200); + + const sharedCredentials = await Db.collections.SharedCredentials.find({ + where: { credentialsId: savedCredential.id }, + }); + + expect(sharedCredentials).toHaveLength(1); + expect(sharedCredentials[0].userId).toBe(owner.id); + }); + + test('should ignore non-existing sharee', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + + const response = await authOwnerAgent + .put(`/credentials/${savedCredential.id}/share`) + .send({ shareWithIds: ['bce38a11-5e45-4d1c-a9ee-36e4a20ab0fc'] }); + + expect(response.statusCode).toBe(200); + + const sharedCredentials = await Db.collections.SharedCredentials.find({ + where: { credentialsId: savedCredential.id }, + }); + + expect(sharedCredentials).toHaveLength(1); + expect(sharedCredentials[0].userId).toBe(owner.id); + }); + + test('should respond 400 if invalid payload is provided', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + + const responses = await Promise.all([ + authOwnerAgent.put(`/credentials/${savedCredential.id}/share`).send(), + authOwnerAgent.put(`/credentials/${savedCredential.id}/share`).send({ shareWithIds: [1] }), + ]); + + responses.forEach((response) => expect(response.statusCode).toBe(400)); + }); + test('should unshare the credential', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + + const [member1, member2] = await testDb.createManyUsers(2, { + globalRole: globalMemberRole, + }); + + await testDb.shareCredentialWithUsers(savedCredential, [member1, member2]); + + const response = await authOwnerAgent + .put(`/credentials/${savedCredential.id}/share`) + .send({ shareWithIds: [] }); + + expect(response.statusCode).toBe(200); + + const sharedCredentials = await Db.collections.SharedCredentials.find({ + where: { credentialsId: savedCredential.id }, + }); + + expect(sharedCredentials).toHaveLength(1); + expect(sharedCredentials[0].userId).toBe(owner.id); + }); }); function validateMainCredentialData(credential: CredentialWithSharings) { diff --git a/packages/cli/test/integration/credentials.test.ts b/packages/cli/test/integration/credentials.test.ts index 1de7561721..3a2f5f8415 100644 --- a/packages/cli/test/integration/credentials.test.ts +++ b/packages/cli/test/integration/credentials.test.ts @@ -1,26 +1,31 @@ -import express from 'express'; +import type { Application } from 'express'; +import type { SuperAgentTest } from 'supertest'; import { UserSettings } from 'n8n-core'; import * as Db from '@/Db'; +import config from '@/config'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; +import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; import { randomCredentialPayload, randomName, randomString } from './shared/random'; import * as testDb from './shared/testDb'; import type { SaveCredentialFunction } from './shared/types'; import * as utils from './shared/utils'; - -import config from '@/config'; -import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { AuthAgent } from './shared/types'; // mock that credentialsSharing is not enabled const mockIsCredentialsSharingEnabled = jest.spyOn(UserManagementHelpers, 'isSharingEnabled'); mockIsCredentialsSharingEnabled.mockReturnValue(false); -let app: express.Application; +let app: Application; let globalOwnerRole: Role; let globalMemberRole: Role; +let owner: User; +let member: User; +let authOwnerAgent: SuperAgentTest; +let authMemberAgent: SuperAgentTest; let saveCredential: SaveCredentialFunction; let authAgent: AuthAgent; @@ -33,13 +38,18 @@ beforeAll(async () => { globalMemberRole = await testDb.getGlobalMemberRole(); const credentialOwnerRole = await testDb.getCredentialOwnerRole(); + owner = await testDb.createUser({ globalRole: globalOwnerRole }); + member = await testDb.createUser({ globalRole: globalMemberRole }); + saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole); authAgent = utils.createAuthAgent(app); + authOwnerAgent = authAgent(owner); + authMemberAgent = authAgent(member); }); beforeEach(async () => { - await testDb.truncate(['User', 'SharedCredentials', 'Credentials']); + await testDb.truncate(['SharedCredentials', 'Credentials']); }); afterAll(async () => { @@ -49,526 +59,490 @@ afterAll(async () => { // ---------------------------------------- // GET /credentials - fetch all credentials // ---------------------------------------- +describe('GET /credentials', () => { + test('should return all creds for owner', async () => { + const [{ id: savedOwnerCredentialId }, { id: savedMemberCredentialId }] = await Promise.all([ + saveCredential(randomCredentialPayload(), { user: owner }), + saveCredential(randomCredentialPayload(), { user: member }), + ]); -test('GET /credentials should return all creds for owner', async () => { - const [owner, member] = await Promise.all([ - testDb.createUser({ globalRole: globalOwnerRole }), - testDb.createUser({ globalRole: globalMemberRole }), - ]); + const response = await authOwnerAgent.get('/credentials'); - const [{ id: savedOwnerCredentialId }, { id: savedMemberCredentialId }] = await Promise.all([ - saveCredential(randomCredentialPayload(), { user: owner }), - saveCredential(randomCredentialPayload(), { user: member }), - ]); + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(2); // owner retrieved owner cred and member cred - const response = await authAgent(owner).get('/credentials'); + const savedCredentialsIds = [savedOwnerCredentialId, savedMemberCredentialId]; + response.body.data.forEach((credential: CredentialsEntity) => { + validateMainCredentialData(credential); + expect(credential.data).toBeUndefined(); + expect(savedCredentialsIds).toContain(credential.id); + }); + }); - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(2); // owner retrieved owner cred and member cred + test('should return only own creds for member', async () => { + const [member1, member2] = await testDb.createManyUsers(2, { + globalRole: globalMemberRole, + }); - const savedCredentialsIds = [savedOwnerCredentialId, savedMemberCredentialId]; - response.body.data.forEach((credential: CredentialsEntity) => { - validateMainCredentialData(credential); - expect(credential.data).toBeUndefined(); - expect(savedCredentialsIds).toContain(credential.id); + const [savedCredential1] = await Promise.all([ + saveCredential(randomCredentialPayload(), { user: member1 }), + saveCredential(randomCredentialPayload(), { user: member2 }), + ]); + + const response = await authAgent(member1).get('/credentials'); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(1); // member retrieved only own cred + + const [member1Credential] = response.body.data; + + validateMainCredentialData(member1Credential); + expect(member1Credential.data).toBeUndefined(); + expect(member1Credential.id).toBe(savedCredential1.id); }); }); -test('GET /credentials should return only own creds for member', async () => { - const [member1, member2] = await testDb.createManyUsers(2, { - globalRole: globalMemberRole, +describe('POST /credentials', () => { + test('should create cred', async () => { + const payload = randomCredentialPayload(); + + const response = await authOwnerAgent.post('/credentials').send(payload); + + expect(response.statusCode).toBe(200); + + const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + + expect(name).toBe(payload.name); + expect(type).toBe(payload.type); + if (!payload.nodesAccess) { + fail('Payload did not contain a nodesAccess array'); + } + expect(nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType); + expect(encryptedData).not.toBe(payload.data); + + const credential = await Db.collections.Credentials.findOneByOrFail({ id }); + + expect(credential.name).toBe(payload.name); + expect(credential.type).toBe(payload.type); + expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess![0].nodeType); + expect(credential.data).not.toBe(payload.data); + + const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ + relations: ['user', 'credentials'], + where: { credentialsId: credential.id }, + }); + + expect(sharedCredential.user.id).toBe(owner.id); + expect(sharedCredential.credentials.name).toBe(payload.name); }); - const [savedCredential1] = await Promise.all([ - saveCredential(randomCredentialPayload(), { user: member1 }), - saveCredential(randomCredentialPayload(), { user: member2 }), - ]); - - const response = await authAgent(member1).get('/credentials'); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(1); // member retrieved only own cred - - const [member1Credential] = response.body.data; - - validateMainCredentialData(member1Credential); - expect(member1Credential.data).toBeUndefined(); - expect(member1Credential.id).toBe(savedCredential1.id); -}); - -test('POST /credentials should create cred', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const payload = randomCredentialPayload(); - - const response = await authAgent(ownerShell).post('/credentials').send(payload); - - expect(response.statusCode).toBe(200); - - const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; - - expect(name).toBe(payload.name); - expect(type).toBe(payload.type); - if (!payload.nodesAccess) { - fail('Payload did not contain a nodesAccess array'); - } - expect(nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType); - expect(encryptedData).not.toBe(payload.data); - - const credential = await Db.collections.Credentials.findOneByOrFail({ id }); - - expect(credential.name).toBe(payload.name); - expect(credential.type).toBe(payload.type); - expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess![0].nodeType); - expect(credential.data).not.toBe(payload.data); - - const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ - relations: ['user', 'credentials'], - where: { credentialsId: credential.id }, + test('should fail with invalid inputs', async () => { + await Promise.all( + INVALID_PAYLOADS.map(async (invalidPayload) => { + const response = await authOwnerAgent.post('/credentials').send(invalidPayload); + expect(response.statusCode).toBe(400); + }), + ); }); - expect(sharedCredential.user.id).toBe(ownerShell.id); - expect(sharedCredential.credentials.name).toBe(payload.name); -}); + test('should fail with missing encryption key', async () => { + const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); + mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); -test('POST /credentials should fail with invalid inputs', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = authAgent(ownerShell); + const response = await authOwnerAgent.post('/credentials').send(randomCredentialPayload()); - await Promise.all( - INVALID_PAYLOADS.map(async (invalidPayload) => { - const response = await authOwnerAgent.post('/credentials').send(invalidPayload); - expect(response.statusCode).toBe(400); - }), - ); -}); + expect(response.statusCode).toBe(500); -test('POST /credentials should fail with missing encryption key', async () => { - const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); - mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); - - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell).post('/credentials').send(randomCredentialPayload()); - - expect(response.statusCode).toBe(500); - - mock.mockRestore(); -}); - -test('POST /credentials should ignore ID in payload', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = authAgent(ownerShell); - - const firstResponse = await authOwnerAgent - .post('/credentials') - .send({ id: '8', ...randomCredentialPayload() }); - - expect(firstResponse.body.data.id).not.toBe('8'); - - const secondResponse = await authOwnerAgent - .post('/credentials') - .send({ id: 8, ...randomCredentialPayload() }); - - expect(secondResponse.body.data.id).not.toBe(8); -}); - -test('DELETE /credentials/:id should delete owned cred for owner', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); - - const response = await authAgent(ownerShell).delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ data: true }); - - const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(deletedCredential).toBeNull(); // deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - - expect(deletedSharedCredential).toBeNull(); // deleted -}); - -test('DELETE /credentials/:id should delete non-owned cred for owner', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); - - const response = await authAgent(ownerShell).delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ data: true }); - - const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(deletedCredential).toBeNull(); // deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - - expect(deletedSharedCredential).toBeNull(); // deleted -}); - -test('DELETE /credentials/:id should delete owned cred for member', async () => { - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); - - const response = await authAgent(member).delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(200); - expect(response.body).toEqual({ data: true }); - - const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(deletedCredential).toBeNull(); // deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - - expect(deletedSharedCredential).toBeNull(); // deleted -}); - -test('DELETE /credentials/:id should not delete non-owned cred for member', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); - - const response = await authAgent(member).delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(404); - - const shellCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(shellCredential).toBeDefined(); // not deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - - expect(deletedSharedCredential).toBeDefined(); // not deleted -}); - -test('DELETE /credentials/:id should fail if cred not found', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell).delete('/credentials/123'); - - expect(response.statusCode).toBe(404); -}); - -test('PATCH /credentials/:id should update owned cred for owner', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); - const patchPayload = randomCredentialPayload(); - - const response = await authAgent(ownerShell) - .patch(`/credentials/${savedCredential.id}`) - .send(patchPayload); - - expect(response.statusCode).toBe(200); - - const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; - - expect(name).toBe(patchPayload.name); - expect(type).toBe(patchPayload.type); - if (!patchPayload.nodesAccess) { - fail('Payload did not contain a nodesAccess array'); - } - expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); - - expect(encryptedData).not.toBe(patchPayload.data); - - const credential = await Db.collections.Credentials.findOneByOrFail({ id }); - - expect(credential.name).toBe(patchPayload.name); - expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); - expect(credential.data).not.toBe(patchPayload.data); - - const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ - relations: ['credentials'], - where: { credentialsId: credential.id }, + mock.mockRestore(); }); - expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated + test('should ignore ID in payload', async () => { + const firstResponse = await authOwnerAgent + .post('/credentials') + .send({ id: '8', ...randomCredentialPayload() }); + + expect(firstResponse.body.data.id).not.toBe('8'); + + const secondResponse = await authOwnerAgent + .post('/credentials') + .send({ id: 8, ...randomCredentialPayload() }); + + expect(secondResponse.body.data.id).not.toBe(8); + }); }); -test('PATCH /credentials/:id should update non-owned cred for owner', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); - const patchPayload = randomCredentialPayload(); +describe('DELETE /credentials/:id', () => { + test('should delete owned cred for owner', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - const response = await authAgent(ownerShell) - .patch(`/credentials/${savedCredential.id}`) - .send(patchPayload); + const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ data: true }); - const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + const deletedCredential = await Db.collections.Credentials.findOneBy({ + id: savedCredential.id, + }); - expect(name).toBe(patchPayload.name); - expect(type).toBe(patchPayload.type); + expect(deletedCredential).toBeNull(); // deleted - if (!patchPayload.nodesAccess) { - fail('Payload did not contain a nodesAccess array'); - } - expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); + const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - expect(encryptedData).not.toBe(patchPayload.data); - - const credential = await Db.collections.Credentials.findOneByOrFail({ id }); - - expect(credential.name).toBe(patchPayload.name); - expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); - expect(credential.data).not.toBe(patchPayload.data); - - const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ - relations: ['credentials'], - where: { credentialsId: credential.id }, + expect(deletedSharedCredential).toBeNull(); // deleted }); - expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated -}); + test('should delete non-owned cred for owner', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); -test('PATCH /credentials/:id should update owned cred for member', async () => { - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); - const patchPayload = randomCredentialPayload(); + const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); - const response = await authAgent(member) - .patch(`/credentials/${savedCredential.id}`) - .send(patchPayload); + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ data: true }); - expect(response.statusCode).toBe(200); + const deletedCredential = await Db.collections.Credentials.findOneBy({ + id: savedCredential.id, + }); - const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + expect(deletedCredential).toBeNull(); // deleted - expect(name).toBe(patchPayload.name); - expect(type).toBe(patchPayload.type); + const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - if (!patchPayload.nodesAccess) { - fail('Payload did not contain a nodesAccess array'); - } - expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); - - expect(encryptedData).not.toBe(patchPayload.data); - - const credential = await Db.collections.Credentials.findOneByOrFail({ id }); - - expect(credential.name).toBe(patchPayload.name); - expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); - expect(credential.data).not.toBe(patchPayload.data); - - const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ - relations: ['credentials'], - where: { credentialsId: credential.id }, + expect(deletedSharedCredential).toBeNull(); // deleted }); - expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated -}); + test('should delete owned cred for member', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); -test('PATCH /credentials/:id should not update non-owned cred for member', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); - const patchPayload = randomCredentialPayload(); + const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); - const response = await authAgent(member) - .patch(`/credentials/${savedCredential.id}`) - .send(patchPayload); + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ data: true }); - expect(response.statusCode).toBe(404); + const deletedCredential = await Db.collections.Credentials.findOneBy({ + id: savedCredential.id, + }); - const shellCredential = await Db.collections.Credentials.findOneByOrFail({ - id: savedCredential.id, + expect(deletedCredential).toBeNull(); // deleted + + const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); + + expect(deletedSharedCredential).toBeNull(); // deleted }); - expect(shellCredential.name).not.toBe(patchPayload.name); // not updated + test('should not delete non-owned cred for member', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + + const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); + + expect(response.statusCode).toBe(404); + + const shellCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); + + expect(shellCredential).toBeDefined(); // not deleted + + const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); + + expect(deletedSharedCredential).toBeDefined(); // not deleted + }); + + test('should fail if cred not found', async () => { + const response = await authOwnerAgent.delete('/credentials/123'); + + expect(response.statusCode).toBe(404); + }); }); -test('PATCH /credentials/:id should fail with invalid inputs', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = authAgent(ownerShell); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); +describe('PATCH /credentials/:id', () => { + test('should update owned cred for owner', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const patchPayload = randomCredentialPayload(); - await Promise.all( - INVALID_PAYLOADS.map(async (invalidPayload) => { - const response = await authOwnerAgent - .patch(`/credentials/${savedCredential.id}`) - .send(invalidPayload); + const response = await authOwnerAgent + .patch(`/credentials/${savedCredential.id}`) + .send(patchPayload); - if (response.statusCode === 500) { - console.log(response.statusCode, response.body); + expect(response.statusCode).toBe(200); + + const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + + expect(name).toBe(patchPayload.name); + expect(type).toBe(patchPayload.type); + if (!patchPayload.nodesAccess) { + fail('Payload did not contain a nodesAccess array'); + } + expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); + + expect(encryptedData).not.toBe(patchPayload.data); + + const credential = await Db.collections.Credentials.findOneByOrFail({ id }); + + expect(credential.name).toBe(patchPayload.name); + expect(credential.type).toBe(patchPayload.type); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.data).not.toBe(patchPayload.data); + + const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ + relations: ['credentials'], + where: { credentialsId: credential.id }, + }); + + expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated + }); + + test('should update non-owned cred for owner', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const patchPayload = randomCredentialPayload(); + + const response = await authOwnerAgent + .patch(`/credentials/${savedCredential.id}`) + .send(patchPayload); + + expect(response.statusCode).toBe(200); + + const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + + expect(name).toBe(patchPayload.name); + expect(type).toBe(patchPayload.type); + + if (!patchPayload.nodesAccess) { + fail('Payload did not contain a nodesAccess array'); + } + expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); + + expect(encryptedData).not.toBe(patchPayload.data); + + const credential = await Db.collections.Credentials.findOneByOrFail({ id }); + + expect(credential.name).toBe(patchPayload.name); + expect(credential.type).toBe(patchPayload.type); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.data).not.toBe(patchPayload.data); + + const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ + relations: ['credentials'], + where: { credentialsId: credential.id }, + }); + + expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated + }); + + test('should update owned cred for member', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const patchPayload = randomCredentialPayload(); + + const response = await authMemberAgent + .patch(`/credentials/${savedCredential.id}`) + .send(patchPayload); + + expect(response.statusCode).toBe(200); + + const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; + + expect(name).toBe(patchPayload.name); + expect(type).toBe(patchPayload.type); + + if (!patchPayload.nodesAccess) { + fail('Payload did not contain a nodesAccess array'); + } + expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); + + expect(encryptedData).not.toBe(patchPayload.data); + + const credential = await Db.collections.Credentials.findOneByOrFail({ id }); + + expect(credential.name).toBe(patchPayload.name); + expect(credential.type).toBe(patchPayload.type); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.data).not.toBe(patchPayload.data); + + const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ + relations: ['credentials'], + where: { credentialsId: credential.id }, + }); + + expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated + }); + + test('should not update non-owned cred for member', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + const patchPayload = randomCredentialPayload(); + + const response = await authMemberAgent + .patch(`/credentials/${savedCredential.id}`) + .send(patchPayload); + + expect(response.statusCode).toBe(404); + + const shellCredential = await Db.collections.Credentials.findOneByOrFail({ + id: savedCredential.id, + }); + + expect(shellCredential.name).not.toBe(patchPayload.name); // not updated + }); + + test('should fail with invalid inputs', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); + + await Promise.all( + INVALID_PAYLOADS.map(async (invalidPayload) => { + const response = await authOwnerAgent + .patch(`/credentials/${savedCredential.id}`) + .send(invalidPayload); + + if (response.statusCode === 500) { + console.log(response.statusCode, response.body); + } + expect(response.statusCode).toBe(400); + }), + ); + }); + + test('should fail if cred not found', async () => { + const response = await authOwnerAgent.patch('/credentials/123').send(randomCredentialPayload()); + + expect(response.statusCode).toBe(404); + }); + + test('should fail with missing encryption key', async () => { + const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); + mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); + + const response = await authOwnerAgent.post('/credentials').send(randomCredentialPayload()); + + expect(response.statusCode).toBe(500); + + mock.mockRestore(); + }); +}); + +describe('GET /credentials/new', () => { + test('should return default name for new credential or its increment', async () => { + const name = config.getEnv('credentials.defaultName'); + let tempName = name; + + for (let i = 0; i < 4; i++) { + const response = await authOwnerAgent.get(`/credentials/new?name=${name}`); + + expect(response.statusCode).toBe(200); + if (i === 0) { + expect(response.body.data.name).toBe(name); + } else { + tempName = name + ' ' + (i + 1); + expect(response.body.data.name).toBe(tempName); } - expect(response.statusCode).toBe(400); - }), - ); -}); - -test('PATCH /credentials/:id should fail if cred not found', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell) - .patch('/credentials/123') - .send(randomCredentialPayload()); - - expect(response.statusCode).toBe(404); -}); - -test('PATCH /credentials/:id should fail with missing encryption key', async () => { - const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); - mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); - - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell).post('/credentials').send(randomCredentialPayload()); - - expect(response.statusCode).toBe(500); - - mock.mockRestore(); -}); - -test('GET /credentials/new should return default name for new credential or its increment', async () => { - const ownerShell = await testDb.createUser({ globalRole: globalOwnerRole }); - const authOwnerAgent = authAgent(ownerShell); - const name = config.getEnv('credentials.defaultName'); - let tempName = name; - - for (let i = 0; i < 4; i++) { - const response = await authOwnerAgent.get(`/credentials/new?name=${name}`); - - expect(response.statusCode).toBe(200); - if (i === 0) { - expect(response.body.data.name).toBe(name); - } else { - tempName = name + ' ' + (i + 1); - expect(response.body.data.name).toBe(tempName); + await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: owner }); } - await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: ownerShell }); - } -}); + }); -test('GET /credentials/new should return name from query for new credential or its increment', async () => { - const ownerShell = await testDb.createUser({ globalRole: globalOwnerRole }); - const authOwnerAgent = authAgent(ownerShell); - const name = 'special credential name'; - let tempName = name; + test('should return name from query for new credential or its increment', async () => { + const name = 'special credential name'; + let tempName = name; - for (let i = 0; i < 4; i++) { - const response = await authOwnerAgent.get(`/credentials/new?name=${name}`); + for (let i = 0; i < 4; i++) { + const response = await authOwnerAgent.get(`/credentials/new?name=${name}`); - expect(response.statusCode).toBe(200); - if (i === 0) { - expect(response.body.data.name).toBe(name); - } else { - tempName = name + ' ' + (i + 1); - expect(response.body.data.name).toBe(tempName); + expect(response.statusCode).toBe(200); + if (i === 0) { + expect(response.body.data.name).toBe(name); + } else { + tempName = name + ' ' + (i + 1); + expect(response.body.data.name).toBe(tempName); + } + await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: owner }); } - await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: ownerShell }); - } + }); }); -test('GET /credentials/:id should retrieve owned cred for owner', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = authAgent(ownerShell); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); +describe('GET /credentials/:id', () => { + test('should retrieve owned cred for owner', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); - const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); + const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); - expect(firstResponse.statusCode).toBe(200); + expect(firstResponse.statusCode).toBe(200); - validateMainCredentialData(firstResponse.body.data); - expect(firstResponse.body.data.data).toBeUndefined(); + validateMainCredentialData(firstResponse.body.data); + expect(firstResponse.body.data.data).toBeUndefined(); - const secondResponse = await authOwnerAgent - .get(`/credentials/${savedCredential.id}`) - .query({ includeData: true }); + const secondResponse = await authOwnerAgent + .get(`/credentials/${savedCredential.id}`) + .query({ includeData: true }); - validateMainCredentialData(secondResponse.body.data); - expect(secondResponse.body.data.data).toBeDefined(); -}); + validateMainCredentialData(secondResponse.body.data); + expect(secondResponse.body.data.data).toBeDefined(); + }); -test('GET /credentials/:id should retrieve owned cred for member', async () => { - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const authMemberAgent = authAgent(member); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + test('should retrieve owned cred for member', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); - const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`); + const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`); - expect(firstResponse.statusCode).toBe(200); + expect(firstResponse.statusCode).toBe(200); - validateMainCredentialData(firstResponse.body.data); - expect(firstResponse.body.data.data).toBeUndefined(); + validateMainCredentialData(firstResponse.body.data); + expect(firstResponse.body.data.data).toBeUndefined(); - const secondResponse = await authMemberAgent - .get(`/credentials/${savedCredential.id}`) - .query({ includeData: true }); + const secondResponse = await authMemberAgent + .get(`/credentials/${savedCredential.id}`) + .query({ includeData: true }); - expect(secondResponse.statusCode).toBe(200); + expect(secondResponse.statusCode).toBe(200); - validateMainCredentialData(secondResponse.body.data); - expect(secondResponse.body.data.data).toBeDefined(); -}); + validateMainCredentialData(secondResponse.body.data); + expect(secondResponse.body.data.data).toBeDefined(); + }); -test('GET /credentials/:id should retrieve non-owned cred for owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const authOwnerAgent = authAgent(owner); - const member = await testDb.createUser({ globalRole: globalMemberRole }); + test('should retrieve non-owned cred for owner', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); - const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); + expect(response1.statusCode).toBe(200); - expect(response1.statusCode).toBe(200); + validateMainCredentialData(response1.body.data); + expect(response1.body.data.data).toBeUndefined(); - validateMainCredentialData(response1.body.data); - expect(response1.body.data.data).toBeUndefined(); + const response2 = await authOwnerAgent + .get(`/credentials/${savedCredential.id}`) + .query({ includeData: true }); - const response2 = await authOwnerAgent - .get(`/credentials/${savedCredential.id}`) - .query({ includeData: true }); + expect(response2.statusCode).toBe(200); - expect(response2.statusCode).toBe(200); + validateMainCredentialData(response2.body.data); + expect(response2.body.data.data).toBeDefined(); + }); - validateMainCredentialData(response2.body.data); - expect(response2.body.data.data).toBeDefined(); -}); + test('should not retrieve non-owned cred for member', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); -test('GET /credentials/:id should not retrieve non-owned cred for member', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); + const response = await authMemberAgent.get(`/credentials/${savedCredential.id}`); - const response = await authAgent(member).get(`/credentials/${savedCredential.id}`); + expect(response.statusCode).toBe(404); + expect(response.body.data).toBeUndefined(); // owner's cred not returned + }); - expect(response.statusCode).toBe(404); - expect(response.body.data).toBeUndefined(); // owner's cred not returned -}); + test('should fail with missing encryption key', async () => { + const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); -test('GET /credentials/:id should fail with missing encryption key', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); + const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); + mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); - const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); - mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); + const response = await authOwnerAgent + .get(`/credentials/${savedCredential.id}`) + .query({ includeData: true }); - const response = await authAgent(ownerShell) - .get(`/credentials/${savedCredential.id}`) - .query({ includeData: true }); + expect(response.statusCode).toBe(500); - expect(response.statusCode).toBe(500); + mock.mockRestore(); + }); - mock.mockRestore(); -}); + test('should return 404 if cred not found', async () => { + const response = await authOwnerAgent.get('/credentials/789'); + expect(response.statusCode).toBe(404); -test('GET /credentials/:id should return 404 if cred not found', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const response = await authAgent(ownerShell).get('/credentials/789'); - expect(response.statusCode).toBe(404); - - const responseAbc = await authAgent(ownerShell).get('/credentials/abc'); - expect(responseAbc.statusCode).toBe(404); + const responseAbc = await authOwnerAgent.get('/credentials/abc'); + expect(responseAbc.statusCode).toBe(404); + }); }); function validateMainCredentialData(credential: CredentialsEntity) { diff --git a/packages/cli/test/integration/eventbus.test.ts b/packages/cli/test/integration/eventbus.test.ts index d30b542f14..a2dee871a1 100644 --- a/packages/cli/test/integration/eventbus.test.ts +++ b/packages/cli/test/integration/eventbus.test.ts @@ -2,6 +2,8 @@ import express from 'express'; import config from '@/config'; import axios from 'axios'; import syslog from 'syslog-client'; +import { v4 as uuid } from 'uuid'; +import type { SuperAgentTest } from 'supertest'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; import { Role } from '@db/entities/Role'; @@ -15,14 +17,12 @@ import { MessageEventBusDestinationWebhookOptions, } from 'n8n-workflow'; import { eventBus } from '@/eventbus'; -import { SuperAgentTest } from 'supertest'; import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric'; import { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; import { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; import { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee'; import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAudit'; -import { v4 as uuid } from 'uuid'; -import { EventNamesTypes } from '../../src/eventbus/EventMessageClasses'; +import { EventNamesTypes } from '@/eventbus/EventMessageClasses'; jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); jest.mock('axios'); @@ -54,6 +54,7 @@ const testWebhookDestination: MessageEventBusDestinationWebhookOptions = { enabled: false, subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'], }; + const testSentryDestination: MessageEventBusDestinationSentryOptions = { ...defaultMessageEventBusDestinationSentryOptions, id: '450ca04b-87dd-4837-a052-ab3a347a00e9', @@ -101,13 +102,10 @@ beforeAll(async () => { config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter'); config.set('eventBus.logWriter.keepLogCount', '1'); config.set('enterprise.features.logStreaming', true); - - await eventBus.initialize(); -}); - -beforeEach(async () => { config.set('userManagement.disabled', false); config.set('userManagement.isInstanceOwnerSetUp', true); + + await eventBus.initialize(); }); afterAll(async () => { diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index f823355758..9c2358e3e0 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -1,11 +1,12 @@ import express from 'express'; import type { Entry as LdapUser } from 'ldapts'; +import { Not } from 'typeorm'; import { jsonParse } from 'n8n-workflow'; import config from '@/config'; import * as Db from '@/Db'; import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; -import { LDAP_DEFAULT_CONFIGURATION, LDAP_ENABLED, LDAP_FEATURE_NAME } from '@/Ldap/constants'; +import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants'; import { LdapManager } from '@/Ldap/LdapManager.ee'; import { LdapService } from '@/Ldap/LdapService.ee'; import { encryptPassword, saveLdapSynchronization } from '@/Ldap/helpers'; @@ -21,7 +22,6 @@ jest.mock('@/UserManagement/email/NodeMailer'); let app: express.Application; let globalMemberRole: Role; -let globalOwnerRole: Role; let owner: User; let authAgent: AuthAgent; @@ -42,11 +42,12 @@ const defaultLdapConfig = { beforeAll(async () => { app = await utils.initTestServer({ endpointGroups: ['auth', 'ldap'] }); - const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles(); + const [globalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles(); - globalOwnerRole = fetchedGlobalOwnerRole; globalMemberRole = fetchedGlobalMemberRole; + owner = await testDb.createUser({ globalRole: globalOwnerRole }); + authAgent = utils.createAuthAgent(app); defaultLdapConfig.bindingAdminPassword = await encryptPassword( @@ -64,10 +65,9 @@ beforeEach(async () => { 'Credentials', 'SharedWorkflow', 'Workflow', - 'User', ]); - owner = await testDb.createUser({ globalRole: globalOwnerRole }); + await Db.collections.User.delete({ id: Not(owner.id) }); jest.mock('@/telemetry'); diff --git a/packages/cli/test/integration/license.api.test.ts b/packages/cli/test/integration/license.api.test.ts index b8908915e1..44d84e1150 100644 --- a/packages/cli/test/integration/license.api.test.ts +++ b/packages/cli/test/integration/license.api.test.ts @@ -1,41 +1,36 @@ -import express from 'express'; - +import type { SuperAgentTest } from 'supertest'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; -import * as testDb from './shared/testDb'; -import type { AuthAgent } from './shared/types'; -import * as utils from './shared/utils'; +import type { User } from '@db/entities/User'; import { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; import { License } from '@/License'; +import * as testDb from './shared/testDb'; +import * as utils from './shared/utils'; const MOCK_SERVER_URL = 'https://server.com/v1'; const MOCK_RENEW_OFFSET = 259200; -const MOCK_INSTANCE_ID = 'instance-id'; -let app: express.Application; -let globalOwnerRole: Role; -let globalMemberRole: Role; -let authAgent: AuthAgent; -let license: License; +let owner: User; +let member: User; +let authOwnerAgent: SuperAgentTest; +let authMemberAgent: SuperAgentTest; beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['license'] }); + const app = await utils.initTestServer({ endpointGroups: ['license'] }); - globalOwnerRole = await testDb.getGlobalOwnerRole(); - globalMemberRole = await testDb.getGlobalMemberRole(); + const globalOwnerRole = await testDb.getGlobalOwnerRole(); + const globalMemberRole = await testDb.getGlobalMemberRole(); + owner = await testDb.createUserShell(globalOwnerRole); + member = await testDb.createUserShell(globalMemberRole); - authAgent = utils.createAuthAgent(app); + const authAgent = utils.createAuthAgent(app); + authOwnerAgent = authAgent(owner); + authMemberAgent = authAgent(member); config.set('license.serverUrl', MOCK_SERVER_URL); config.set('license.autoRenewEnabled', true); config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET); }); -beforeEach(async () => { - license = new License(); - await license.init(MOCK_INSTANCE_ID); -}); - afterEach(async () => { await testDb.truncate(['Settings']); }); @@ -44,98 +39,66 @@ afterAll(async () => { await testDb.terminate(); }); -test('GET /license should return license information to the instance owner', async () => { - const userShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(userShell).get('/license'); - - expect(response.statusCode).toBe(200); - - // No license defined so we just expect the result to be the defaults - expect(response.body).toStrictEqual(DEFAULT_LICENSE_RESPONSE); -}); - -test('GET /license should return license information to a regular user', async () => { - const userShell = await testDb.createUserShell(globalMemberRole); - - const response = await authAgent(userShell).get('/license'); - - expect(response.statusCode).toBe(200); - - // No license defined so we just expect the result to be the defaults - expect(response.body).toStrictEqual(DEFAULT_LICENSE_RESPONSE); -}); - -test('POST /license/activate should work for instance owner', async () => { - const userShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(userShell) - .post('/license/activate') - .send({ activationKey: 'abcde' }); - - expect(response.statusCode).toBe(200); - - // No license defined so we just expect the result to be the defaults - expect(response.body).toMatchObject(DEFAULT_POST_RESPONSE); -}); - -test('POST /license/activate does not work for regular users', async () => { - const userShell = await testDb.createUserShell(globalMemberRole); - - const response = await authAgent(userShell) - .post('/license/activate') - .send({ activationKey: 'abcde' }); - - expect(response.statusCode).toBe(403); - expect(response.body.message).toBe(NON_OWNER_ACTIVATE_RENEW_MESSAGE); -}); - -test('POST /license/activate errors out properly', async () => { - License.prototype.activate = jest.fn().mockImplementation(() => { - throw new Error(INVALID_ACIVATION_KEY_MESSAGE); +describe('GET /license', () => { + test('should return license information to the instance owner', async () => { + // No license defined so we just expect the result to be the defaults + await authOwnerAgent.get('/license').expect(200, DEFAULT_LICENSE_RESPONSE); }); - const userShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(userShell) - .post('/license/activate') - .send({ activationKey: 'abcde' }); - - expect(response.statusCode).toBe(400); - expect(response.body.message).toBe(INVALID_ACIVATION_KEY_MESSAGE); + test('should return license information to a regular user', async () => { + // No license defined so we just expect the result to be the defaults + await authMemberAgent.get('/license').expect(200, DEFAULT_LICENSE_RESPONSE); + }); }); -test('POST /license/renew should work for instance owner', async () => { - const userShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(userShell).post('/license/renew'); - - expect(response.statusCode).toBe(200); - - // No license defined so we just expect the result to be the defaults - expect(response.body).toMatchObject(DEFAULT_POST_RESPONSE); -}); - -test('POST /license/renew does not work for regular users', async () => { - const userShell = await testDb.createUserShell(globalMemberRole); - - const response = await authAgent(userShell).post('/license/renew'); - - expect(response.statusCode).toBe(403); - expect(response.body.message).toBe(NON_OWNER_ACTIVATE_RENEW_MESSAGE); -}); - -test('POST /license/renew errors out properly', async () => { - License.prototype.renew = jest.fn().mockImplementation(() => { - throw new Error(RENEW_ERROR_MESSAGE); +describe('POST /license/activate', () => { + test('should work for instance owner', async () => { + await authOwnerAgent + .post('/license/activate') + .send({ activationKey: 'abcde' }) + .expect(200, DEFAULT_POST_RESPONSE); }); - const userShell = await testDb.createUserShell(globalOwnerRole); + test('does not work for regular users', async () => { + await authMemberAgent + .post('/license/activate') + .send({ activationKey: 'abcde' }) + .expect(403, { code: 403, message: NON_OWNER_ACTIVATE_RENEW_MESSAGE }); + }); - const response = await authAgent(userShell).post('/license/renew'); + test('errors out properly', async () => { + License.prototype.activate = jest.fn().mockImplementation(() => { + throw new Error(INVALID_ACIVATION_KEY_MESSAGE); + }); - expect(response.statusCode).toBe(400); - expect(response.body.message).toBe(RENEW_ERROR_MESSAGE); + await authOwnerAgent + .post('/license/activate') + .send({ activationKey: 'abcde' }) + .expect(400, { code: 400, message: INVALID_ACIVATION_KEY_MESSAGE }); + }); +}); + +describe('POST /license/renew', () => { + test('should work for instance owner', async () => { + // No license defined so we just expect the result to be the defaults + await authOwnerAgent.post('/license/renew').expect(200, DEFAULT_POST_RESPONSE); + }); + + test('does not work for regular users', async () => { + await authMemberAgent + .post('/license/renew') + .expect(403, { code: 403, message: NON_OWNER_ACTIVATE_RENEW_MESSAGE }); + }); + + test('errors out properly', async () => { + License.prototype.renew = jest.fn().mockImplementation(() => { + throw new Error(RENEW_ERROR_MESSAGE); + }); + + await authOwnerAgent + .post('/license/renew') + .expect(400, { code: 400, message: RENEW_ERROR_MESSAGE }); + }); }); const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = { diff --git a/packages/cli/test/integration/me.api.test.ts b/packages/cli/test/integration/me.api.test.ts index 9a2d2cd7c5..7c5004a36e 100644 --- a/packages/cli/test/integration/me.api.test.ts +++ b/packages/cli/test/integration/me.api.test.ts @@ -1,10 +1,10 @@ -import express from 'express'; +import type { Application } from 'express'; +import type { SuperAgentTest } from 'supertest'; import { IsNull } from 'typeorm'; import validator from 'validator'; - -import config from '@/config'; import * as Db from '@/Db'; import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { randomApiKey, @@ -17,7 +17,7 @@ import * as testDb from './shared/testDb'; import type { AuthAgent } from './shared/types'; import * as utils from './shared/utils'; -let app: express.Application; +let app: Application; let globalOwnerRole: Role; let globalMemberRole: Role; let authAgent: AuthAgent; @@ -40,10 +40,16 @@ afterAll(async () => { }); describe('Owner shell', () => { - test('PATCH /me should succeed with valid inputs', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerShellAgent = authAgent(ownerShell); + let ownerShell: User; + let authOwnerShellAgent: SuperAgentTest; + beforeEach(async () => { + ownerShell = await testDb.createUserShell(globalOwnerRole); + await testDb.addApiKey(ownerShell); + authOwnerShellAgent = authAgent(ownerShell); + }); + + test('PATCH /me should succeed with valid inputs', async () => { for (const validPayload of VALID_PATCH_ME_PAYLOADS) { const response = await authOwnerShellAgent.patch('/me').send(validPayload); @@ -83,9 +89,6 @@ describe('Owner shell', () => { }); test('PATCH /me should fail with invalid inputs', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerShellAgent = authAgent(ownerShell); - for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { const response = await authOwnerShellAgent.patch('/me').send(invalidPayload); expect(response.statusCode).toBe(400); @@ -98,9 +101,6 @@ describe('Owner shell', () => { }); test('PATCH /me/password should fail for shell', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerShellAgent = authAgent(ownerShell); - const validPasswordPayload = { currentPassword: randomValidPassword(), newPassword: randomValidPassword(), @@ -130,9 +130,6 @@ describe('Owner shell', () => { }); test('POST /me/survey should succeed with valid inputs', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerShellAgent = authAgent(ownerShell); - const validPayloads = [SURVEY, {}]; for (const validPayload of validPayloads) { @@ -150,9 +147,7 @@ describe('Owner shell', () => { }); test('POST /me/api-key should create an api key', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell).post('/me/api-key'); + const response = await authOwnerShellAgent.post('/me/api-key'); expect(response.statusCode).toBe(200); expect(response.body.data.apiKey).toBeDefined(); @@ -166,20 +161,14 @@ describe('Owner shell', () => { }); test('GET /me/api-key should fetch the api key', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); - - const response = await authAgent(ownerShell).get('/me/api-key'); + const response = await authOwnerShellAgent.get('/me/api-key'); expect(response.statusCode).toBe(200); expect(response.body.data.apiKey).toEqual(ownerShell.apiKey); }); test('DELETE /me/api-key should fetch the api key', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); - - const response = await authAgent(ownerShell).delete('/me/api-key'); + const response = await authOwnerShellAgent.delete('/me/api-key'); expect(response.statusCode).toBe(200); @@ -192,19 +181,22 @@ describe('Owner shell', () => { }); describe('Member', () => { - beforeEach(async () => { - config.set('userManagement.isInstanceOwnerSetUp', true); + const memberPassword = randomValidPassword(); + let member: User; + let authMemberAgent: SuperAgentTest; - await Db.collections.Settings.update( - { key: 'userManagement.isInstanceOwnerSetUp' }, - { value: JSON.stringify(true) }, - ); + beforeEach(async () => { + member = await testDb.createUser({ + password: memberPassword, + globalRole: globalMemberRole, + apiKey: randomApiKey(), + }); + authMemberAgent = authAgent(member); + + await utils.setInstanceOwnerSetUp(true); }); test('PATCH /me should succeed with valid inputs', async () => { - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const authMemberAgent = authAgent(member); - for (const validPayload of VALID_PATCH_ME_PAYLOADS) { const response = await authMemberAgent.patch('/me').send(validPayload); @@ -244,9 +236,6 @@ describe('Member', () => { }); test('PATCH /me should fail with invalid inputs', async () => { - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const authMemberAgent = authAgent(member); - for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { const response = await authMemberAgent.patch('/me').send(invalidPayload); expect(response.statusCode).toBe(400); @@ -259,18 +248,12 @@ describe('Member', () => { }); test('PATCH /me/password should succeed with valid inputs', async () => { - const memberPassword = randomValidPassword(); - const member = await testDb.createUser({ - password: memberPassword, - globalRole: globalMemberRole, - }); - const validPayload = { currentPassword: memberPassword, newPassword: randomValidPassword(), }; - const response = await authAgent(member).patch('/me/password').send(validPayload); + const response = await authMemberAgent.patch('/me/password').send(validPayload); expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); @@ -280,9 +263,6 @@ describe('Member', () => { }); test('PATCH /me/password should fail with invalid inputs', async () => { - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const authMemberAgent = authAgent(member); - for (const payload of INVALID_PASSWORD_PAYLOADS) { const response = await authMemberAgent.patch('/me/password').send(payload); expect([400, 500].includes(response.statusCode)).toBe(true); @@ -299,9 +279,6 @@ describe('Member', () => { }); test('POST /me/survey should succeed with valid inputs', async () => { - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const authMemberAgent = authAgent(member); - const validPayloads = [SURVEY, {}]; for (const validPayload of validPayloads) { @@ -318,11 +295,6 @@ describe('Member', () => { }); test('POST /me/api-key should create an api key', async () => { - const member = await testDb.createUser({ - globalRole: globalMemberRole, - apiKey: randomApiKey(), - }); - const response = await authAgent(member).post('/me/api-key'); expect(response.statusCode).toBe(200); @@ -335,11 +307,6 @@ describe('Member', () => { }); test('GET /me/api-key should fetch the api key', async () => { - const member = await testDb.createUser({ - globalRole: globalMemberRole, - apiKey: randomApiKey(), - }); - const response = await authAgent(member).get('/me/api-key'); expect(response.statusCode).toBe(200); @@ -347,11 +314,6 @@ describe('Member', () => { }); test('DELETE /me/api-key should fetch the api key', async () => { - const member = await testDb.createUser({ - globalRole: globalMemberRole, - apiKey: randomApiKey(), - }); - const response = await authAgent(member).delete('/me/api-key'); expect(response.statusCode).toBe(200); @@ -364,7 +326,7 @@ describe('Member', () => { describe('Owner', () => { beforeEach(async () => { - config.set('userManagement.isInstanceOwnerSetUp', true); + await utils.setInstanceOwnerSetUp(true); }); test('PATCH /me should succeed with valid inputs', async () => { diff --git a/packages/cli/test/integration/nodes.api.test.ts b/packages/cli/test/integration/nodes.api.test.ts index 9e20ca1b74..37c619d13e 100644 --- a/packages/cli/test/integration/nodes.api.test.ts +++ b/packages/cli/test/integration/nodes.api.test.ts @@ -1,10 +1,6 @@ import path from 'path'; - -import express from 'express'; import { mocked } from 'jest-mock'; - -import * as utils from './shared/utils'; -import * as testDb from './shared/testDb'; +import type { SuperAgentTest } from 'supertest'; import { executeCommand, checkNpmPackageStatus, @@ -15,13 +11,13 @@ import { import { findInstalledPackage, isPackageInstalled } from '@/CommunityNodes/packageModel'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { InstalledPackages } from '@db/entities/InstalledPackages'; - -import type { Role } from '@db/entities/Role'; -import type { AuthAgent } from './shared/types'; +import type { User } from '@db/entities/User'; import type { InstalledNodes } from '@db/entities/InstalledNodes'; -import { COMMUNITY_PACKAGE_VERSION } from './shared/constants'; import { NodeTypes } from '@/NodeTypes'; import { Push } from '@/push'; +import { COMMUNITY_PACKAGE_VERSION } from './shared/constants'; +import * as utils from './shared/utils'; +import * as testDb from './shared/testDb'; const mockLoadNodesAndCredentials = utils.mockInstance(LoadNodesAndCredentials); utils.mockInstance(NodeTypes); @@ -48,22 +44,21 @@ jest.mock('@/CommunityNodes/packageModel', () => { const mockedEmptyPackage = mocked(utils.emptyPackage); -let app: express.Application; -let globalOwnerRole: Role; -let authAgent: AuthAgent; +let ownerShell: User; +let authOwnerShellAgent: SuperAgentTest; beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['nodes'] }); + const app = await utils.initTestServer({ endpointGroups: ['nodes'] }); - globalOwnerRole = await testDb.getGlobalOwnerRole(); - - authAgent = utils.createAuthAgent(app); + const globalOwnerRole = await testDb.getGlobalOwnerRole(); + ownerShell = await testDb.createUserShell(globalOwnerRole); + authOwnerShellAgent = utils.createAuthAgent(app)(ownerShell); utils.initConfigFile(); }); beforeEach(async () => { - await testDb.truncate(['InstalledNodes', 'InstalledPackages', 'User']); + await testDb.truncate(['InstalledNodes', 'InstalledPackages']); mocked(executeCommand).mockReset(); mocked(findInstalledPackage).mockReset(); @@ -73,255 +68,216 @@ afterAll(async () => { await testDb.terminate(); }); -/** - * GET /nodes - */ +describe('GET /nodes', () => { + test('should respond 200 if no nodes are installed', async () => { + const { + statusCode, + body: { data }, + } = await authOwnerShellAgent.get('/nodes'); -test('GET /nodes should respond 200 if no nodes are installed', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); + expect(statusCode).toBe(200); + expect(data).toHaveLength(0); + }); - const { - statusCode, - body: { data }, - } = await authAgent(ownerShell).get('/nodes'); + test('should return list of one installed package and node', async () => { + const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); + await testDb.saveInstalledNode(utils.installedNodePayload(packageName)); - expect(statusCode).toBe(200); - expect(data).toHaveLength(0); -}); + const { + statusCode, + body: { data }, + } = await authOwnerShellAgent.get('/nodes'); -test('GET /nodes should return list of one installed package and node', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); + expect(statusCode).toBe(200); + expect(data).toHaveLength(1); + expect(data[0].installedNodes).toHaveLength(1); + }); - const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); - await testDb.saveInstalledNode(utils.installedNodePayload(packageName)); + test('should return list of multiple installed packages and nodes', async () => { + const first = await testDb.saveInstalledPackage(utils.installedPackagePayload()); + await testDb.saveInstalledNode(utils.installedNodePayload(first.packageName)); - const { - statusCode, - body: { data }, - } = await authAgent(ownerShell).get('/nodes'); + const second = await testDb.saveInstalledPackage(utils.installedPackagePayload()); + await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName)); + await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName)); - expect(statusCode).toBe(200); - expect(data).toHaveLength(1); - expect(data[0].installedNodes).toHaveLength(1); -}); + const { + statusCode, + body: { data }, + } = await authOwnerShellAgent.get('/nodes'); -test('GET /nodes should return list of multiple installed packages and nodes', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); + expect(statusCode).toBe(200); + expect(data).toHaveLength(2); - const first = await testDb.saveInstalledPackage(utils.installedPackagePayload()); - await testDb.saveInstalledNode(utils.installedNodePayload(first.packageName)); + const allNodes = data.reduce( + (acc: InstalledNodes[], cur: InstalledPackages) => acc.concat(cur.installedNodes), + [], + ); - const second = await testDb.saveInstalledPackage(utils.installedPackagePayload()); - await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName)); - await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName)); + expect(allNodes).toHaveLength(3); + }); - const { - statusCode, - body: { data }, - } = await authAgent(ownerShell).get('/nodes'); + test('should not check for updates if no packages installed', async () => { + await authOwnerShellAgent.get('/nodes'); - expect(statusCode).toBe(200); - expect(data).toHaveLength(2); + expect(mocked(executeCommand)).toHaveBeenCalledTimes(0); + }); - const allNodes = data.reduce( - (acc: InstalledNodes[], cur: InstalledPackages) => acc.concat(cur.installedNodes), - [], - ); + test('should check for updates if packages installed', async () => { + const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); + await testDb.saveInstalledNode(utils.installedNodePayload(packageName)); - expect(allNodes).toHaveLength(3); -}); + await authOwnerShellAgent.get('/nodes'); -test('GET /nodes should not check for updates if no packages installed', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); + expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', { + doNotHandleError: true, + }); + }); - await authAgent(ownerShell).get('/nodes'); + test('should report package updates if available', async () => { + const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); + await testDb.saveInstalledNode(utils.installedNodePayload(packageName)); - expect(mocked(executeCommand)).toHaveBeenCalledTimes(0); -}); + mocked(executeCommand).mockImplementationOnce(() => { + throw { + code: 1, + stdout: JSON.stringify({ + [packageName]: { + current: COMMUNITY_PACKAGE_VERSION.CURRENT, + wanted: COMMUNITY_PACKAGE_VERSION.CURRENT, + latest: COMMUNITY_PACKAGE_VERSION.UPDATED, + location: path.join('node_modules', packageName), + }, + }), + }; + }); -test('GET /nodes should check for updates if packages installed', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); + mocked(isNpmError).mockReturnValueOnce(true); - const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); - await testDb.saveInstalledNode(utils.installedNodePayload(packageName)); + const { + body: { data }, + } = await authOwnerShellAgent.get('/nodes'); - await authAgent(ownerShell).get('/nodes'); - - expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', { - doNotHandleError: true, + expect(data[0].installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT); + expect(data[0].updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED); }); }); -test('GET /nodes should report package updates if available', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); +describe('POST /nodes', () => { + test('should reject if package name is missing', async () => { + const { statusCode } = await authOwnerShellAgent.post('/nodes'); - const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); - await testDb.saveInstalledNode(utils.installedNodePayload(packageName)); - - mocked(executeCommand).mockImplementationOnce(() => { - throw { - code: 1, - stdout: JSON.stringify({ - [packageName]: { - current: COMMUNITY_PACKAGE_VERSION.CURRENT, - wanted: COMMUNITY_PACKAGE_VERSION.CURRENT, - latest: COMMUNITY_PACKAGE_VERSION.UPDATED, - location: path.join('node_modules', packageName), - }, - }), - }; + expect(statusCode).toBe(400); }); - mocked(isNpmError).mockReturnValueOnce(true); + test('should reject if package is duplicate', async () => { + mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages()); + mocked(isPackageInstalled).mockResolvedValueOnce(true); + mocked(hasPackageLoaded).mockReturnValueOnce(true); - const { - body: { data }, - } = await authAgent(ownerShell).get('/nodes'); + const { + statusCode, + body: { message }, + } = await authOwnerShellAgent.post('/nodes').send({ + name: utils.installedPackagePayload().packageName, + }); - expect(data[0].installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT); - expect(data[0].updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED); -}); - -/** - * POST /nodes - */ - -test('POST /nodes should reject if package name is missing', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const { statusCode } = await authAgent(ownerShell).post('/nodes'); - - expect(statusCode).toBe(400); -}); - -test('POST /nodes should reject if package is duplicate', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages()); - mocked(isPackageInstalled).mockResolvedValueOnce(true); - mocked(hasPackageLoaded).mockReturnValueOnce(true); - - const { - statusCode, - body: { message }, - } = await authAgent(ownerShell).post('/nodes').send({ - name: utils.installedPackagePayload().packageName, + expect(statusCode).toBe(400); + expect(message).toContain('already installed'); }); - expect(statusCode).toBe(400); - expect(message).toContain('already installed'); -}); + test('should allow installing packages that could not be loaded', async () => { + mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages()); + mocked(hasPackageLoaded).mockReturnValueOnce(false); + mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' }); -test('POST /nodes should allow installing packages that could not be loaded', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); + mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage); - mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages()); - mocked(hasPackageLoaded).mockReturnValueOnce(false); - mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' }); + const { statusCode } = await authOwnerShellAgent.post('/nodes').send({ + name: utils.installedPackagePayload().packageName, + }); - mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage); - - const { statusCode } = await authAgent(ownerShell).post('/nodes').send({ - name: utils.installedPackagePayload().packageName, + expect(statusCode).toBe(200); + expect(mocked(removePackageFromMissingList)).toHaveBeenCalled(); }); - expect(statusCode).toBe(200); - expect(mocked(removePackageFromMissingList)).toHaveBeenCalled(); + test('should not install a banned package', async () => { + mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' }); + + const { + statusCode, + body: { message }, + } = await authOwnerShellAgent.post('/nodes').send({ + name: utils.installedPackagePayload().packageName, + }); + + expect(statusCode).toBe(400); + expect(message).toContain('banned'); + }); }); -test('POST /nodes should not install a banned package', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' }); +describe('DELETE /nodes', () => { + test('should not delete if package name is empty', async () => { + const response = await authOwnerShellAgent.delete('/nodes'); - const { - statusCode, - body: { message }, - } = await authAgent(ownerShell).post('/nodes').send({ - name: utils.installedPackagePayload().packageName, + expect(response.statusCode).toBe(400); }); - expect(statusCode).toBe(400); - expect(message).toContain('banned'); -}); + test('should reject if package is not installed', async () => { + const { + statusCode, + body: { message }, + } = await authOwnerShellAgent.delete('/nodes').query({ + name: utils.installedPackagePayload().packageName, + }); -/** - * DELETE /nodes - */ - -test('DELETE /nodes should not delete if package name is empty', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell).delete('/nodes'); - - expect(response.statusCode).toBe(400); -}); - -test('DELETE /nodes should reject if package is not installed', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const { - statusCode, - body: { message }, - } = await authAgent(ownerShell).delete('/nodes').query({ - name: utils.installedPackagePayload().packageName, + expect(statusCode).toBe(400); + expect(message).toContain('not installed'); }); - expect(statusCode).toBe(400); - expect(message).toContain('not installed'); + test('should uninstall package', async () => { + const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn()); + + mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage); + + const { statusCode } = await authOwnerShellAgent.delete('/nodes').query({ + name: utils.installedPackagePayload().packageName, + }); + + expect(statusCode).toBe(200); + expect(removeSpy).toHaveBeenCalledTimes(1); + }); }); -test('DELETE /nodes should uninstall package', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); +describe('PATCH /nodes', () => { + test('should reject if package name is empty', async () => { + const response = await authOwnerShellAgent.patch('/nodes'); - const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn()); - - mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage); - - const { statusCode } = await authAgent(ownerShell).delete('/nodes').query({ - name: utils.installedPackagePayload().packageName, + expect(response.statusCode).toBe(400); }); - expect(statusCode).toBe(200); - expect(removeSpy).toHaveBeenCalledTimes(1); -}); + test('reject if package is not installed', async () => { + const { + statusCode, + body: { message }, + } = await authOwnerShellAgent.patch('/nodes').send({ + name: utils.installedPackagePayload().packageName, + }); -/** - * PATCH /nodes - */ - -test('PATCH /nodes should reject if package name is empty', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell).patch('/nodes'); - - expect(response.statusCode).toBe(400); -}); - -test('PATCH /nodes reject if package is not installed', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const { - statusCode, - body: { message }, - } = await authAgent(ownerShell).patch('/nodes').send({ - name: utils.installedPackagePayload().packageName, + expect(statusCode).toBe(400); + expect(message).toContain('not installed'); }); - expect(statusCode).toBe(400); - expect(message).toContain('not installed'); -}); + test('should update a package', async () => { + const updateSpy = + mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage); -test('PATCH /nodes should update a package', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); + mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage); - const updateSpy = - mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage); + await authOwnerShellAgent.patch('/nodes').send({ + name: utils.installedPackagePayload().packageName, + }); - mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage); - - await authAgent(ownerShell).patch('/nodes').send({ - name: utils.installedPackagePayload().packageName, + expect(updateSpy).toHaveBeenCalledTimes(1); }); - - expect(updateSpy).toHaveBeenCalledTimes(1); }); diff --git a/packages/cli/test/integration/owner.api.test.ts b/packages/cli/test/integration/owner.api.test.ts index 0f0e79d5a6..8b59df68d7 100644 --- a/packages/cli/test/integration/owner.api.test.ts +++ b/packages/cli/test/integration/owner.api.test.ts @@ -1,9 +1,11 @@ -import express from 'express'; +import type { Application } from 'express'; import validator from 'validator'; +import type { SuperAgentTest } from 'supertest'; import config from '@/config'; import * as Db from '@/Db'; import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; import { randomEmail, randomInvalidPassword, @@ -11,23 +13,22 @@ import { randomValidPassword, } from './shared/random'; import * as testDb from './shared/testDb'; -import type { AuthAgent } from './shared/types'; import * as utils from './shared/utils'; -let app: express.Application; +let app: Application; let globalOwnerRole: Role; -let authAgent: AuthAgent; +let ownerShell: User; +let authOwnerShellAgent: SuperAgentTest; beforeAll(async () => { app = await utils.initTestServer({ endpointGroups: ['owner'] }); - globalOwnerRole = await testDb.getGlobalOwnerRole(); - - authAgent = utils.createAuthAgent(app); }); beforeEach(async () => { config.set('userManagement.isInstanceOwnerSetUp', false); + ownerShell = await testDb.createUserShell(globalOwnerRole); + authOwnerShellAgent = utils.createAuthAgent(app)(ownerShell); }); afterEach(async () => { @@ -38,152 +39,149 @@ afterAll(async () => { await testDb.terminate(); }); -test('POST /owner/setup should create owner and enable isInstanceOwnerSetUp', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); +describe('POST /owner/setup', () => { + test('should create owner and enable isInstanceOwnerSetUp', async () => { + const newOwnerData = { + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }; - const newOwnerData = { - email: randomEmail(), - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), - }; + const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData); - const response = await authAgent(ownerShell).post('/owner/setup').send(newOwnerData); + expect(response.statusCode).toBe(200); - expect(response.statusCode).toBe(200); + const { + id, + email, + firstName, + lastName, + personalizationAnswers, + globalRole, + password, + resetPasswordToken, + isPending, + apiKey, + } = response.body.data; - const { - id, - email, - firstName, - lastName, - personalizationAnswers, - globalRole, - password, - resetPasswordToken, - isPending, - apiKey, - } = response.body.data; + expect(validator.isUUID(id)).toBe(true); + 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); + expect(resetPasswordToken).toBeUndefined(); + expect(globalRole.name).toBe('owner'); + expect(globalRole.scope).toBe('global'); + expect(apiKey).toBeUndefined(); - expect(validator.isUUID(id)).toBe(true); - 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); - expect(resetPasswordToken).toBeUndefined(); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); - expect(apiKey).toBeUndefined(); + const storedOwner = await Db.collections.User.findOneByOrFail({ id }); + 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 storedOwner = await Db.collections.User.findOneByOrFail({ id }); - 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.getEnv('userManagement.isInstanceOwnerSetUp'); + expect(isInstanceOwnerSetUpConfig).toBe(true); - const isInstanceOwnerSetUpConfig = config.getEnv('userManagement.isInstanceOwnerSetUp'); - expect(isInstanceOwnerSetUpConfig).toBe(true); - - const isInstanceOwnerSetUpSetting = await utils.isInstanceOwnerSetUp(); - expect(isInstanceOwnerSetUpSetting).toBe(true); -}); - -test('POST /owner/setup should create owner with lowercased email', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const newOwnerData = { - email: randomEmail().toUpperCase(), - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), - }; - - const response = await authAgent(ownerShell).post('/owner/setup').send(newOwnerData); - - expect(response.statusCode).toBe(200); - - const { id, email } = response.body.data; - - expect(id).toBe(ownerShell.id); - expect(email).toBe(newOwnerData.email.toLowerCase()); - - const storedOwner = await Db.collections.User.findOneByOrFail({ id }); - expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase()); -}); - -test('POST /owner/setup should fail with invalid inputs', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = authAgent(ownerShell); - - await Promise.all( - INVALID_POST_OWNER_PAYLOADS.map(async (invalidPayload) => { - const response = await authOwnerAgent.post('/owner/setup').send(invalidPayload); - expect(response.statusCode).toBe(400); - }), - ); -}); - -test('POST /owner/skip-setup should persist skipping setup to the DB', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - - const response = await authAgent(ownerShell).post('/owner/skip-setup').send(); - - expect(response.statusCode).toBe(200); - - const skipConfig = config.getEnv('userManagement.skipInstanceOwnerSetup'); - expect(skipConfig).toBe(true); - - const { value } = await Db.collections.Settings.findOneByOrFail({ - key: 'userManagement.skipInstanceOwnerSetup', + const isInstanceOwnerSetUpSetting = await utils.isInstanceOwnerSetUp(); + expect(isInstanceOwnerSetUpSetting).toBe(true); + }); + + test('should create owner with lowercased email', async () => { + const newOwnerData = { + email: randomEmail().toUpperCase(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }; + + const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData); + + expect(response.statusCode).toBe(200); + + const { id, email } = response.body.data; + + expect(id).toBe(ownerShell.id); + expect(email).toBe(newOwnerData.email.toLowerCase()); + + const storedOwner = await Db.collections.User.findOneByOrFail({ id }); + expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase()); + }); + + const INVALID_POST_OWNER_PAYLOADS = [ + { + email: '', + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }, + { + email: randomEmail(), + firstName: '', + lastName: randomName(), + password: randomValidPassword(), + }, + { + email: randomEmail(), + firstName: randomName(), + lastName: '', + password: randomValidPassword(), + }, + { + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomInvalidPassword(), + }, + { + firstName: randomName(), + lastName: randomName(), + }, + { + firstName: randomName(), + }, + { + lastName: randomName(), + }, + { + email: randomEmail(), + firstName: 'John { + const authOwnerAgent = authOwnerShellAgent; + + await Promise.all( + INVALID_POST_OWNER_PAYLOADS.map(async (invalidPayload) => { + const response = await authOwnerAgent.post('/owner/setup').send(invalidPayload); + expect(response.statusCode).toBe(400); + }), + ); }); - expect(value).toBe('true'); }); -const INVALID_POST_OWNER_PAYLOADS = [ - { - email: '', - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), - }, - { - email: randomEmail(), - firstName: '', - lastName: randomName(), - password: randomValidPassword(), - }, - { - email: randomEmail(), - firstName: randomName(), - lastName: '', - password: randomValidPassword(), - }, - { - email: randomEmail(), - firstName: randomName(), - lastName: randomName(), - password: randomInvalidPassword(), - }, - { - firstName: randomName(), - lastName: randomName(), - }, - { - firstName: randomName(), - }, - { - lastName: randomName(), - }, - { - email: randomEmail(), - firstName: 'John { + test('should persist skipping setup to the DB', async () => { + const response = await authOwnerShellAgent.post('/owner/skip-setup').send(); + + expect(response.statusCode).toBe(200); + + const skipConfig = config.getEnv('userManagement.skipInstanceOwnerSetup'); + expect(skipConfig).toBe(true); + + const { value } = await Db.collections.Settings.findOneByOrFail({ + key: 'userManagement.skipInstanceOwnerSetup', + }); + expect(value).toBe('true'); + }); +}); diff --git a/packages/cli/test/integration/passwordReset.api.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts index 13e458ad11..38c9042ddf 100644 --- a/packages/cli/test/integration/passwordReset.api.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -1,10 +1,12 @@ -import express from 'express'; +import type { SuperAgentTest } from 'supertest'; import { v4 as uuid } from 'uuid'; +import { compare } from 'bcryptjs'; -import * as utils from './shared/utils'; import * as Db from '@/Db'; import config from '@/config'; -import { compare } from 'bcryptjs'; +import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; +import * as utils from './shared/utils'; import { randomEmail, randomInvalidPassword, @@ -12,276 +14,237 @@ import { randomValidPassword, } from './shared/random'; import * as testDb from './shared/testDb'; -import type { Role } from '@db/entities/Role'; jest.mock('@/UserManagement/email/NodeMailer'); -let app: express.Application; let globalOwnerRole: Role; let globalMemberRole: Role; +let owner: User; +let authlessAgent: SuperAgentTest; beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['passwordReset'] }); + const app = await utils.initTestServer({ endpointGroups: ['passwordReset'] }); globalOwnerRole = await testDb.getGlobalOwnerRole(); globalMemberRole = await testDb.getGlobalMemberRole(); + + authlessAgent = utils.createAgent(app); }); beforeEach(async () => { await testDb.truncate(['User']); - - jest.mock('@/config'); + owner = await testDb.createUser({ globalRole: globalOwnerRole }); config.set('userManagement.isInstanceOwnerSetUp', true); - config.set('userManagement.emails.mode', ''); }); afterAll(async () => { await testDb.terminate(); }); -test('POST /forgot-password should send password reset email', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); +describe('POST /forgot-password', () => { + test('should send password reset email', async () => { + const member = await testDb.createUser({ + email: 'test@test.com', + globalRole: globalMemberRole, + }); - const authlessAgent = utils.createAgent(app); - const member = await testDb.createUser({ - email: 'test@test.com', - globalRole: globalMemberRole, + config.set('userManagement.emails.mode', 'smtp'); + + await Promise.all( + [{ email: owner.email }, { email: member.email.toUpperCase() }].map(async (payload) => { + const response = await authlessAgent.post('/forgot-password').send(payload); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({}); + + const user = await Db.collections.User.findOneByOrFail({ email: payload.email }); + expect(user.resetPasswordToken).toBeDefined(); + expect(user.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000)); + }), + ); }); - config.set('userManagement.emails.mode', 'smtp'); + test('should fail if emailing is not set up', async () => { + config.set('userManagement.emails.mode', ''); - await Promise.all( - [{ email: owner.email }, { email: member.email.toUpperCase() }].map(async (payload) => { - const response = await authlessAgent.post('/forgot-password').send(payload); + await authlessAgent.post('/forgot-password').send({ email: owner.email }).expect(500); - expect(response.statusCode).toBe(200); - expect(response.body).toEqual({}); + const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email }); + expect(storedOwner.resetPasswordToken).toBeNull(); + }); - const user = await Db.collections.User.findOneByOrFail({ email: payload.email }); - expect(user.resetPasswordToken).toBeDefined(); - expect(user.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000)); - }), - ); + test('should fail with invalid inputs', async () => { + config.set('userManagement.emails.mode', 'smtp'); + + const invalidPayloads = [ + randomEmail(), + [randomEmail()], + {}, + [{ name: randomName() }], + [{ email: randomName() }], + ]; + + await Promise.all( + invalidPayloads.map(async (invalidPayload) => { + const response = await authlessAgent.post('/forgot-password').send(invalidPayload); + expect(response.statusCode).toBe(400); + + const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email }); + expect(storedOwner.resetPasswordToken).toBeNull(); + }), + ); + }); + + test('should fail if user is not found', async () => { + config.set('userManagement.emails.mode', 'smtp'); + + const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() }); + + expect(response.statusCode).toBe(200); // expect 200 to remain vague + }); }); -test('POST /forgot-password should fail if emailing is not set up', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); +describe('GET /resolve-password-token', () => { + beforeEach(() => { + config.set('userManagement.emails.mode', 'smtp'); + }); - const authlessAgent = utils.createAgent(app); + test('should succeed with valid inputs', async () => { + const resetPasswordToken = uuid(); + const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; - const response = await authlessAgent.post('/forgot-password').send({ email: owner.email }); + await Db.collections.User.update(owner.id, { + resetPasswordToken, + resetPasswordTokenExpiration, + }); - expect(response.statusCode).toBe(500); + const response = await authlessAgent + .get('/resolve-password-token') + .query({ userId: owner.id, token: resetPasswordToken }); - const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email }); - expect(storedOwner.resetPasswordToken).toBeNull(); -}); + expect(response.statusCode).toBe(200); + }); -test('POST /forgot-password should fail with invalid inputs', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + test('should fail with invalid inputs', async () => { + const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() }); - const authlessAgent = utils.createAgent(app); + const second = await authlessAgent.get('/resolve-password-token').query({ userId: owner.id }); - config.set('userManagement.emails.mode', 'smtp'); - - const invalidPayloads = [ - randomEmail(), - [randomEmail()], - {}, - [{ name: randomName() }], - [{ email: randomName() }], - ]; - - await Promise.all( - invalidPayloads.map(async (invalidPayload) => { - const response = await authlessAgent.post('/forgot-password').send(invalidPayload); + for (const response of [first, second]) { expect(response.statusCode).toBe(400); - - const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email }); - expect(storedOwner.resetPasswordToken).toBeNull(); - }), - ); -}); - -test('POST /forgot-password should fail if user is not found', async () => { - const authlessAgent = utils.createAgent(app); - - config.set('userManagement.emails.mode', 'smtp'); - - const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() }); - - 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(owner.id, { - resetPasswordToken, - resetPasswordTokenExpiration, + } }); - const response = await authlessAgent - .get('/resolve-password-token') - .query({ userId: owner.id, token: resetPasswordToken }); + test('should fail if user is not found', async () => { + const response = await authlessAgent + .get('/resolve-password-token') + .query({ userId: owner.id, token: uuid() }); - 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: owner.id }); - - for (const response of [first, second]) { - expect(response.statusCode).toBe(400); - } -}); - -test('GET /resolve-password-token should fail if user is not found', async () => { - const owner = await 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: 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(owner.id, { - resetPasswordToken, - resetPasswordTokenExpiration, + expect(response.statusCode).toBe(404); }); - config.set('userManagement.emails.mode', 'smtp'); + test('should fail if token is expired', async () => { + const resetPasswordToken = uuid(); + const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1; - const response = await authlessAgent - .get('/resolve-password-token') - .query({ userId: owner.id, token: resetPasswordToken }); + await Db.collections.User.update(owner.id, { + resetPasswordToken, + resetPasswordTokenExpiration, + }); - expect(response.statusCode).toBe(404); + const response = await authlessAgent + .get('/resolve-password-token') + .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); - +describe('POST /change-password', () => { const resetPasswordToken = uuid(); - const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; - - await Db.collections.User.update(owner.id, { - resetPasswordToken, - resetPasswordTokenExpiration, - }); - const passwordToStore = randomValidPassword(); - const response = await authlessAgent.post('/change-password').send({ - token: resetPasswordToken, - userId: owner.id, - password: passwordToStore, - }); + test('should succeed with valid inputs', async () => { + const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; - expect(response.statusCode).toBe(200); + await Db.collections.User.update(owner.id, { + resetPasswordToken, + resetPasswordTokenExpiration, + }); - const authToken = utils.getAuthToken(response); - expect(authToken).toBeDefined(); - - const { password: storedPassword } = await Db.collections.User.findOneByOrFail({ id: owner.id }); - - 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(owner.id, { - resetPasswordToken, - resetPasswordTokenExpiration, - }); - - const invalidPayloads = [ - { token: uuid() }, - { id: owner.id }, - { password: randomValidPassword() }, - { token: uuid(), id: owner.id }, - { token: uuid(), password: randomValidPassword() }, - { id: owner.id, password: randomValidPassword() }, - { - id: owner.id, - password: randomInvalidPassword(), + const response = await authlessAgent.post('/change-password').send({ token: resetPasswordToken, - }, - { + userId: owner.id, + password: passwordToStore, + }); + + expect(response.statusCode).toBe(200); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); + + const { password: storedPassword } = await Db.collections.User.findOneByOrFail({ id: owner.id, - password: randomValidPassword(), - token: uuid(), - }, - ]; + }); - await Promise.all( - invalidPayloads.map(async (invalidPayload) => { - const response = await authlessAgent.post('/change-password').query(invalidPayload); - expect(response.statusCode).toBe(400); - - const { password: storedPassword } = await Db.collections.User.findOneByOrFail({}); - 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(owner.id, { - resetPasswordToken, - resetPasswordTokenExpiration, + const comparisonResult = await compare(passwordToStore, storedPassword); + expect(comparisonResult).toBe(true); + expect(storedPassword).not.toBe(passwordToStore); }); - const passwordToStore = randomValidPassword(); + test('should fail with invalid inputs', async () => { + const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100; - const response = await authlessAgent.post('/change-password').send({ - token: resetPasswordToken, - userId: owner.id, - password: passwordToStore, + await Db.collections.User.update(owner.id, { + resetPasswordToken, + resetPasswordTokenExpiration, + }); + + const invalidPayloads = [ + { token: uuid() }, + { id: owner.id }, + { password: randomValidPassword() }, + { token: uuid(), id: owner.id }, + { token: uuid(), password: randomValidPassword() }, + { id: owner.id, password: randomValidPassword() }, + { + id: owner.id, + password: randomInvalidPassword(), + token: resetPasswordToken, + }, + { + id: owner.id, + password: randomValidPassword(), + token: uuid(), + }, + ]; + + await Promise.all( + invalidPayloads.map(async (invalidPayload) => { + const response = await authlessAgent.post('/change-password').query(invalidPayload); + expect(response.statusCode).toBe(400); + + const { password: storedPassword } = await Db.collections.User.findOneByOrFail({}); + expect(owner.password).toBe(storedPassword); + }), + ); }); - expect(response.statusCode).toBe(404); + test('should fail when token has expired', async () => { + const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1; + + await Db.collections.User.update(owner.id, { + resetPasswordToken, + resetPasswordTokenExpiration, + }); + + const response = await authlessAgent.post('/change-password').send({ + token: resetPasswordToken, + userId: owner.id, + password: passwordToStore, + }); + + expect(response.statusCode).toBe(404); + }); }); diff --git a/packages/cli/test/integration/publicApi/credentials.test.ts b/packages/cli/test/integration/publicApi/credentials.test.ts index 10b486cbaf..a9754ceb31 100644 --- a/packages/cli/test/integration/publicApi/credentials.test.ts +++ b/packages/cli/test/integration/publicApi/credentials.test.ts @@ -1,24 +1,25 @@ -import express from 'express'; - +import type { SuperAgentTest } from 'supertest'; import { UserSettings } from 'n8n-core'; - import * as Db from '@/Db'; import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { randomApiKey, randomName, randomString } from '../shared/random'; import * as utils from '../shared/utils'; import type { CredentialPayload, SaveCredentialFunction } from '../shared/types'; import * as testDb from '../shared/testDb'; -let app: express.Application; -let globalOwnerRole: Role; let globalMemberRole: Role; let credentialOwnerRole: Role; +let owner: User; +let member: User; +let authOwnerAgent: SuperAgentTest; +let authMemberAgent: SuperAgentTest; let saveCredential: SaveCredentialFunction; beforeAll(async () => { - app = await utils.initTestServer({ + const app = await utils.initTestServer({ endpointGroups: ['publicApi'], applyAuth: false, enablePublicAPI: true, @@ -26,334 +27,265 @@ beforeAll(async () => { utils.initConfigFile(); - const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] = + const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] = await testDb.getAllRoles(); - globalOwnerRole = fetchedGlobalOwnerRole; globalMemberRole = fetchedGlobalMemberRole; credentialOwnerRole = fetchedCredentialOwnerRole; + owner = await testDb.addApiKey(await testDb.createUserShell(globalOwnerRole)); + member = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() }); + + authOwnerAgent = utils.createAgent(app, { + apiPath: 'public', + version: 1, + auth: true, + user: owner, + }); + authMemberAgent = utils.createAgent(app, { + apiPath: 'public', + version: 1, + auth: true, + user: member, + }); + saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole); utils.initCredentialsTypes(); }); beforeEach(async () => { - await testDb.truncate(['User', 'SharedCredentials', 'Credentials']); + await testDb.truncate(['SharedCredentials', 'Credentials']); }); afterAll(async () => { await testDb.terminate(); }); -test('POST /credentials should create credentials', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); +describe('POST /credentials', () => { + test('should create credentials', async () => { + const payload = { + name: 'test credential', + type: 'githubApi', + data: { + accessToken: 'abcdefghijklmnopqrstuvwxyz', + user: 'test', + server: 'testServer', + }, + }; - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: ownerShell, - }); - const payload = { - name: 'test credential', - type: 'githubApi', - data: { - accessToken: 'abcdefghijklmnopqrstuvwxyz', - user: 'test', - server: 'testServer', - }, - }; + const response = await authOwnerAgent.post('/credentials').send(payload); - const response = await authOwnerAgent.post('/credentials').send(payload); + expect(response.statusCode).toBe(200); - expect(response.statusCode).toBe(200); + const { id, name, type } = response.body; - const { id, name, type } = response.body; + expect(name).toBe(payload.name); + expect(type).toBe(payload.type); - expect(name).toBe(payload.name); - expect(type).toBe(payload.type); + const credential = await Db.collections.Credentials.findOneByOrFail({ id }); - const credential = await Db.collections.Credentials.findOneByOrFail({ id }); + expect(credential.name).toBe(payload.name); + expect(credential.type).toBe(payload.type); + expect(credential.data).not.toBe(payload.data); - expect(credential.name).toBe(payload.name); - expect(credential.type).toBe(payload.type); - expect(credential.data).not.toBe(payload.data); + const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ + relations: ['user', 'credentials', 'role'], + where: { credentialsId: credential.id, userId: owner.id }, + }); - const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ - relations: ['user', 'credentials', 'role'], - where: { credentialsId: credential.id, userId: ownerShell.id }, + expect(sharedCredential.role).toEqual(credentialOwnerRole); + expect(sharedCredential.credentials.name).toBe(payload.name); }); - expect(sharedCredential.role).toEqual(credentialOwnerRole); - expect(sharedCredential.credentials.name).toBe(payload.name); + test('should fail with invalid inputs', async () => { + await Promise.all( + INVALID_PAYLOADS.map(async (invalidPayload) => { + const response = await authOwnerAgent.post('/credentials').send(invalidPayload); + expect(response.statusCode === 400 || response.statusCode === 415).toBe(true); + }), + ); + }); + + test('should fail with missing encryption key', async () => { + const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); + mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); + + const response = await authOwnerAgent.post('/credentials').send(credentialPayload()); + + expect(response.statusCode).toBe(500); + + mock.mockRestore(); + }); }); -test('POST /credentials should fail with invalid inputs', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); +describe('DELETE /credentials/:id', () => { + test('should delete owned cred for owner', async () => { + const savedCredential = await saveCredential(dbCredential(), { user: owner }); - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: ownerShell, + const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); + + expect(response.statusCode).toBe(200); + + const { name, type } = response.body; + + expect(name).toBe(savedCredential.name); + expect(type).toBe(savedCredential.type); + + const deletedCredential = await Db.collections.Credentials.findOneBy({ + id: savedCredential.id, + }); + + expect(deletedCredential).toBeNull(); // deleted + + const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); + + expect(deletedSharedCredential).toBeNull(); // deleted }); - await Promise.all( - INVALID_PAYLOADS.map(async (invalidPayload) => { - const response = await authOwnerAgent.post('/credentials').send(invalidPayload); - expect(response.statusCode === 400 || response.statusCode === 415).toBe(true); - }), - ); + test('should delete non-owned cred for owner', async () => { + const savedCredential = await saveCredential(dbCredential(), { user: member }); + + const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); + + expect(response.statusCode).toBe(200); + + const deletedCredential = await Db.collections.Credentials.findOneBy({ + id: savedCredential.id, + }); + + expect(deletedCredential).toBeNull(); // deleted + + const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); + + expect(deletedSharedCredential).toBeNull(); // deleted + }); + + test('should delete owned cred for member', async () => { + const savedCredential = await saveCredential(dbCredential(), { user: member }); + + const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); + + expect(response.statusCode).toBe(200); + + const { name, type } = response.body; + + expect(name).toBe(savedCredential.name); + expect(type).toBe(savedCredential.type); + + const deletedCredential = await Db.collections.Credentials.findOneBy({ + id: savedCredential.id, + }); + + expect(deletedCredential).toBeNull(); // deleted + + const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); + + expect(deletedSharedCredential).toBeNull(); // deleted + }); + + test('should delete owned cred for member but leave others untouched', async () => { + const anotherMember = await testDb.createUser({ + globalRole: globalMemberRole, + apiKey: randomApiKey(), + }); + + const savedCredential = await saveCredential(dbCredential(), { user: member }); + const notToBeChangedCredential = await saveCredential(dbCredential(), { user: member }); + const notToBeChangedCredential2 = await saveCredential(dbCredential(), { + user: anotherMember, + }); + + const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); + + expect(response.statusCode).toBe(200); + + const { name, type } = response.body; + + expect(name).toBe(savedCredential.name); + expect(type).toBe(savedCredential.type); + + const deletedCredential = await Db.collections.Credentials.findOneBy({ + id: savedCredential.id, + }); + + expect(deletedCredential).toBeNull(); // deleted + + const deletedSharedCredential = await Db.collections.SharedCredentials.findOne({ + where: { + credentialsId: savedCredential.id, + }, + }); + + expect(deletedSharedCredential).toBeNull(); // deleted + + await Promise.all( + [notToBeChangedCredential, notToBeChangedCredential2].map(async (credential) => { + const untouchedCredential = await Db.collections.Credentials.findOneBy({ + id: credential.id, + }); + + expect(untouchedCredential).toEqual(credential); // not deleted + + const untouchedSharedCredential = await Db.collections.SharedCredentials.findOne({ + where: { + credentialsId: credential.id, + }, + }); + + expect(untouchedSharedCredential).toBeDefined(); // not deleted + }), + ); + }); + + test('should not delete non-owned cred for member', async () => { + const savedCredential = await saveCredential(dbCredential(), { user: owner }); + + const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); + + expect(response.statusCode).toBe(404); + + const shellCredential = await Db.collections.Credentials.findOneBy({ + id: savedCredential.id, + }); + + expect(shellCredential).toBeDefined(); // not deleted + + const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); + + expect(deletedSharedCredential).toBeDefined(); // not deleted + }); + + test('should fail if cred not found', async () => { + const response = await authOwnerAgent.delete('/credentials/123'); + + expect(response.statusCode).toBe(404); + }); }); -test('POST /credentials should fail with missing encryption key', async () => { - const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); - mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); +describe('GET /credentials/schema/:credentialType', () => { + test('should fail due to not found type', async () => { + const response = await authOwnerAgent.get('/credentials/schema/testing'); - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: ownerShell, + expect(response.statusCode).toBe(404); }); - const response = await authOwnerAgent.post('/credentials').send(credentialPayload()); + test('should retrieve credential type', async () => { + const response = await authOwnerAgent.get('/credentials/schema/githubApi'); - expect(response.statusCode).toBe(500); + const { additionalProperties, type, properties, required } = response.body; - mock.mockRestore(); -}); - -test('DELETE /credentials/:id should delete owned cred for owner', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: ownerShell, + expect(additionalProperties).toBe(false); + expect(type).toBe('object'); + expect(properties.server).toBeDefined(); + expect(properties.server.type).toBe('string'); + expect(properties.user.type).toBeDefined(); + expect(properties.user.type).toBe('string'); + expect(properties.accessToken.type).toBeDefined(); + expect(properties.accessToken.type).toBe('string'); + expect(required).toEqual(expect.arrayContaining(['server', 'user', 'accessToken'])); + expect(response.statusCode).toBe(200); }); - - const savedCredential = await saveCredential(dbCredential(), { user: ownerShell }); - - const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(200); - - const { name, type } = response.body; - - expect(name).toBe(savedCredential.name); - expect(type).toBe(savedCredential.type); - - const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(deletedCredential).toBeNull(); // deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - - expect(deletedSharedCredential).toBeNull(); // deleted -}); - -test('DELETE /credentials/:id should delete non-owned cred for owner', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: ownerShell, - }); - - const member = await testDb.createUser({ globalRole: globalMemberRole }); - - const savedCredential = await saveCredential(dbCredential(), { user: member }); - - const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(200); - - const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(deletedCredential).toBeNull(); // deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - - expect(deletedSharedCredential).toBeNull(); // deleted -}); - -test('DELETE /credentials/:id should delete owned cred for member', async () => { - const member = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() }); - - const authMemberAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: member, - }); - - const savedCredential = await saveCredential(dbCredential(), { user: member }); - - const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(200); - - const { name, type } = response.body; - - expect(name).toBe(savedCredential.name); - expect(type).toBe(savedCredential.type); - - const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(deletedCredential).toBeNull(); // deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - - expect(deletedSharedCredential).toBeNull(); // deleted -}); - -test('DELETE /credentials/:id should delete owned cred for member but leave others untouched', async () => { - const member1 = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() }); - const member2 = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() }); - - const savedCredential = await saveCredential(dbCredential(), { user: member1 }); - const notToBeChangedCredential = await saveCredential(dbCredential(), { user: member1 }); - const notToBeChangedCredential2 = await saveCredential(dbCredential(), { user: member2 }); - - const authMemberAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: member1, - }); - - const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(200); - - const { name, type } = response.body; - - expect(name).toBe(savedCredential.name); - expect(type).toBe(savedCredential.type); - - const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(deletedCredential).toBeNull(); // deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOne({ - where: { - credentialsId: savedCredential.id, - }, - }); - - expect(deletedSharedCredential).toBeNull(); // deleted - - await Promise.all( - [notToBeChangedCredential, notToBeChangedCredential2].map(async (credential) => { - const untouchedCredential = await Db.collections.Credentials.findOneBy({ id: credential.id }); - - expect(untouchedCredential).toEqual(credential); // not deleted - - const untouchedSharedCredential = await Db.collections.SharedCredentials.findOne({ - where: { - credentialsId: credential.id, - }, - }); - - expect(untouchedSharedCredential).toBeDefined(); // not deleted - }), - ); -}); - -test('DELETE /credentials/:id should not delete non-owned cred for member', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const member = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() }); - - const authMemberAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: member, - }); - const savedCredential = await saveCredential(dbCredential(), { user: ownerShell }); - - const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`); - - expect(response.statusCode).toBe(404); - - const shellCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - - expect(shellCredential).toBeDefined(); // not deleted - - const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({}); - - expect(deletedSharedCredential).toBeDefined(); // not deleted -}); - -test('DELETE /credentials/:id should fail if cred not found', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: ownerShell, - }); - - const response = await authOwnerAgent.delete('/credentials/123'); - - expect(response.statusCode).toBe(404); -}); - -test('GET /credentials/schema/:credentialType should fail due to not found type', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: ownerShell, - }); - - const response = await authOwnerAgent.get('/credentials/schema/testing'); - - expect(response.statusCode).toBe(404); -}); - -test('GET /credentials/schema/:credentialType should retrieve credential type', async () => { - let ownerShell = await testDb.createUserShell(globalOwnerRole); - ownerShell = await testDb.addApiKey(ownerShell); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - version: 1, - auth: true, - user: ownerShell, - }); - - const response = await authOwnerAgent.get('/credentials/schema/githubApi'); - - const { additionalProperties, type, properties, required } = response.body; - - expect(additionalProperties).toBe(false); - expect(type).toBe('object'); - expect(properties.server).toBeDefined(); - expect(properties.server.type).toBe('string'); - expect(properties.user.type).toBeDefined(); - expect(properties.user.type).toBe('string'); - expect(properties.accessToken.type).toBeDefined(); - expect(properties.accessToken.type).toBe('string'); - expect(required).toEqual(expect.arrayContaining(['server', 'user', 'accessToken'])); - expect(response.statusCode).toBe(200); }); const credentialPayload = (): CredentialPayload => ({ diff --git a/packages/cli/test/integration/publicApi/executions.test.ts b/packages/cli/test/integration/publicApi/executions.test.ts index c377c870af..4c70977dce 100644 --- a/packages/cli/test/integration/publicApi/executions.test.ts +++ b/packages/cli/test/integration/publicApi/executions.test.ts @@ -1,15 +1,16 @@ -import express from 'express'; - +import type { Application } from 'express'; +import type { SuperAgentTest } from 'supertest'; import config from '@/config'; -import { Role } from '@db/entities/Role'; -import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; +import type { User } from '@db/entities/User'; +import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { randomApiKey } from '../shared/random'; import * as utils from '../shared/utils'; import * as testDb from '../shared/testDb'; -let app: express.Application; -let globalOwnerRole: Role; +let app: Application; +let owner: User; +let authOwnerAgent: SuperAgentTest; let workflowRunner: ActiveWorkflowRunner; beforeAll(async () => { @@ -19,7 +20,8 @@ beforeAll(async () => { enablePublicAPI: true, }); - globalOwnerRole = await testDb.getGlobalOwnerRole(); + const globalOwnerRole = await testDb.getGlobalOwnerRole(); + owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); await utils.initBinaryManager(); await utils.initNodeTypes(); @@ -31,13 +33,19 @@ beforeEach(async () => { await testDb.truncate([ 'SharedCredentials', 'SharedWorkflow', - 'User', 'Workflow', 'Credentials', 'Execution', 'Settings', ]); + authOwnerAgent = utils.createAgent(app, { + apiPath: 'public', + auth: true, + user: owner, + version: 1, + }); + config.set('userManagement.disabled', false); config.set('userManagement.isInstanceOwnerSetUp', true); }); @@ -50,270 +58,27 @@ afterAll(async () => { await testDb.terminate(); }); -test('GET /executions/:id should fail due to missing API Key', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); +const testWithAPIKey = + (method: 'get' | 'post' | 'put' | 'delete', url: string, apiKey: string | null) => async () => { + authOwnerAgent.set({ 'X-N8N-API-KEY': apiKey }); + const response = await authOwnerAgent[method](url); + expect(response.statusCode).toBe(401); + }; - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); +describe('GET /executions/:id', () => { + test('should fail due to missing API Key', testWithAPIKey('get', '/executions/1', null)); - const response = await authOwnerAgent.get('/executions/1'); + test('should fail due to invalid API Key', testWithAPIKey('get', '/executions/1', 'abcXYZ')); - expect(response.statusCode).toBe(401); -}); + test('should get an execution', async () => { + const workflow = await testDb.createWorkflow({}, owner); -test('GET /executions/:id should fail due to invalid API Key', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - owner.apiKey = 'abcXYZ'; + const execution = await testDb.createSuccessfulExecution(workflow); - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); + const response = await authOwnerAgent.get(`/executions/${execution.id}`); - const response = await authOwnerAgent.get('/executions/1'); + expect(response.statusCode).toBe(200); - expect(response.statusCode).toBe(401); -}); - -test('GET /executions/:id should get an execution', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const workflow = await testDb.createWorkflow({}, owner); - - const execution = await testDb.createSuccessfulExecution(workflow); - - const response = await authOwnerAgent.get(`/executions/${execution.id}`); - - expect(response.statusCode).toBe(200); - - const { - id, - finished, - mode, - retryOf, - retrySuccessId, - startedAt, - stoppedAt, - workflowId, - waitTill, - } = response.body; - - expect(id).toBeDefined(); - expect(finished).toBe(true); - expect(mode).toEqual(execution.mode); - expect(retrySuccessId).toBeNull(); - expect(retryOf).toBeNull(); - expect(startedAt).not.toBeNull(); - expect(stoppedAt).not.toBeNull(); - expect(workflowId).toBe(execution.workflowId); - expect(waitTill).toBeNull(); -}); - -test('DELETE /executions/:id should fail due to missing API Key', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const response = await authOwnerAgent.delete('/executions/1'); - - expect(response.statusCode).toBe(401); -}); - -test('DELETE /executions/:id should fail due to invalid API Key', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - owner.apiKey = 'abcXYZ'; - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const response = await authOwnerAgent.delete('/executions/1'); - - expect(response.statusCode).toBe(401); -}); - -test('DELETE /executions/:id should delete an execution', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const workflow = await testDb.createWorkflow({}, owner); - - const execution = await testDb.createSuccessfulExecution(workflow); - - const response = await authOwnerAgent.delete(`/executions/${execution.id}`); - - expect(response.statusCode).toBe(200); - - const { - id, - finished, - mode, - retryOf, - retrySuccessId, - startedAt, - stoppedAt, - workflowId, - waitTill, - } = response.body; - - expect(id).toBeDefined(); - expect(finished).toBe(true); - expect(mode).toEqual(execution.mode); - expect(retrySuccessId).toBeNull(); - expect(retryOf).toBeNull(); - expect(startedAt).not.toBeNull(); - expect(stoppedAt).not.toBeNull(); - expect(workflowId).toBe(execution.workflowId); - expect(waitTill).toBeNull(); -}); - -test('GET /executions should fail due to missing API Key', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const response = await authOwnerAgent.get('/executions'); - - expect(response.statusCode).toBe(401); -}); - -test('GET /executions should fail due to invalid API Key', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - owner.apiKey = 'abcXYZ'; - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const response = await authOwnerAgent.get('/executions'); - - expect(response.statusCode).toBe(401); -}); - -test('GET /executions should retrieve all successful executions', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const workflow = await testDb.createWorkflow({}, owner); - - const successfullExecution = await testDb.createSuccessfulExecution(workflow); - - await testDb.createErrorExecution(workflow); - - const response = await authOwnerAgent.get(`/executions`).query({ - status: 'success', - }); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(1); - expect(response.body.nextCursor).toBe(null); - - const { - id, - finished, - mode, - retryOf, - retrySuccessId, - startedAt, - stoppedAt, - workflowId, - waitTill, - } = response.body.data[0]; - - expect(id).toBeDefined(); - expect(finished).toBe(true); - expect(mode).toEqual(successfullExecution.mode); - expect(retrySuccessId).toBeNull(); - expect(retryOf).toBeNull(); - expect(startedAt).not.toBeNull(); - expect(stoppedAt).not.toBeNull(); - expect(workflowId).toBe(successfullExecution.workflowId); - expect(waitTill).toBeNull(); -}); - -// failing on Postgres and MySQL - ref: https://github.com/n8n-io/n8n/pull/3834 -test.skip('GET /executions should paginate two executions', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const workflow = await testDb.createWorkflow({}, owner); - - const firstSuccessfulExecution = await testDb.createSuccessfulExecution(workflow); - - const secondSuccessfulExecution = await testDb.createSuccessfulExecution(workflow); - - await testDb.createErrorExecution(workflow); - - const firstExecutionResponse = await authOwnerAgent.get(`/executions`).query({ - status: 'success', - limit: 1, - }); - - expect(firstExecutionResponse.statusCode).toBe(200); - expect(firstExecutionResponse.body.data.length).toBe(1); - expect(firstExecutionResponse.body.nextCursor).toBeDefined(); - - const secondExecutionResponse = await authOwnerAgent.get(`/executions`).query({ - status: 'success', - limit: 1, - cursor: firstExecutionResponse.body.nextCursor, - }); - - expect(secondExecutionResponse.statusCode).toBe(200); - expect(secondExecutionResponse.body.data.length).toBe(1); - expect(secondExecutionResponse.body.nextCursor).toBeNull(); - - const successfulExecutions = [firstSuccessfulExecution, secondSuccessfulExecution]; - const executions = [...firstExecutionResponse.body.data, ...secondExecutionResponse.body.data]; - - for (let i = 0; i < executions.length; i++) { const { id, finished, @@ -324,146 +89,33 @@ test.skip('GET /executions should paginate two executions', async () => { stoppedAt, workflowId, waitTill, - } = executions[i]; + } = response.body; expect(id).toBeDefined(); expect(finished).toBe(true); - expect(mode).toEqual(successfulExecutions[i].mode); + expect(mode).toEqual(execution.mode); expect(retrySuccessId).toBeNull(); expect(retryOf).toBeNull(); expect(startedAt).not.toBeNull(); expect(stoppedAt).not.toBeNull(); - expect(workflowId).toBe(successfulExecutions[i].workflowId); + expect(workflowId).toBe(execution.workflowId); expect(waitTill).toBeNull(); - } + }); }); -test('GET /executions should retrieve all error executions', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); +describe('DELETE /executions/:id', () => { + test('should fail due to missing API Key', testWithAPIKey('delete', '/executions/1', null)); - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); + test('should fail due to invalid API Key', testWithAPIKey('delete', '/executions/1', 'abcXYZ')); - const workflow = await testDb.createWorkflow({}, owner); + test('should delete an execution', async () => { + const workflow = await testDb.createWorkflow({}, owner); + const execution = await testDb.createSuccessfulExecution(workflow); - await testDb.createSuccessfulExecution(workflow); + const response = await authOwnerAgent.delete(`/executions/${execution.id}`); - const errorExecution = await testDb.createErrorExecution(workflow); + expect(response.statusCode).toBe(200); - const response = await authOwnerAgent.get(`/executions`).query({ - status: 'error', - }); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(1); - expect(response.body.nextCursor).toBe(null); - - const { - id, - finished, - mode, - retryOf, - retrySuccessId, - startedAt, - stoppedAt, - workflowId, - waitTill, - } = response.body.data[0]; - - expect(id).toBeDefined(); - expect(finished).toBe(false); - expect(mode).toEqual(errorExecution.mode); - expect(retrySuccessId).toBeNull(); - expect(retryOf).toBeNull(); - expect(startedAt).not.toBeNull(); - expect(stoppedAt).not.toBeNull(); - expect(workflowId).toBe(errorExecution.workflowId); - expect(waitTill).toBeNull(); -}); - -test('GET /executions should return all waiting executions', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const workflow = await testDb.createWorkflow({}, owner); - - await testDb.createSuccessfulExecution(workflow); - - await testDb.createErrorExecution(workflow); - - const waitingExecution = await testDb.createWaitingExecution(workflow); - - const response = await authOwnerAgent.get(`/executions`).query({ - status: 'waiting', - }); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(1); - expect(response.body.nextCursor).toBe(null); - - const { - id, - finished, - mode, - retryOf, - retrySuccessId, - startedAt, - stoppedAt, - workflowId, - waitTill, - } = response.body.data[0]; - - expect(id).toBeDefined(); - expect(finished).toBe(false); - expect(mode).toEqual(waitingExecution.mode); - expect(retrySuccessId).toBeNull(); - expect(retryOf).toBeNull(); - expect(startedAt).not.toBeNull(); - expect(stoppedAt).not.toBeNull(); - expect(workflowId).toBe(waitingExecution.workflowId); - expect(new Date(waitTill).getTime()).toBeGreaterThan(Date.now() - 1000); -}); - -test('GET /executions should retrieve all executions of specific workflow', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - - const authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); - - const [workflow, workflow2] = await testDb.createManyWorkflows(2, {}, owner); - - const savedExecutions = await testDb.createManyExecutions( - 2, - workflow, - // @ts-ignore - testDb.createSuccessfulExecution, - ); - // @ts-ignore - await testDb.createManyExecutions(2, workflow2, testDb.createSuccessfulExecution); - - const response = await authOwnerAgent.get(`/executions`).query({ - workflowId: workflow.id, - }); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(2); - expect(response.body.nextCursor).toBe(null); - - for (const execution of response.body.data) { const { id, finished, @@ -474,16 +126,240 @@ test('GET /executions should retrieve all executions of specific workflow', asyn stoppedAt, workflowId, waitTill, - } = execution; + } = response.body; - expect(savedExecutions.some((exec) => exec.id === id)).toBe(true); + expect(id).toBeDefined(); expect(finished).toBe(true); - expect(mode).toBeDefined(); + expect(mode).toEqual(execution.mode); expect(retrySuccessId).toBeNull(); expect(retryOf).toBeNull(); expect(startedAt).not.toBeNull(); expect(stoppedAt).not.toBeNull(); - expect(workflowId).toBe(workflow.id); + expect(workflowId).toBe(execution.workflowId); expect(waitTill).toBeNull(); - } + }); +}); + +describe('GET /executions', () => { + test('should fail due to missing API Key', testWithAPIKey('get', '/executions', null)); + + test('should fail due to invalid API Key', testWithAPIKey('get', '/executions', 'abcXYZ')); + + test('should retrieve all successful executions', async () => { + const workflow = await testDb.createWorkflow({}, owner); + + const successfulExecution = await testDb.createSuccessfulExecution(workflow); + + await testDb.createErrorExecution(workflow); + + const response = await authOwnerAgent.get(`/executions`).query({ + status: 'success', + }); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(1); + expect(response.body.nextCursor).toBe(null); + + const { + id, + finished, + mode, + retryOf, + retrySuccessId, + startedAt, + stoppedAt, + workflowId, + waitTill, + } = response.body.data[0]; + + expect(id).toBeDefined(); + expect(finished).toBe(true); + expect(mode).toEqual(successfulExecution.mode); + expect(retrySuccessId).toBeNull(); + expect(retryOf).toBeNull(); + expect(startedAt).not.toBeNull(); + expect(stoppedAt).not.toBeNull(); + expect(workflowId).toBe(successfulExecution.workflowId); + expect(waitTill).toBeNull(); + }); + + // failing on Postgres and MySQL - ref: https://github.com/n8n-io/n8n/pull/3834 + test.skip('should paginate two executions', async () => { + const workflow = await testDb.createWorkflow({}, owner); + + const firstSuccessfulExecution = await testDb.createSuccessfulExecution(workflow); + + const secondSuccessfulExecution = await testDb.createSuccessfulExecution(workflow); + + await testDb.createErrorExecution(workflow); + + const firstExecutionResponse = await authOwnerAgent.get(`/executions`).query({ + status: 'success', + limit: 1, + }); + + expect(firstExecutionResponse.statusCode).toBe(200); + expect(firstExecutionResponse.body.data.length).toBe(1); + expect(firstExecutionResponse.body.nextCursor).toBeDefined(); + + const secondExecutionResponse = await authOwnerAgent.get(`/executions`).query({ + status: 'success', + limit: 1, + cursor: firstExecutionResponse.body.nextCursor, + }); + + expect(secondExecutionResponse.statusCode).toBe(200); + expect(secondExecutionResponse.body.data.length).toBe(1); + expect(secondExecutionResponse.body.nextCursor).toBeNull(); + + const successfulExecutions = [firstSuccessfulExecution, secondSuccessfulExecution]; + const executions = [...firstExecutionResponse.body.data, ...secondExecutionResponse.body.data]; + + for (let i = 0; i < executions.length; i++) { + const { + id, + finished, + mode, + retryOf, + retrySuccessId, + startedAt, + stoppedAt, + workflowId, + waitTill, + } = executions[i]; + + expect(id).toBeDefined(); + expect(finished).toBe(true); + expect(mode).toEqual(successfulExecutions[i].mode); + expect(retrySuccessId).toBeNull(); + expect(retryOf).toBeNull(); + expect(startedAt).not.toBeNull(); + expect(stoppedAt).not.toBeNull(); + expect(workflowId).toBe(successfulExecutions[i].workflowId); + expect(waitTill).toBeNull(); + } + }); + + test('should retrieve all error executions', async () => { + const workflow = await testDb.createWorkflow({}, owner); + + await testDb.createSuccessfulExecution(workflow); + + const errorExecution = await testDb.createErrorExecution(workflow); + + const response = await authOwnerAgent.get(`/executions`).query({ + status: 'error', + }); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(1); + expect(response.body.nextCursor).toBe(null); + + const { + id, + finished, + mode, + retryOf, + retrySuccessId, + startedAt, + stoppedAt, + workflowId, + waitTill, + } = response.body.data[0]; + + expect(id).toBeDefined(); + expect(finished).toBe(false); + expect(mode).toEqual(errorExecution.mode); + expect(retrySuccessId).toBeNull(); + expect(retryOf).toBeNull(); + expect(startedAt).not.toBeNull(); + expect(stoppedAt).not.toBeNull(); + expect(workflowId).toBe(errorExecution.workflowId); + expect(waitTill).toBeNull(); + }); + + test('should return all waiting executions', async () => { + const workflow = await testDb.createWorkflow({}, owner); + + await testDb.createSuccessfulExecution(workflow); + + await testDb.createErrorExecution(workflow); + + const waitingExecution = await testDb.createWaitingExecution(workflow); + + const response = await authOwnerAgent.get(`/executions`).query({ + status: 'waiting', + }); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(1); + expect(response.body.nextCursor).toBe(null); + + const { + id, + finished, + mode, + retryOf, + retrySuccessId, + startedAt, + stoppedAt, + workflowId, + waitTill, + } = response.body.data[0]; + + expect(id).toBeDefined(); + expect(finished).toBe(false); + expect(mode).toEqual(waitingExecution.mode); + expect(retrySuccessId).toBeNull(); + expect(retryOf).toBeNull(); + expect(startedAt).not.toBeNull(); + expect(stoppedAt).not.toBeNull(); + expect(workflowId).toBe(waitingExecution.workflowId); + expect(new Date(waitTill).getTime()).toBeGreaterThan(Date.now() - 1000); + }); + + test('should retrieve all executions of specific workflow', async () => { + const [workflow, workflow2] = await testDb.createManyWorkflows(2, {}, owner); + + const savedExecutions = await testDb.createManyExecutions( + 2, + workflow, + // @ts-ignore + testDb.createSuccessfulExecution, + ); + // @ts-ignore + await testDb.createManyExecutions(2, workflow2, testDb.createSuccessfulExecution); + + const response = await authOwnerAgent.get(`/executions`).query({ + workflowId: workflow.id, + }); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(2); + expect(response.body.nextCursor).toBe(null); + + for (const execution of response.body.data) { + const { + id, + finished, + mode, + retryOf, + retrySuccessId, + startedAt, + stoppedAt, + workflowId, + waitTill, + } = execution; + + expect(savedExecutions.some((exec) => exec.id === id)).toBe(true); + expect(finished).toBe(true); + expect(mode).toBeDefined(); + expect(retrySuccessId).toBeNull(); + expect(retryOf).toBeNull(); + expect(startedAt).not.toBeNull(); + expect(stoppedAt).not.toBeNull(); + expect(workflowId).toBe(workflow.id); + expect(waitTill).toBeNull(); + } + }); }); diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 4199b8276c..02c76b616c 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -2,337 +2,104 @@ import type { Application } from 'express'; import type { SuperAgentTest } from 'supertest'; import * as Db from '@/Db'; import config from '@/config'; -import { Role } from '@db/entities/Role'; -import { TagEntity } from '@db/entities/TagEntity'; +import type { Role } from '@db/entities/Role'; +import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; -import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; +import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { randomApiKey } from '../shared/random'; import * as utils from '../shared/utils'; import * as testDb from '../shared/testDb'; -describe('Workflows Public API', () => { - let app: Application; - let globalOwnerRole: Role; - let globalMemberRole: Role; - let workflowOwnerRole: Role; - let owner: User; - let member: User; - let authOwnerAgent: SuperAgentTest; - let authMemberAgent: SuperAgentTest; - let workflowRunner: ActiveWorkflowRunner; +let app: Application; +let workflowOwnerRole: Role; +let owner: User; +let member: User; +let authOwnerAgent: SuperAgentTest; +let authMemberAgent: SuperAgentTest; +let workflowRunner: ActiveWorkflowRunner; - beforeAll(async () => { - app = await utils.initTestServer({ - endpointGroups: ['publicApi'], - applyAuth: false, - enablePublicAPI: true, - }); - - const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole, fetchedWorkflowOwnerRole] = - await testDb.getAllRoles(); - - globalOwnerRole = fetchedGlobalOwnerRole; - globalMemberRole = fetchedGlobalMemberRole; - workflowOwnerRole = fetchedWorkflowOwnerRole; - - owner = await testDb.createUser({ - globalRole: globalOwnerRole, - apiKey: randomApiKey(), - }); - - member = await testDb.createUser({ - globalRole: globalMemberRole, - apiKey: randomApiKey(), - }); - - utils.initConfigFile(); - await utils.initNodeTypes(); - workflowRunner = await utils.initActiveWorkflowRunner(); +beforeAll(async () => { + app = await utils.initTestServer({ + endpointGroups: ['publicApi'], + applyAuth: false, + enablePublicAPI: true, }); - beforeEach(async () => { - await testDb.truncate([ - 'SharedCredentials', - 'SharedWorkflow', - 'Tag', - 'Workflow', - 'Credentials', + const [globalOwnerRole, globalMemberRole, fetchedWorkflowOwnerRole] = await testDb.getAllRoles(); + + workflowOwnerRole = fetchedWorkflowOwnerRole; + + owner = await testDb.createUser({ + globalRole: globalOwnerRole, + apiKey: randomApiKey(), + }); + + member = await testDb.createUser({ + globalRole: globalMemberRole, + apiKey: randomApiKey(), + }); + + utils.initConfigFile(); + await utils.initNodeTypes(); + workflowRunner = await utils.initActiveWorkflowRunner(); +}); + +beforeEach(async () => { + await testDb.truncate(['SharedCredentials', 'SharedWorkflow', 'Tag', 'Workflow', 'Credentials']); + + authOwnerAgent = utils.createAgent(app, { + apiPath: 'public', + auth: true, + user: owner, + version: 1, + }); + + authMemberAgent = utils.createAgent(app, { + apiPath: 'public', + auth: true, + user: member, + version: 1, + }); + + config.set('userManagement.disabled', false); + config.set('userManagement.isInstanceOwnerSetUp', true); +}); + +afterEach(async () => { + await workflowRunner?.removeAll(); +}); + +afterAll(async () => { + await testDb.terminate(); +}); + +const testWithAPIKey = + (method: 'get' | 'post' | 'put' | 'delete', url: string, apiKey: string | null) => async () => { + authOwnerAgent.set({ 'X-N8N-API-KEY': apiKey }); + const response = await authOwnerAgent[method](url); + expect(response.statusCode).toBe(401); + }; + +describe('GET /workflows', () => { + test('should fail due to missing API Key', testWithAPIKey('get', '/workflows', null)); + + test('should fail due to invalid API Key', testWithAPIKey('get', '/workflows', 'abcXYZ')); + + test('should return all owned workflows', async () => { + await Promise.all([ + testDb.createWorkflow({}, member), + testDb.createWorkflow({}, member), + testDb.createWorkflow({}, member), ]); - authOwnerAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: owner, - version: 1, - }); + const response = await authMemberAgent.get('/workflows'); - authMemberAgent = utils.createAgent(app, { - apiPath: 'public', - auth: true, - user: member, - version: 1, - }); - - config.set('userManagement.disabled', false); - config.set('userManagement.isInstanceOwnerSetUp', true); - }); - - afterEach(async () => { - await workflowRunner?.removeAll(); - }); - - afterAll(async () => { - await testDb.terminate(); - }); - - const testWithAPIKey = - (method: 'get' | 'post' | 'put' | 'delete', url: string, apiKey: string | null) => async () => { - authOwnerAgent.set({ 'X-N8N-API-KEY': apiKey }); - const response = await authOwnerAgent[method](url); - expect(response.statusCode).toBe(401); - }; - - describe('GET /workflows', () => { - test('should fail due to missing API Key', testWithAPIKey('get', '/workflows', null)); - - test('should fail due to invalid API Key', testWithAPIKey('get', '/workflows', 'abcXYZ')); - - test('should return all owned workflows', async () => { - await Promise.all([ - testDb.createWorkflow({}, member), - testDb.createWorkflow({}, member), - testDb.createWorkflow({}, member), - ]); - - const response = await authMemberAgent.get('/workflows'); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(3); - expect(response.body.nextCursor).toBeNull(); - - for (const workflow of response.body.data) { - const { - id, - connections, - active, - staticData, - nodes, - settings, - name, - createdAt, - updatedAt, - tags, - } = workflow; - - expect(id).toBeDefined(); - expect(name).toBeDefined(); - expect(connections).toBeDefined(); - expect(active).toBe(false); - expect(staticData).toBeDefined(); - expect(nodes).toBeDefined(); - expect(tags).toBeDefined(); - expect(settings).toBeDefined(); - expect(createdAt).toBeDefined(); - expect(updatedAt).toBeDefined(); - } - }); - - test('should return all owned workflows with pagination', async () => { - await Promise.all([ - testDb.createWorkflow({}, member), - testDb.createWorkflow({}, member), - testDb.createWorkflow({}, member), - ]); - - const response = await authMemberAgent.get('/workflows?limit=1'); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(1); - expect(response.body.nextCursor).not.toBeNull(); - - const response2 = await authMemberAgent.get( - `/workflows?limit=1&cursor=${response.body.nextCursor}`, - ); - - expect(response2.statusCode).toBe(200); - expect(response2.body.data.length).toBe(1); - expect(response2.body.nextCursor).not.toBeNull(); - expect(response2.body.nextCursor).not.toBe(response.body.nextCursor); - - const responses = [...response.body.data, ...response2.body.data]; - - for (const workflow of responses) { - const { - id, - connections, - active, - staticData, - nodes, - settings, - name, - createdAt, - updatedAt, - tags, - } = workflow; - - expect(id).toBeDefined(); - expect(name).toBeDefined(); - expect(connections).toBeDefined(); - expect(active).toBe(false); - expect(staticData).toBeDefined(); - expect(nodes).toBeDefined(); - expect(tags).toBeDefined(); - expect(settings).toBeDefined(); - expect(createdAt).toBeDefined(); - expect(updatedAt).toBeDefined(); - } - - // check that we really received a different result - expect(Number(response.body.data[0].id)).toBeLessThan(Number(response2.body.data[0].id)); - }); - - test('should return all owned workflows filtered by tag', async () => { - const tag = await testDb.createTag({}); - - const [workflow] = await Promise.all([ - testDb.createWorkflow({ tags: [tag] }, member), - testDb.createWorkflow({}, member), - ]); - - const response = await authMemberAgent.get(`/workflows?tags=${tag.name}`); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(1); - - const { - id, - connections, - active, - staticData, - nodes, - settings, - name, - createdAt, - updatedAt, - tags: wfTags, - } = response.body.data[0]; - - expect(id).toBe(workflow.id); - expect(name).toBeDefined(); - expect(connections).toBeDefined(); - expect(active).toBe(false); - expect(staticData).toBeDefined(); - expect(nodes).toBeDefined(); - expect(settings).toBeDefined(); - expect(createdAt).toBeDefined(); - expect(updatedAt).toBeDefined(); - - expect(wfTags.length).toBe(1); - expect(wfTags[0].id).toBe(tag.id); - }); - - test('should return all owned workflows filtered by tags', async () => { - const tags = await Promise.all([await testDb.createTag({}), await testDb.createTag({})]); - const tagNames = tags.map((tag) => tag.name).join(','); - - const [workflow1, workflow2] = await Promise.all([ - testDb.createWorkflow({ tags }, member), - testDb.createWorkflow({ tags }, member), - testDb.createWorkflow({}, member), - testDb.createWorkflow({ tags: [tags[0]] }, member), - testDb.createWorkflow({ tags: [tags[1]] }, member), - ]); - - const response = await authMemberAgent.get(`/workflows?tags=${tagNames}`); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(2); - - for (const workflow of response.body.data) { - const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = - workflow; - - expect(id).toBeDefined(); - expect([workflow1.id, workflow2.id].includes(id)).toBe(true); - - expect(name).toBeDefined(); - expect(connections).toBeDefined(); - expect(active).toBe(false); - expect(staticData).toBeDefined(); - expect(nodes).toBeDefined(); - expect(settings).toBeDefined(); - expect(createdAt).toBeDefined(); - expect(updatedAt).toBeDefined(); - - expect(workflow.tags.length).toBe(2); - workflow.tags.forEach((tag: TagEntity) => { - expect(tags.some((savedTag) => savedTag.id === tag.id)).toBe(true); - }); - } - }); - - test('should return all workflows for owner', async () => { - await Promise.all([ - testDb.createWorkflow({}, owner), - testDb.createWorkflow({}, member), - testDb.createWorkflow({}, owner), - testDb.createWorkflow({}, member), - testDb.createWorkflow({}, owner), - ]); - - const response = await authOwnerAgent.get('/workflows'); - - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(5); - expect(response.body.nextCursor).toBeNull(); - - for (const workflow of response.body.data) { - const { - id, - connections, - active, - staticData, - nodes, - settings, - name, - createdAt, - updatedAt, - tags, - } = workflow; - - expect(id).toBeDefined(); - expect(name).toBeDefined(); - expect(connections).toBeDefined(); - expect(active).toBe(false); - expect(staticData).toBeDefined(); - expect(nodes).toBeDefined(); - expect(tags).toBeDefined(); - expect(settings).toBeDefined(); - expect(createdAt).toBeDefined(); - expect(updatedAt).toBeDefined(); - } - }); - }); - - describe('GET /workflows/:id', () => { - test('should fail due to missing API Key', testWithAPIKey('get', '/workflows/2', null)); - - test('should fail due to invalid API Key', testWithAPIKey('get', '/workflows/2', 'abcXYZ')); - - test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.get(`/workflows/2`); - expect(response.statusCode).toBe(404); - }); - - test('should retrieve workflow', async () => { - // create and assign workflow to owner - const workflow = await testDb.createWorkflow({}, member); - - const response = await authMemberAgent.get(`/workflows/${workflow.id}`); - - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(3); + expect(response.body.nextCursor).toBeNull(); + for (const workflow of response.body.data) { const { id, connections, @@ -344,581 +111,798 @@ describe('Workflows Public API', () => { createdAt, updatedAt, tags, - } = response.body; - - expect(id).toEqual(workflow.id); - expect(name).toEqual(workflow.name); - expect(connections).toEqual(workflow.connections); - expect(active).toBe(false); - expect(staticData).toEqual(workflow.staticData); - expect(nodes).toEqual(workflow.nodes); - expect(tags).toEqual([]); - expect(settings).toEqual(workflow.settings); - expect(createdAt).toEqual(workflow.createdAt.toISOString()); - expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); - }); - - test('should retrieve non-owned workflow for owner', async () => { - // create and assign workflow to owner - const workflow = await testDb.createWorkflow({}, member); - - const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); - - expect(response.statusCode).toBe(200); - - const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = - response.body; - - expect(id).toEqual(workflow.id); - expect(name).toEqual(workflow.name); - expect(connections).toEqual(workflow.connections); - expect(active).toBe(false); - expect(staticData).toEqual(workflow.staticData); - expect(nodes).toEqual(workflow.nodes); - expect(settings).toEqual(workflow.settings); - expect(createdAt).toEqual(workflow.createdAt.toISOString()); - expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); - }); - }); - - describe('DELETE /workflows/:id', () => { - test('should fail due to missing API Key', testWithAPIKey('delete', '/workflows/2', null)); - - test('should fail due to invalid API Key', testWithAPIKey('delete', '/workflows/2', 'abcXYZ')); - - test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.delete(`/workflows/2`); - expect(response.statusCode).toBe(404); - }); - - test('should delete the workflow', async () => { - // create and assign workflow to owner - const workflow = await testDb.createWorkflow({}, member); - - const response = await authMemberAgent.delete(`/workflows/${workflow.id}`); - - expect(response.statusCode).toBe(200); - - const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = - response.body; - - expect(id).toEqual(workflow.id); - expect(name).toEqual(workflow.name); - expect(connections).toEqual(workflow.connections); - expect(active).toBe(false); - expect(staticData).toEqual(workflow.staticData); - expect(nodes).toEqual(workflow.nodes); - expect(settings).toEqual(workflow.settings); - expect(createdAt).toEqual(workflow.createdAt.toISOString()); - expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); - - // make sure the workflow actually deleted from the db - const sharedWorkflow = await Db.collections.SharedWorkflow.findOneBy({ - workflowId: workflow.id, - }); - - expect(sharedWorkflow).toBeNull(); - }); - - test('should delete non-owned workflow when owner', async () => { - // create and assign workflow to owner - const workflow = await testDb.createWorkflow({}, member); - - const response = await authMemberAgent.delete(`/workflows/${workflow.id}`); - - expect(response.statusCode).toBe(200); - - const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = - response.body; - - expect(id).toEqual(workflow.id); - expect(name).toEqual(workflow.name); - expect(connections).toEqual(workflow.connections); - expect(active).toBe(false); - expect(staticData).toEqual(workflow.staticData); - expect(nodes).toEqual(workflow.nodes); - expect(settings).toEqual(workflow.settings); - expect(createdAt).toEqual(workflow.createdAt.toISOString()); - expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); - - // make sure the workflow actually deleted from the db - const sharedWorkflow = await Db.collections.SharedWorkflow.findOneBy({ - workflowId: workflow.id, - }); - - expect(sharedWorkflow).toBeNull(); - }); - }); - - describe('POST /workflows/:id/activate', () => { - test( - 'should fail due to missing API Key', - testWithAPIKey('post', '/workflows/2/activate', null), - ); - - test( - 'should fail due to invalid API Key', - testWithAPIKey('post', '/workflows/2/activate', 'abcXYZ'), - ); - - test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.post(`/workflows/2/activate`); - expect(response.statusCode).toBe(404); - }); - - test('should fail due to trying to activate a workflow without a trigger', async () => { - const workflow = await testDb.createWorkflow({}, owner); - const response = await authOwnerAgent.post(`/workflows/${workflow.id}/activate`); - expect(response.statusCode).toBe(400); - }); - - test('should set workflow as active', async () => { - const workflow = await testDb.createWorkflowWithTrigger({}, member); - - const response = await authMemberAgent.post(`/workflows/${workflow.id}/activate`); - - expect(response.statusCode).toBe(200); - - const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = - response.body; - - expect(id).toEqual(workflow.id); - expect(name).toEqual(workflow.name); - expect(connections).toEqual(workflow.connections); - expect(active).toBe(true); - expect(staticData).toEqual(workflow.staticData); - expect(nodes).toEqual(workflow.nodes); - expect(settings).toEqual(workflow.settings); - expect(createdAt).toEqual(workflow.createdAt.toISOString()); - expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); - - // check whether the workflow is on the database - const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: member.id, - workflowId: workflow.id, - }, - relations: ['workflow'], - }); - - expect(sharedWorkflow?.workflow.active).toBe(true); - - // check whether the workflow is on the active workflow runner - expect(await workflowRunner.isActive(workflow.id)).toBe(true); - }); - - test('should set non-owned workflow as active when owner', async () => { - const workflow = await testDb.createWorkflowWithTrigger({}, member); - - const response = await authMemberAgent.post(`/workflows/${workflow.id}/activate`); - - expect(response.statusCode).toBe(200); - - const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = - response.body; - - expect(id).toEqual(workflow.id); - expect(name).toEqual(workflow.name); - expect(connections).toEqual(workflow.connections); - expect(active).toBe(true); - expect(staticData).toEqual(workflow.staticData); - expect(nodes).toEqual(workflow.nodes); - expect(settings).toEqual(workflow.settings); - expect(createdAt).toEqual(workflow.createdAt.toISOString()); - expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); - - // check whether the workflow is on the database - const sharedOwnerWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: owner.id, - workflowId: workflow.id, - }, - }); - - expect(sharedOwnerWorkflow).toBeNull(); - - const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: member.id, - workflowId: workflow.id, - }, - relations: ['workflow'], - }); - - expect(sharedWorkflow?.workflow.active).toBe(true); - - // check whether the workflow is on the active workflow runner - expect(await workflowRunner.isActive(workflow.id)).toBe(true); - }); - }); - - describe('POST /workflows/:id/deactivate', () => { - test( - 'should fail due to missing API Key', - testWithAPIKey('post', '/workflows/2/deactivate', null), - ); - - test( - 'should fail due to invalid API Key', - testWithAPIKey('post', '/workflows/2/deactivate', 'abcXYZ'), - ); - - test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.post(`/workflows/2/deactivate`); - expect(response.statusCode).toBe(404); - }); - - test('should deactivate workflow', async () => { - const workflow = await testDb.createWorkflowWithTrigger({}, member); - - await authMemberAgent.post(`/workflows/${workflow.id}/activate`); - - const workflowDeactivationResponse = await authMemberAgent.post( - `/workflows/${workflow.id}/deactivate`, - ); - - const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = - workflowDeactivationResponse.body; - - expect(id).toEqual(workflow.id); - expect(name).toEqual(workflow.name); - expect(connections).toEqual(workflow.connections); - expect(active).toBe(false); - expect(staticData).toEqual(workflow.staticData); - expect(nodes).toEqual(workflow.nodes); - expect(settings).toEqual(workflow.settings); - expect(createdAt).toBeDefined(); - expect(updatedAt).toBeDefined(); - - // get the workflow after it was deactivated - const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: member.id, - workflowId: workflow.id, - }, - relations: ['workflow'], - }); - - // check whether the workflow is deactivated in the database - expect(sharedWorkflow?.workflow.active).toBe(false); - - expect(await workflowRunner.isActive(workflow.id)).toBe(false); - }); - - test('should deactivate non-owned workflow when owner', async () => { - const workflow = await testDb.createWorkflowWithTrigger({}, member); - - await authMemberAgent.post(`/workflows/${workflow.id}/activate`); - - const workflowDeactivationResponse = await authMemberAgent.post( - `/workflows/${workflow.id}/deactivate`, - ); - - const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = - workflowDeactivationResponse.body; - - expect(id).toEqual(workflow.id); - expect(name).toEqual(workflow.name); - expect(connections).toEqual(workflow.connections); - expect(active).toBe(false); - expect(staticData).toEqual(workflow.staticData); - expect(nodes).toEqual(workflow.nodes); - expect(settings).toEqual(workflow.settings); - expect(createdAt).toBeDefined(); - expect(updatedAt).toBeDefined(); - - // check whether the workflow is deactivated in the database - const sharedOwnerWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: owner.id, - workflowId: workflow.id, - }, - }); - - expect(sharedOwnerWorkflow).toBeNull(); - - const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: member.id, - workflowId: workflow.id, - }, - relations: ['workflow'], - }); - - expect(sharedWorkflow?.workflow.active).toBe(false); - - expect(await workflowRunner.isActive(workflow.id)).toBe(false); - }); - }); - - describe('POST /workflows', () => { - test('should fail due to missing API Key', testWithAPIKey('post', '/workflows', null)); - - test('should fail due to invalid API Key', testWithAPIKey('post', '/workflows', 'abcXYZ')); - - test('should fail due to invalid body', async () => { - const response = await authOwnerAgent.post('/workflows').send({}); - expect(response.statusCode).toBe(400); - }); - - test('should create workflow', async () => { - const payload = { - name: 'testing', - nodes: [ - { - id: 'uuid-1234', - parameters: {}, - name: 'Start', - type: 'n8n-nodes-base.start', - typeVersion: 1, - position: [240, 300], - }, - ], - connections: {}, - staticData: null, - settings: { - saveExecutionProgress: true, - saveManualExecutions: true, - saveDataErrorExecution: 'all', - saveDataSuccessExecution: 'all', - executionTimeout: 3600, - timezone: 'America/New_York', - }, - }; - - const response = await authMemberAgent.post('/workflows').send(payload); - - expect(response.statusCode).toBe(200); - - const { id, name, nodes, connections, staticData, active, settings, createdAt, updatedAt } = - response.body; + } = workflow; expect(id).toBeDefined(); - expect(name).toBe(payload.name); - expect(connections).toEqual(payload.connections); - expect(settings).toEqual(payload.settings); - expect(staticData).toEqual(payload.staticData); - expect(nodes).toEqual(payload.nodes); + expect(name).toBeDefined(); + expect(connections).toBeDefined(); expect(active).toBe(false); + expect(staticData).toBeDefined(); + expect(nodes).toBeDefined(); + expect(tags).toBeDefined(); + expect(settings).toBeDefined(); expect(createdAt).toBeDefined(); - expect(updatedAt).toEqual(createdAt); - - // check if created workflow in DB - const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: member.id, - workflowId: response.body.id, - }, - relations: ['workflow', 'role'], - }); - - expect(sharedWorkflow?.workflow.name).toBe(name); - expect(sharedWorkflow?.workflow.createdAt.toISOString()).toBe(createdAt); - expect(sharedWorkflow?.role).toEqual(workflowOwnerRole); - }); + expect(updatedAt).toBeDefined(); + } }); - describe('PUT /workflows/:id', () => { - test('should fail due to missing API Key', testWithAPIKey('put', '/workflows/1', null)); + test('should return all owned workflows with pagination', async () => { + await Promise.all([ + testDb.createWorkflow({}, member), + testDb.createWorkflow({}, member), + testDb.createWorkflow({}, member), + ]); - test('should fail due to invalid API Key', testWithAPIKey('put', '/workflows/1', 'abcXYZ')); + const response = await authMemberAgent.get('/workflows?limit=1'); - test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.put(`/workflows/1`).send({ - name: 'testing', - nodes: [ - { - id: 'uuid-1234', - parameters: {}, - name: 'Start', - type: 'n8n-nodes-base.start', - typeVersion: 1, - position: [240, 300], - }, - ], - connections: {}, - staticData: null, - settings: { - saveExecutionProgress: true, - saveManualExecutions: true, - saveDataErrorExecution: 'all', - saveDataSuccessExecution: 'all', - executionTimeout: 3600, - timezone: 'America/New_York', - }, - }); + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(1); + expect(response.body.nextCursor).not.toBeNull(); - expect(response.statusCode).toBe(404); - }); + const response2 = await authMemberAgent.get( + `/workflows?limit=1&cursor=${response.body.nextCursor}`, + ); - test('should fail due to invalid body', async () => { - const response = await authOwnerAgent.put(`/workflows/1`).send({ - nodes: [ - { - id: 'uuid-1234', - parameters: {}, - name: 'Start', - type: 'n8n-nodes-base.start', - typeVersion: 1, - position: [240, 300], - }, - ], - connections: {}, - staticData: null, - settings: { - saveExecutionProgress: true, - saveManualExecutions: true, - saveDataErrorExecution: 'all', - saveDataSuccessExecution: 'all', - executionTimeout: 3600, - timezone: 'America/New_York', - }, - }); + expect(response2.statusCode).toBe(200); + expect(response2.body.data.length).toBe(1); + expect(response2.body.nextCursor).not.toBeNull(); + expect(response2.body.nextCursor).not.toBe(response.body.nextCursor); - expect(response.statusCode).toBe(400); - }); + const responses = [...response.body.data, ...response2.body.data]; - test('should update workflow', async () => { - const workflow = await testDb.createWorkflow({}, member); - const payload = { - name: 'name updated', - nodes: [ - { - id: 'uuid-1234', - parameters: {}, - name: 'Start', - type: 'n8n-nodes-base.start', - typeVersion: 1, - position: [240, 300], - }, - { - id: 'uuid-1234', - parameters: {}, - name: 'Cron', - type: 'n8n-nodes-base.cron', - typeVersion: 1, - position: [400, 300], - }, - ], - connections: {}, - staticData: '{"id":1}', - settings: { - saveExecutionProgress: false, - saveManualExecutions: false, - saveDataErrorExecution: 'all', - saveDataSuccessExecution: 'all', - executionTimeout: 3600, - timezone: 'America/New_York', - }, - }; + for (const workflow of responses) { + const { + id, + connections, + active, + staticData, + nodes, + settings, + name, + createdAt, + updatedAt, + tags, + } = workflow; - const response = await authMemberAgent.put(`/workflows/${workflow.id}`).send(payload); - - const { id, name, nodes, connections, staticData, active, settings, createdAt, updatedAt } = - response.body; - - expect(response.statusCode).toBe(200); - - expect(id).toBe(workflow.id); - expect(name).toBe(payload.name); - expect(connections).toEqual(payload.connections); - expect(settings).toEqual(payload.settings); - expect(staticData).toMatchObject(JSON.parse(payload.staticData)); - expect(nodes).toEqual(payload.nodes); + expect(id).toBeDefined(); + expect(name).toBeDefined(); + expect(connections).toBeDefined(); expect(active).toBe(false); - expect(createdAt).toBe(workflow.createdAt.toISOString()); - expect(updatedAt).not.toBe(workflow.updatedAt.toISOString()); + expect(staticData).toBeDefined(); + expect(nodes).toBeDefined(); + expect(tags).toBeDefined(); + expect(settings).toBeDefined(); + expect(createdAt).toBeDefined(); + expect(updatedAt).toBeDefined(); + } - // check updated workflow in DB - const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: member.id, - workflowId: response.body.id, - }, - relations: ['workflow'], - }); + // check that we really received a different result + expect(Number(response.body.data[0].id)).toBeLessThan(Number(response2.body.data[0].id)); + }); - expect(sharedWorkflow?.workflow.name).toBe(payload.name); - expect(sharedWorkflow?.workflow.updatedAt.getTime()).toBeGreaterThan( - workflow.updatedAt.getTime(), - ); - }); + test('should return all owned workflows filtered by tag', async () => { + const tag = await testDb.createTag({}); - test('should update non-owned workflow if owner', async () => { - const workflow = await testDb.createWorkflow({}, member); + const [workflow] = await Promise.all([ + testDb.createWorkflow({ tags: [tag] }, member), + testDb.createWorkflow({}, member), + ]); - const payload = { - name: 'name owner updated', - nodes: [ - { - id: 'uuid-1', - parameters: {}, - name: 'Start', - type: 'n8n-nodes-base.start', - typeVersion: 1, - position: [240, 300], - }, - { - id: 'uuid-2', - parameters: {}, - name: 'Cron', - type: 'n8n-nodes-base.cron', - typeVersion: 1, - position: [400, 300], - }, - ], - connections: {}, - staticData: '{"id":1}', - settings: { - saveExecutionProgress: false, - saveManualExecutions: false, - saveDataErrorExecution: 'all', - saveDataSuccessExecution: 'all', - executionTimeout: 3600, - timezone: 'America/New_York', - }, - }; + const response = await authMemberAgent.get(`/workflows?tags=${tag.name}`); - const response = await authMemberAgent.put(`/workflows/${workflow.id}`).send(payload); + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(1); - const { id, name, nodes, connections, staticData, active, settings, createdAt, updatedAt } = - response.body; + const { + id, + connections, + active, + staticData, + nodes, + settings, + name, + createdAt, + updatedAt, + tags: wfTags, + } = response.body.data[0]; - expect(response.statusCode).toBe(200); + expect(id).toBe(workflow.id); + expect(name).toBeDefined(); + expect(connections).toBeDefined(); + expect(active).toBe(false); + expect(staticData).toBeDefined(); + expect(nodes).toBeDefined(); + expect(settings).toBeDefined(); + expect(createdAt).toBeDefined(); + expect(updatedAt).toBeDefined(); - expect(id).toBe(workflow.id); - expect(name).toBe(payload.name); - expect(connections).toEqual(payload.connections); - expect(settings).toEqual(payload.settings); - expect(staticData).toMatchObject(JSON.parse(payload.staticData)); - expect(nodes).toEqual(payload.nodes); + expect(wfTags.length).toBe(1); + expect(wfTags[0].id).toBe(tag.id); + }); + + test('should return all owned workflows filtered by tags', async () => { + const tags = await Promise.all([await testDb.createTag({}), await testDb.createTag({})]); + const tagNames = tags.map((tag) => tag.name).join(','); + + const [workflow1, workflow2] = await Promise.all([ + testDb.createWorkflow({ tags }, member), + testDb.createWorkflow({ tags }, member), + testDb.createWorkflow({}, member), + testDb.createWorkflow({ tags: [tags[0]] }, member), + testDb.createWorkflow({ tags: [tags[1]] }, member), + ]); + + const response = await authMemberAgent.get(`/workflows?tags=${tagNames}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(2); + + for (const workflow of response.body.data) { + const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = + workflow; + + expect(id).toBeDefined(); + expect([workflow1.id, workflow2.id].includes(id)).toBe(true); + + expect(name).toBeDefined(); + expect(connections).toBeDefined(); expect(active).toBe(false); - expect(createdAt).toBe(workflow.createdAt.toISOString()); - expect(updatedAt).not.toBe(workflow.updatedAt.toISOString()); + expect(staticData).toBeDefined(); + expect(nodes).toBeDefined(); + expect(settings).toBeDefined(); + expect(createdAt).toBeDefined(); + expect(updatedAt).toBeDefined(); - // check updated workflow in DB - const sharedOwnerWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: owner.id, - workflowId: response.body.id, - }, + expect(workflow.tags.length).toBe(2); + workflow.tags.forEach((tag: TagEntity) => { + expect(tags.some((savedTag) => savedTag.id === tag.id)).toBe(true); }); + } + }); - expect(sharedOwnerWorkflow).toBeNull(); + test('should return all workflows for owner', async () => { + await Promise.all([ + testDb.createWorkflow({}, owner), + testDb.createWorkflow({}, member), + testDb.createWorkflow({}, owner), + testDb.createWorkflow({}, member), + testDb.createWorkflow({}, owner), + ]); - const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ - where: { - userId: member.id, - workflowId: response.body.id, - }, - relations: ['workflow', 'role'], - }); + const response = await authOwnerAgent.get('/workflows'); - expect(sharedWorkflow?.workflow.name).toBe(payload.name); - expect(sharedWorkflow?.workflow.updatedAt.getTime()).toBeGreaterThan( - workflow.updatedAt.getTime(), - ); - expect(sharedWorkflow?.role).toEqual(workflowOwnerRole); - }); + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(5); + expect(response.body.nextCursor).toBeNull(); + + for (const workflow of response.body.data) { + const { + id, + connections, + active, + staticData, + nodes, + settings, + name, + createdAt, + updatedAt, + tags, + } = workflow; + + expect(id).toBeDefined(); + expect(name).toBeDefined(); + expect(connections).toBeDefined(); + expect(active).toBe(false); + expect(staticData).toBeDefined(); + expect(nodes).toBeDefined(); + expect(tags).toBeDefined(); + expect(settings).toBeDefined(); + expect(createdAt).toBeDefined(); + expect(updatedAt).toBeDefined(); + } + }); +}); + +describe('GET /workflows/:id', () => { + test('should fail due to missing API Key', testWithAPIKey('get', '/workflows/2', null)); + + test('should fail due to invalid API Key', testWithAPIKey('get', '/workflows/2', 'abcXYZ')); + + test('should fail due to non-existing workflow', async () => { + const response = await authOwnerAgent.get(`/workflows/2`); + expect(response.statusCode).toBe(404); + }); + + test('should retrieve workflow', async () => { + // create and assign workflow to owner + const workflow = await testDb.createWorkflow({}, member); + + const response = await authMemberAgent.get(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + + const { + id, + connections, + active, + staticData, + nodes, + settings, + name, + createdAt, + updatedAt, + tags, + } = response.body; + + expect(id).toEqual(workflow.id); + expect(name).toEqual(workflow.name); + expect(connections).toEqual(workflow.connections); + expect(active).toBe(false); + expect(staticData).toEqual(workflow.staticData); + expect(nodes).toEqual(workflow.nodes); + expect(tags).toEqual([]); + expect(settings).toEqual(workflow.settings); + expect(createdAt).toEqual(workflow.createdAt.toISOString()); + expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); + }); + + test('should retrieve non-owned workflow for owner', async () => { + // create and assign workflow to owner + const workflow = await testDb.createWorkflow({}, member); + + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + + const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = + response.body; + + expect(id).toEqual(workflow.id); + expect(name).toEqual(workflow.name); + expect(connections).toEqual(workflow.connections); + expect(active).toBe(false); + expect(staticData).toEqual(workflow.staticData); + expect(nodes).toEqual(workflow.nodes); + expect(settings).toEqual(workflow.settings); + expect(createdAt).toEqual(workflow.createdAt.toISOString()); + expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); + }); +}); + +describe('DELETE /workflows/:id', () => { + test('should fail due to missing API Key', testWithAPIKey('delete', '/workflows/2', null)); + + test('should fail due to invalid API Key', testWithAPIKey('delete', '/workflows/2', 'abcXYZ')); + + test('should fail due to non-existing workflow', async () => { + const response = await authOwnerAgent.delete(`/workflows/2`); + expect(response.statusCode).toBe(404); + }); + + test('should delete the workflow', async () => { + // create and assign workflow to owner + const workflow = await testDb.createWorkflow({}, member); + + const response = await authMemberAgent.delete(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + + const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = + response.body; + + expect(id).toEqual(workflow.id); + expect(name).toEqual(workflow.name); + expect(connections).toEqual(workflow.connections); + expect(active).toBe(false); + expect(staticData).toEqual(workflow.staticData); + expect(nodes).toEqual(workflow.nodes); + expect(settings).toEqual(workflow.settings); + expect(createdAt).toEqual(workflow.createdAt.toISOString()); + expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); + + // make sure the workflow actually deleted from the db + const sharedWorkflow = await Db.collections.SharedWorkflow.findOneBy({ + workflowId: workflow.id, + }); + + expect(sharedWorkflow).toBeNull(); + }); + + test('should delete non-owned workflow when owner', async () => { + // create and assign workflow to owner + const workflow = await testDb.createWorkflow({}, member); + + const response = await authMemberAgent.delete(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + + const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = + response.body; + + expect(id).toEqual(workflow.id); + expect(name).toEqual(workflow.name); + expect(connections).toEqual(workflow.connections); + expect(active).toBe(false); + expect(staticData).toEqual(workflow.staticData); + expect(nodes).toEqual(workflow.nodes); + expect(settings).toEqual(workflow.settings); + expect(createdAt).toEqual(workflow.createdAt.toISOString()); + expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); + + // make sure the workflow actually deleted from the db + const sharedWorkflow = await Db.collections.SharedWorkflow.findOneBy({ + workflowId: workflow.id, + }); + + expect(sharedWorkflow).toBeNull(); + }); +}); + +describe('POST /workflows/:id/activate', () => { + test('should fail due to missing API Key', testWithAPIKey('post', '/workflows/2/activate', null)); + + test( + 'should fail due to invalid API Key', + testWithAPIKey('post', '/workflows/2/activate', 'abcXYZ'), + ); + + test('should fail due to non-existing workflow', async () => { + const response = await authOwnerAgent.post(`/workflows/2/activate`); + expect(response.statusCode).toBe(404); + }); + + test('should fail due to trying to activate a workflow without a trigger', async () => { + const workflow = await testDb.createWorkflow({}, owner); + const response = await authOwnerAgent.post(`/workflows/${workflow.id}/activate`); + expect(response.statusCode).toBe(400); + }); + + test('should set workflow as active', async () => { + const workflow = await testDb.createWorkflowWithTrigger({}, member); + + const response = await authMemberAgent.post(`/workflows/${workflow.id}/activate`); + + expect(response.statusCode).toBe(200); + + const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = + response.body; + + expect(id).toEqual(workflow.id); + expect(name).toEqual(workflow.name); + expect(connections).toEqual(workflow.connections); + expect(active).toBe(true); + expect(staticData).toEqual(workflow.staticData); + expect(nodes).toEqual(workflow.nodes); + expect(settings).toEqual(workflow.settings); + expect(createdAt).toEqual(workflow.createdAt.toISOString()); + expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); + + // check whether the workflow is on the database + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: member.id, + workflowId: workflow.id, + }, + relations: ['workflow'], + }); + + expect(sharedWorkflow?.workflow.active).toBe(true); + + // check whether the workflow is on the active workflow runner + expect(await workflowRunner.isActive(workflow.id)).toBe(true); + }); + + test('should set non-owned workflow as active when owner', async () => { + const workflow = await testDb.createWorkflowWithTrigger({}, member); + + const response = await authMemberAgent.post(`/workflows/${workflow.id}/activate`); + + expect(response.statusCode).toBe(200); + + const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = + response.body; + + expect(id).toEqual(workflow.id); + expect(name).toEqual(workflow.name); + expect(connections).toEqual(workflow.connections); + expect(active).toBe(true); + expect(staticData).toEqual(workflow.staticData); + expect(nodes).toEqual(workflow.nodes); + expect(settings).toEqual(workflow.settings); + expect(createdAt).toEqual(workflow.createdAt.toISOString()); + expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); + + // check whether the workflow is on the database + const sharedOwnerWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: owner.id, + workflowId: workflow.id, + }, + }); + + expect(sharedOwnerWorkflow).toBeNull(); + + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: member.id, + workflowId: workflow.id, + }, + relations: ['workflow'], + }); + + expect(sharedWorkflow?.workflow.active).toBe(true); + + // check whether the workflow is on the active workflow runner + expect(await workflowRunner.isActive(workflow.id)).toBe(true); + }); +}); + +describe('POST /workflows/:id/deactivate', () => { + test( + 'should fail due to missing API Key', + testWithAPIKey('post', '/workflows/2/deactivate', null), + ); + + test( + 'should fail due to invalid API Key', + testWithAPIKey('post', '/workflows/2/deactivate', 'abcXYZ'), + ); + + test('should fail due to non-existing workflow', async () => { + const response = await authOwnerAgent.post(`/workflows/2/deactivate`); + expect(response.statusCode).toBe(404); + }); + + test('should deactivate workflow', async () => { + const workflow = await testDb.createWorkflowWithTrigger({}, member); + + await authMemberAgent.post(`/workflows/${workflow.id}/activate`); + + const workflowDeactivationResponse = await authMemberAgent.post( + `/workflows/${workflow.id}/deactivate`, + ); + + const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = + workflowDeactivationResponse.body; + + expect(id).toEqual(workflow.id); + expect(name).toEqual(workflow.name); + expect(connections).toEqual(workflow.connections); + expect(active).toBe(false); + expect(staticData).toEqual(workflow.staticData); + expect(nodes).toEqual(workflow.nodes); + expect(settings).toEqual(workflow.settings); + expect(createdAt).toBeDefined(); + expect(updatedAt).toBeDefined(); + + // get the workflow after it was deactivated + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: member.id, + workflowId: workflow.id, + }, + relations: ['workflow'], + }); + + // check whether the workflow is deactivated in the database + expect(sharedWorkflow?.workflow.active).toBe(false); + + expect(await workflowRunner.isActive(workflow.id)).toBe(false); + }); + + test('should deactivate non-owned workflow when owner', async () => { + const workflow = await testDb.createWorkflowWithTrigger({}, member); + + await authMemberAgent.post(`/workflows/${workflow.id}/activate`); + + const workflowDeactivationResponse = await authMemberAgent.post( + `/workflows/${workflow.id}/deactivate`, + ); + + const { id, connections, active, staticData, nodes, settings, name, createdAt, updatedAt } = + workflowDeactivationResponse.body; + + expect(id).toEqual(workflow.id); + expect(name).toEqual(workflow.name); + expect(connections).toEqual(workflow.connections); + expect(active).toBe(false); + expect(staticData).toEqual(workflow.staticData); + expect(nodes).toEqual(workflow.nodes); + expect(settings).toEqual(workflow.settings); + expect(createdAt).toBeDefined(); + expect(updatedAt).toBeDefined(); + + // check whether the workflow is deactivated in the database + const sharedOwnerWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: owner.id, + workflowId: workflow.id, + }, + }); + + expect(sharedOwnerWorkflow).toBeNull(); + + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: member.id, + workflowId: workflow.id, + }, + relations: ['workflow'], + }); + + expect(sharedWorkflow?.workflow.active).toBe(false); + + expect(await workflowRunner.isActive(workflow.id)).toBe(false); + }); +}); + +describe('POST /workflows', () => { + test('should fail due to missing API Key', testWithAPIKey('post', '/workflows', null)); + + test('should fail due to invalid API Key', testWithAPIKey('post', '/workflows', 'abcXYZ')); + + test('should fail due to invalid body', async () => { + const response = await authOwnerAgent.post('/workflows').send({}); + expect(response.statusCode).toBe(400); + }); + + test('should create workflow', async () => { + const payload = { + name: 'testing', + nodes: [ + { + id: 'uuid-1234', + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [240, 300], + }, + ], + connections: {}, + staticData: null, + settings: { + saveExecutionProgress: true, + saveManualExecutions: true, + saveDataErrorExecution: 'all', + saveDataSuccessExecution: 'all', + executionTimeout: 3600, + timezone: 'America/New_York', + }, + }; + + const response = await authMemberAgent.post('/workflows').send(payload); + + expect(response.statusCode).toBe(200); + + const { id, name, nodes, connections, staticData, active, settings, createdAt, updatedAt } = + response.body; + + expect(id).toBeDefined(); + expect(name).toBe(payload.name); + expect(connections).toEqual(payload.connections); + expect(settings).toEqual(payload.settings); + expect(staticData).toEqual(payload.staticData); + expect(nodes).toEqual(payload.nodes); + expect(active).toBe(false); + expect(createdAt).toBeDefined(); + expect(updatedAt).toEqual(createdAt); + + // check if created workflow in DB + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: member.id, + workflowId: response.body.id, + }, + relations: ['workflow', 'role'], + }); + + expect(sharedWorkflow?.workflow.name).toBe(name); + expect(sharedWorkflow?.workflow.createdAt.toISOString()).toBe(createdAt); + expect(sharedWorkflow?.role).toEqual(workflowOwnerRole); + }); +}); + +describe('PUT /workflows/:id', () => { + test('should fail due to missing API Key', testWithAPIKey('put', '/workflows/1', null)); + + test('should fail due to invalid API Key', testWithAPIKey('put', '/workflows/1', 'abcXYZ')); + + test('should fail due to non-existing workflow', async () => { + const response = await authOwnerAgent.put(`/workflows/1`).send({ + name: 'testing', + nodes: [ + { + id: 'uuid-1234', + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [240, 300], + }, + ], + connections: {}, + staticData: null, + settings: { + saveExecutionProgress: true, + saveManualExecutions: true, + saveDataErrorExecution: 'all', + saveDataSuccessExecution: 'all', + executionTimeout: 3600, + timezone: 'America/New_York', + }, + }); + + expect(response.statusCode).toBe(404); + }); + + test('should fail due to invalid body', async () => { + const response = await authOwnerAgent.put(`/workflows/1`).send({ + nodes: [ + { + id: 'uuid-1234', + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [240, 300], + }, + ], + connections: {}, + staticData: null, + settings: { + saveExecutionProgress: true, + saveManualExecutions: true, + saveDataErrorExecution: 'all', + saveDataSuccessExecution: 'all', + executionTimeout: 3600, + timezone: 'America/New_York', + }, + }); + + expect(response.statusCode).toBe(400); + }); + + test('should update workflow', async () => { + const workflow = await testDb.createWorkflow({}, member); + const payload = { + name: 'name updated', + nodes: [ + { + id: 'uuid-1234', + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [240, 300], + }, + { + id: 'uuid-1234', + parameters: {}, + name: 'Cron', + type: 'n8n-nodes-base.cron', + typeVersion: 1, + position: [400, 300], + }, + ], + connections: {}, + staticData: '{"id":1}', + settings: { + saveExecutionProgress: false, + saveManualExecutions: false, + saveDataErrorExecution: 'all', + saveDataSuccessExecution: 'all', + executionTimeout: 3600, + timezone: 'America/New_York', + }, + }; + + const response = await authMemberAgent.put(`/workflows/${workflow.id}`).send(payload); + + const { id, name, nodes, connections, staticData, active, settings, createdAt, updatedAt } = + response.body; + + expect(response.statusCode).toBe(200); + + expect(id).toBe(workflow.id); + expect(name).toBe(payload.name); + expect(connections).toEqual(payload.connections); + expect(settings).toEqual(payload.settings); + expect(staticData).toMatchObject(JSON.parse(payload.staticData)); + expect(nodes).toEqual(payload.nodes); + expect(active).toBe(false); + expect(createdAt).toBe(workflow.createdAt.toISOString()); + expect(updatedAt).not.toBe(workflow.updatedAt.toISOString()); + + // check updated workflow in DB + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: member.id, + workflowId: response.body.id, + }, + relations: ['workflow'], + }); + + expect(sharedWorkflow?.workflow.name).toBe(payload.name); + expect(sharedWorkflow?.workflow.updatedAt.getTime()).toBeGreaterThan( + workflow.updatedAt.getTime(), + ); + }); + + test('should update non-owned workflow if owner', async () => { + const workflow = await testDb.createWorkflow({}, member); + + const payload = { + name: 'name owner updated', + nodes: [ + { + id: 'uuid-1', + parameters: {}, + name: 'Start', + type: 'n8n-nodes-base.start', + typeVersion: 1, + position: [240, 300], + }, + { + id: 'uuid-2', + parameters: {}, + name: 'Cron', + type: 'n8n-nodes-base.cron', + typeVersion: 1, + position: [400, 300], + }, + ], + connections: {}, + staticData: '{"id":1}', + settings: { + saveExecutionProgress: false, + saveManualExecutions: false, + saveDataErrorExecution: 'all', + saveDataSuccessExecution: 'all', + executionTimeout: 3600, + timezone: 'America/New_York', + }, + }; + + const response = await authMemberAgent.put(`/workflows/${workflow.id}`).send(payload); + + const { id, name, nodes, connections, staticData, active, settings, createdAt, updatedAt } = + response.body; + + expect(response.statusCode).toBe(200); + + expect(id).toBe(workflow.id); + expect(name).toBe(payload.name); + expect(connections).toEqual(payload.connections); + expect(settings).toEqual(payload.settings); + expect(staticData).toMatchObject(JSON.parse(payload.staticData)); + expect(nodes).toEqual(payload.nodes); + expect(active).toBe(false); + expect(createdAt).toBe(workflow.createdAt.toISOString()); + expect(updatedAt).not.toBe(workflow.updatedAt.toISOString()); + + // check updated workflow in DB + const sharedOwnerWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: owner.id, + workflowId: response.body.id, + }, + }); + + expect(sharedOwnerWorkflow).toBeNull(); + + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + where: { + userId: member.id, + workflowId: response.body.id, + }, + relations: ['workflow', 'role'], + }); + + expect(sharedWorkflow?.workflow.name).toBe(payload.name); + expect(sharedWorkflow?.workflow.updatedAt.getTime()).toBeGreaterThan( + workflow.updatedAt.getTime(), + ); + expect(sharedWorkflow?.role).toEqual(workflowOwnerRole); }); }); diff --git a/packages/cli/test/integration/saml/saml.api.test.ts b/packages/cli/test/integration/saml/saml.api.test.ts index 0a4c1abfdf..3eda945f9e 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -1,18 +1,14 @@ -import express from 'express'; - +import type { SuperAgentTest } from 'supertest'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; +import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; +import { setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; import { randomEmail, randomName, randomValidPassword } from '../shared/random'; import * as testDb from '../shared/testDb'; -import type { AuthAgent } from '../shared/types'; import * as utils from '../shared/utils'; -import { setSamlLoginEnabled } from '../../../src/sso/saml/samlHelpers'; -import { setCurrentAuthenticationMethod } from '../../../src/sso/ssoHelpers'; -let app: express.Application; -let globalOwnerRole: Role; -let globalMemberRole: Role; -let authAgent: AuthAgent; +let owner: User; +let authOwnerAgent: SuperAgentTest; function enableSaml(enable: boolean) { setSamlLoginEnabled(enable); @@ -21,58 +17,59 @@ function enableSaml(enable: boolean) { } beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['me'] }); - - globalOwnerRole = await testDb.getGlobalOwnerRole(); - globalMemberRole = await testDb.getGlobalMemberRole(); - - authAgent = utils.createAuthAgent(app); + const app = await utils.initTestServer({ endpointGroups: ['me'] }); + owner = await testDb.createOwner(); + authOwnerAgent = utils.createAuthAgent(app)(owner); }); -beforeEach(async () => { - await testDb.truncate(['User']); -}); +// beforeEach(async () => { +// await testDb.truncate(['User']); +// }); afterAll(async () => { await testDb.terminate(); }); describe('Instance owner', () => { - test('PATCH /me should succeed with valid inputs', async () => { - const owner = await testDb.createOwner(); - const authOwnerAgent = authAgent(owner); - const response = await authOwnerAgent.patch('/me').send({ - email: randomEmail(), - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), + describe('PATCH /me', () => { + test('should succeed with valid inputs', async () => { + enableSaml(false); + await authOwnerAgent + .patch('/me') + .send({ + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }) + .expect(200); + }); + + test('should throw BadRequestError if email is changed when SAML is enabled', async () => { + enableSaml(true); + await authOwnerAgent + .patch('/me') + .send({ + email: randomEmail(), + firstName: randomName(), + lastName: randomName(), + }) + .expect(400, { code: 400, message: 'SAML user may not change their email' }); }); - expect(response.statusCode).toBe(200); }); - test('PATCH /me should throw BadRequestError if email is changed when SAML is enabled', async () => { - enableSaml(true); - const owner = await testDb.createOwner(); - const authOwnerAgent = authAgent(owner); - const response = await authOwnerAgent.patch('/me').send({ - email: randomEmail(), - firstName: randomName(), - lastName: randomName(), + describe('PATCH /password', () => { + test('should throw BadRequestError if password is changed when SAML is enabled', async () => { + enableSaml(true); + await authOwnerAgent + .patch('/me/password') + .send({ + password: randomValidPassword(), + }) + .expect(400, { + code: 400, + message: 'With SAML enabled, users need to use their SAML provider to change passwords', + }); }); - expect(response.statusCode).toBe(400); - expect(response.body.message).toContain('SAML'); - enableSaml(false); - }); - - test('PATCH /password should throw BadRequestError if password is changed when SAML is enabled', async () => { - enableSaml(true); - const owner = await testDb.createOwner(); - const authOwnerAgent = authAgent(owner); - const response = await authOwnerAgent.patch('/me/password').send({ - password: randomValidPassword(), - }); - expect(response.statusCode).toBe(400); - expect(response.body.message).toContain('SAML'); - enableSaml(false); }); }); diff --git a/packages/cli/test/integration/shared/augmentation.d.ts b/packages/cli/test/integration/shared/augmentation.d.ts index ecd3ec8e13..4dfa538e9a 100644 --- a/packages/cli/test/integration/shared/augmentation.d.ts +++ b/packages/cli/test/integration/shared/augmentation.d.ts @@ -1,5 +1,4 @@ import superagent = require('superagent'); -import type { ObjectLiteral } from 'typeorm'; /** * Make `SuperTest` string-indexable. diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index bef914b73a..d827f1562d 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -9,13 +9,11 @@ import set from 'lodash.set'; import { BinaryDataManager, UserSettings } from 'n8n-core'; import { ICredentialType, - ICredentialTypes, IDataObject, IExecuteFunctions, INode, INodeExecutionData, INodeParameters, - INodesAndCredentials, ITriggerFunctions, ITriggerResponse, LoggerProxy, @@ -90,13 +88,6 @@ export const mockInstance = ( return instance; }; -const loadNodesAndCredentials: INodesAndCredentials = { - loaded: { nodes: {}, credentials: {} }, - known: { nodes: {}, credentials: {} }, - credentialTypes: {} as ICredentialTypes, -}; -Container.set(LoadNodesAndCredentials, loadNodesAndCredentials); - /** * Initialize a test server. */ @@ -740,6 +731,15 @@ export async function isInstanceOwnerSetUp() { return Boolean(value); } +export const setInstanceOwnerSetUp = async (value: boolean) => { + config.set('userManagement.isInstanceOwnerSetUp', value); + + await Db.collections.Settings.update( + { key: 'userManagement.isInstanceOwnerSetUp' }, + { value: JSON.stringify(value) }, + ); +}; + // ---------------------------------- // misc // ---------------------------------- diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index a5c480a375..759d12b8cb 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -1,5 +1,6 @@ -import express from 'express'; import validator from 'validator'; +import { Not } from 'typeorm'; +import type { SuperAgentTest } from 'supertest'; import config from '@/config'; import * as Db from '@/Db'; @@ -8,6 +9,9 @@ import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { compareHash } from '@/UserManagement/UserManagementHelper'; +import { UserManagementMailer } from '@/UserManagement/email/UserManagementMailer'; +import { NodeMailer } from '@/UserManagement/email/NodeMailer'; + import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { randomCredentialPayload, @@ -17,41 +21,40 @@ import { randomValidPassword, } from './shared/random'; import * as testDb from './shared/testDb'; -import type { AuthAgent } from './shared/types'; import * as utils from './shared/utils'; -import { UserManagementMailer } from '@/UserManagement/email/UserManagementMailer'; -import { NodeMailer } from '@/UserManagement/email/NodeMailer'; - jest.mock('@/UserManagement/email/NodeMailer'); -let app: express.Application; let globalMemberRole: Role; -let globalOwnerRole: Role; let workflowOwnerRole: Role; let credentialOwnerRole: Role; -let authAgent: AuthAgent; +let owner: User; +let authlessAgent: SuperAgentTest; +let authOwnerAgent: SuperAgentTest; beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['users'] }); + const app = await utils.initTestServer({ endpointGroups: ['users'] }); const [ - fetchedGlobalOwnerRole, + globalOwnerRole, fetchedGlobalMemberRole, fetchedWorkflowOwnerRole, fetchedCredentialOwnerRole, ] = await testDb.getAllRoles(); - globalOwnerRole = fetchedGlobalOwnerRole; globalMemberRole = fetchedGlobalMemberRole; workflowOwnerRole = fetchedWorkflowOwnerRole; credentialOwnerRole = fetchedCredentialOwnerRole; - authAgent = utils.createAuthAgent(app); + owner = await testDb.createUser({ globalRole: globalOwnerRole }); + + authlessAgent = utils.createAgent(app); + authOwnerAgent = utils.createAuthAgent(app)(owner); }); beforeEach(async () => { - await testDb.truncate(['User', 'SharedCredentials', 'SharedWorkflow', 'Workflow', 'Credentials']); + await testDb.truncate(['SharedCredentials', 'SharedWorkflow', 'Workflow', 'Credentials']); + await Db.collections.User.delete({ id: Not(owner.id) }); jest.mock('@/config'); @@ -65,18 +68,16 @@ afterAll(async () => { await testDb.terminate(); }); -test('GET /users should return all users', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); +describe('GET /users', () => { + test('should return all users', async () => { + await testDb.createUser({ globalRole: globalMemberRole }); - await testDb.createUser({ globalRole: globalMemberRole }); + const response = await authOwnerAgent.get('/users'); - const response = await authAgent(owner).get('/users'); + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(2); - expect(response.statusCode).toBe(200); - expect(response.body.data.length).toBe(2); - - await Promise.all( - response.body.data.map(async (user: User) => { + response.body.data.map((user: User) => { const { id, email, @@ -100,442 +101,421 @@ test('GET /users should return all users', async () => { expect(isPending).toBe(false); expect(globalRole).toBeDefined(); expect(apiKey).not.toBeDefined(); - }), - ); + }); + }); }); -test('DELETE /users/:id should delete the user', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); +describe('DELETE /users/:id', () => { + test('should delete the user', async () => { + const userToDelete = await testDb.createUser({ globalRole: globalMemberRole }); - const userToDelete = await testDb.createUser({ globalRole: globalMemberRole }); + const newWorkflow = new WorkflowEntity(); - const newWorkflow = new WorkflowEntity(); + Object.assign(newWorkflow, { + name: randomName(), + active: false, + connections: {}, + nodes: [], + }); - Object.assign(newWorkflow, { - name: randomName(), - active: false, - connections: {}, - nodes: [], + const savedWorkflow = await Db.collections.Workflow.save(newWorkflow); + + await Db.collections.SharedWorkflow.save({ + role: workflowOwnerRole, + user: userToDelete, + workflow: savedWorkflow, + }); + + const newCredential = new CredentialsEntity(); + + Object.assign(newCredential, { + name: randomName(), + data: '', + type: '', + nodesAccess: [], + }); + + const savedCredential = await Db.collections.Credentials.save(newCredential); + + await Db.collections.SharedCredentials.save({ + role: credentialOwnerRole, + user: userToDelete, + credentials: savedCredential, + }); + + const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); + + const user = await Db.collections.User.findOneBy({ id: userToDelete.id }); + expect(user).toBeNull(); // deleted + + const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ + relations: ['user'], + where: { userId: userToDelete.id, roleId: workflowOwnerRole.id }, + }); + expect(sharedWorkflow).toBeNull(); // deleted + + const sharedCredential = await Db.collections.SharedCredentials.findOne({ + relations: ['user'], + where: { userId: userToDelete.id, roleId: credentialOwnerRole.id }, + }); + expect(sharedCredential).toBeNull(); // deleted + + const workflow = await Db.collections.Workflow.findOneBy({ id: savedWorkflow.id }); + expect(workflow).toBeNull(); // deleted + + // TODO: Include active workflow and check whether webhook has been removed + + const credential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); + expect(credential).toBeNull(); // deleted }); - const savedWorkflow = await Db.collections.Workflow.save(newWorkflow); + test('should fail to delete self', async () => { + const response = await authOwnerAgent.delete(`/users/${owner.id}`); - await Db.collections.SharedWorkflow.save({ - role: workflowOwnerRole, - user: userToDelete, - workflow: savedWorkflow, + expect(response.statusCode).toBe(400); + + const user = await Db.collections.User.findOneBy({ id: owner.id }); + expect(user).toBeDefined(); }); - const newCredential = new CredentialsEntity(); + test('should fail if user to delete is transferee', async () => { + const { id: idToDelete } = await testDb.createUser({ globalRole: globalMemberRole }); - Object.assign(newCredential, { - name: randomName(), - data: '', - type: '', - nodesAccess: [], + const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({ + transferId: idToDelete, + }); + + expect(response.statusCode).toBe(400); + + const user = await Db.collections.User.findOneBy({ id: idToDelete }); + expect(user).toBeDefined(); }); - const savedCredential = await Db.collections.Credentials.save(newCredential); + test('with transferId should perform transfer', async () => { + const userToDelete = await testDb.createUser({ globalRole: globalMemberRole }); - await Db.collections.SharedCredentials.save({ - role: credentialOwnerRole, - user: userToDelete, - credentials: savedCredential, + const savedWorkflow = await testDb.createWorkflow(undefined, userToDelete); + + const savedCredential = await testDb.saveCredential(randomCredentialPayload(), { + user: userToDelete, + role: credentialOwnerRole, + }); + + const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`).query({ + transferId: owner.id, + }); + + expect(response.statusCode).toBe(200); + + const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({ + relations: ['workflow'], + where: { userId: owner.id }, + }); + + expect(sharedWorkflow.workflow).toBeDefined(); + expect(sharedWorkflow.workflow.id).toBe(savedWorkflow.id); + + const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ + relations: ['credentials'], + where: { userId: owner.id }, + }); + + expect(sharedCredential.credentials).toBeDefined(); + expect(sharedCredential.credentials.id).toBe(savedCredential.id); + + const deletedUser = await Db.collections.User.findOneBy({ id: userToDelete.id }); + + expect(deletedUser).toBeNull(); }); - - const response = await authAgent(owner).delete(`/users/${userToDelete.id}`); - - expect(response.statusCode).toBe(200); - expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); - - const user = await Db.collections.User.findOneBy({ id: userToDelete.id }); - expect(user).toBeNull(); // deleted - - const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ - relations: ['user'], - where: { userId: userToDelete.id, roleId: workflowOwnerRole.id }, - }); - expect(sharedWorkflow).toBeNull(); // deleted - - const sharedCredential = await Db.collections.SharedCredentials.findOne({ - relations: ['user'], - where: { userId: userToDelete.id, roleId: credentialOwnerRole.id }, - }); - expect(sharedCredential).toBeNull(); // deleted - - const workflow = await Db.collections.Workflow.findOneBy({ id: savedWorkflow.id }); - expect(workflow).toBeNull(); // deleted - - // TODO: Include active workflow and check whether webhook has been removed - - const credential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id }); - expect(credential).toBeNull(); // deleted }); -test('DELETE /users/:id should fail to delete self', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); +describe('POST /users/:id', () => { + test('should fill out a user shell', async () => { + const memberShell = await testDb.createUserShell(globalMemberRole); - const response = await authAgent(owner).delete(`/users/${owner.id}`); - - expect(response.statusCode).toBe(400); - - const user = await Db.collections.User.findOneBy({ id: owner.id }); - expect(user).toBeDefined(); -}); - -test('DELETE /users/:id should fail if user to delete is transferee', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const { id: idToDelete } = await testDb.createUser({ globalRole: globalMemberRole }); - - const response = await authAgent(owner).delete(`/users/${idToDelete}`).query({ - transferId: idToDelete, - }); - - expect(response.statusCode).toBe(400); - - const user = await Db.collections.User.findOneBy({ id: idToDelete }); - expect(user).toBeDefined(); -}); - -test('DELETE /users/:id with transferId should perform transfer', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const userToDelete = await testDb.createUser({ globalRole: globalMemberRole }); - - const savedWorkflow = await testDb.createWorkflow(undefined, userToDelete); - - const savedCredential = await testDb.saveCredential(randomCredentialPayload(), { - user: userToDelete, - role: credentialOwnerRole, - }); - - const response = await authAgent(owner).delete(`/users/${userToDelete.id}`).query({ - transferId: owner.id, - }); - - expect(response.statusCode).toBe(200); - - const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({ - relations: ['workflow'], - where: { userId: owner.id }, - }); - - expect(sharedWorkflow.workflow).toBeDefined(); - expect(sharedWorkflow.workflow.id).toBe(savedWorkflow.id); - - const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ - relations: ['credentials'], - where: { userId: owner.id }, - }); - - expect(sharedCredential.credentials).toBeDefined(); - expect(sharedCredential.credentials.id).toBe(savedCredential.id); - - const deletedUser = await Db.collections.User.findOneBy({ id: userToDelete.id }); - - expect(deletedUser).toBeNull(); -}); - -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 response = await authlessAgent.post(`/users/${memberShell.id}`).send(memberData); - - const { - id, - email, - firstName, - lastName, - personalizationAnswers, - password, - resetPasswordToken, - globalRole, - isPending, - apiKey, - } = response.body.data; - - expect(validator.isUUID(id)).toBe(true); - expect(email).toBeDefined(); - expect(firstName).toBe(memberData.firstName); - expect(lastName).toBe(memberData.lastName); - expect(personalizationAnswers).toBeNull(); - expect(password).toBeUndefined(); - expect(resetPasswordToken).toBeUndefined(); - expect(isPending).toBe(false); - expect(globalRole).toBeDefined(); - expect(apiKey).not.toBeDefined(); - - const authToken = utils.getAuthToken(response); - expect(authToken).toBeDefined(); - - const member = await Db.collections.User.findOneByOrFail({ id: 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 memberShellEmail = randomEmail(); - - const memberShell = await Db.collections.User.save({ - email: memberShellEmail, - globalRole: globalMemberRole, - }); - - const invalidPayloads = [ - { - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), - }, - { - inviterId: owner.id, - firstName: randomName(), - password: randomValidPassword(), - }, - { - inviterId: owner.id, - firstName: randomName(), - password: randomValidPassword(), - }, - { + const memberData = { inviterId: owner.id, firstName: randomName(), lastName: randomName(), - }, - { - inviterId: owner.id, - firstName: randomName(), - lastName: randomName(), - password: randomInvalidPassword(), - }, - ]; + password: randomValidPassword(), + }; - await Promise.all( - invalidPayloads.map(async (invalidPayload) => { - const response = await authlessAgent.post(`/users/${memberShell.id}`).send(invalidPayload); - expect(response.statusCode).toBe(400); + const response = await authlessAgent.post(`/users/${memberShell.id}`).send(memberData); - const storedUser = await Db.collections.User.findOneOrFail({ - where: { email: memberShellEmail }, - }); + const { + id, + email, + firstName, + lastName, + personalizationAnswers, + password, + resetPasswordToken, + globalRole, + isPending, + apiKey, + } = response.body.data; - expect(storedUser.firstName).toBeNull(); - expect(storedUser.lastName).toBeNull(); - expect(storedUser.password).toBeNull(); - }), - ); -}); - -test('POST /users/:id should fail with already accepted invite', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - - const newMemberData = { - inviterId: owner.id, - firstName: randomName(), - lastName: randomName(), - password: randomValidPassword(), - }; - - const authlessAgent = utils.createAgent(app); - - const response = await authlessAgent.post(`/users/${member.id}`).send(newMemberData); - - expect(response.statusCode).toBe(400); - - 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 compareHash(member.password, storedMember.password); - expect(comparisonResult).toBe(false); - expect(storedMember.password).not.toBe(newMemberData.password); -}); - -test('POST /users should succeed if emailing is not set up', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const response = await authAgent(owner) - .post('/users') - .send([{ email: randomEmail() }]); - - expect(response.statusCode).toBe(200); - expect(response.body.data[0].user.inviteAcceptUrl).toBeDefined(); -}); - -test('POST /users should fail if user management is disabled', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - config.set('userManagement.disabled', true); - config.set('userManagement.isInstanceOwnerSetUp', false); - - const response = await authAgent(owner) - .post('/users') - .send([{ email: randomEmail() }]); - - expect(response.statusCode).toBe(400); -}); - -test('POST /users should email invites and create user shells but ignore existing', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const memberShell = await testDb.createUserShell(globalMemberRole); - - config.set('userManagement.emails.mode', 'smtp'); - - const testEmails = [randomEmail(), randomEmail().toUpperCase(), memberShell.email, member.email]; - - const payload = testEmails.map((e) => ({ email: e })); - - const response = await authAgent(owner).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(id).not.toBe(member.id); - - const lowerCasedEmail = receivedEmail.toLowerCase(); - expect(receivedEmail).toBe(lowerCasedEmail); - expect(payload.some(({ email }) => email.toLowerCase() === lowerCasedEmail)).toBe(true); - - if (error) { - expect(error).toBe('Email could not be sent'); - } - - const storedUser = await Db.collections.User.findOneByOrFail({ id }); - const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } = - storedUser; - - expect(firstName).toBeNull(); - expect(lastName).toBeNull(); + expect(email).toBeDefined(); + expect(firstName).toBe(memberData.firstName); + expect(lastName).toBe(memberData.lastName); expect(personalizationAnswers).toBeNull(); - expect(password).toBeNull(); - expect(resetPasswordToken).toBeNull(); - } + expect(password).toBeUndefined(); + expect(resetPasswordToken).toBeUndefined(); + expect(isPending).toBe(false); + expect(globalRole).toBeDefined(); + expect(apiKey).not.toBeDefined(); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); + + const member = await Db.collections.User.findOneByOrFail({ id: memberShell.id }); + expect(member.firstName).toBe(memberData.firstName); + expect(member.lastName).toBe(memberData.lastName); + expect(member.password).not.toBe(memberData.password); + }); + + test('should fail with invalid inputs', async () => { + const memberShellEmail = randomEmail(); + + const memberShell = await Db.collections.User.save({ + email: memberShellEmail, + globalRole: globalMemberRole, + }); + + 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(), + }, + ]; + + 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('should fail with already accepted invite', async () => { + const member = await testDb.createUser({ globalRole: globalMemberRole }); + + const newMemberData = { + inviterId: owner.id, + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }; + + const response = await authlessAgent.post(`/users/${member.id}`).send(newMemberData); + + expect(response.statusCode).toBe(400); + + 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 compareHash(member.password, storedMember.password); + expect(comparisonResult).toBe(false); + expect(storedMember.password).not.toBe(newMemberData.password); + }); }); -test('POST /users should fail with invalid inputs', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const authOwnerAgent = authAgent(owner); +describe('POST /users', () => { + beforeEach(() => { + config.set('userManagement.emails.mode', 'smtp'); + }); - config.set('userManagement.emails.mode', 'smtp'); + test('should succeed if emailing is not set up', async () => { + const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]); - const invalidPayloads = [ - randomEmail(), - [randomEmail()], - {}, - [{ name: randomName() }], - [{ email: randomName() }], - ]; + expect(response.statusCode).toBe(200); + expect(response.body.data[0].user.inviteAcceptUrl).toBeDefined(); + }); - await Promise.all( - invalidPayloads.map(async (invalidPayload) => { - const response = await authOwnerAgent.post('/users').send(invalidPayload); - expect(response.statusCode).toBe(400); + test('should fail if user management is disabled', async () => { + config.set('userManagement.disabled', true); + config.set('userManagement.isInstanceOwnerSetUp', false); - const users = await Db.collections.User.find(); - expect(users.length).toBe(1); // DB unaffected - }), - ); + const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]); + + expect(response.statusCode).toBe(400); + }); + + test('should email invites and create user shells but ignore existing', async () => { + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const memberShell = await testDb.createUserShell(globalMemberRole); + + const testEmails = [ + randomEmail(), + randomEmail().toUpperCase(), + memberShell.email, + member.email, + ]; + + 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(id).not.toBe(member.id); + + const lowerCasedEmail = receivedEmail.toLowerCase(); + expect(receivedEmail).toBe(lowerCasedEmail); + expect(payload.some(({ email }) => email.toLowerCase() === lowerCasedEmail)).toBe(true); + + if (error) { + expect(error).toBe('Email could not be sent'); + } + + const storedUser = await Db.collections.User.findOneByOrFail({ id }); + const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } = + storedUser; + + expect(firstName).toBeNull(); + expect(lastName).toBeNull(); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeNull(); + expect(resetPasswordToken).toBeNull(); + } + }); + + test('should fail with invalid inputs', async () => { + const invalidPayloads = [ + randomEmail(), + [randomEmail()], + {}, + [{ name: randomName() }], + [{ email: randomName() }], + ]; + + 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 + }), + ); + }); + + test('should ignore an empty payload', async () => { + const response = await authOwnerAgent.post('/users').send([]); + + const { data } = response.body; + + expect(response.statusCode).toBe(200); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBe(0); + + const users = await Db.collections.User.find(); + expect(users.length).toBe(1); + }); }); -test('POST /users should ignore an empty payload', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); +describe('POST /users/:id/reinvite', () => { + beforeEach(() => { + config.set('userManagement.emails.mode', 'smtp'); + // those configs are needed to make sure the reinvite email is sent,because of this check isEmailSetUp() + config.set('userManagement.emails.smtp.host', 'host'); + config.set('userManagement.emails.smtp.auth.user', 'user'); + config.set('userManagement.emails.smtp.auth.pass', 'pass'); + }); - config.set('userManagement.emails.mode', 'smtp'); + test('should send reinvite, but fail if user already accepted invite', async () => { + const email = randomEmail(); + const payload = [{ email }]; + const response = await authOwnerAgent.post('/users').send(payload); - const response = await authAgent(owner).post('/users').send([]); + expect(response.statusCode).toBe(200); - const { data } = response.body; + const { data } = response.body; + const invitedUserId = data[0].user.id; + const reinviteResponse = await authOwnerAgent.post(`/users/${invitedUserId}/reinvite`); - expect(response.statusCode).toBe(200); - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBe(0); + expect(reinviteResponse.statusCode).toBe(200); - const users = await Db.collections.User.find(); - expect(users.length).toBe(1); + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const reinviteMemberResponse = await authOwnerAgent.post(`/users/${member.id}/reinvite`); + + expect(reinviteMemberResponse.statusCode).toBe(400); + }); }); -test('POST /users/:id/reinvite should send reinvite, but fail if user already accepted invite', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); +describe('UserManagementMailer expect NodeMailer.verifyConnection', () => { + let mockInit: jest.SpyInstance, []>; + let mockVerifyConnection: jest.SpyInstance, []>; - config.set('userManagement.emails.mode', 'smtp'); + beforeAll(() => { + mockVerifyConnection = jest + .spyOn(NodeMailer.prototype, 'verifyConnection') + .mockImplementation(async () => {}); + mockInit = jest.spyOn(NodeMailer.prototype, 'init').mockImplementation(async () => {}); + }); - // those configs are needed to make sure the reinvite email is sent,because of this check isEmailSetUp() - config.set('userManagement.emails.smtp.host', 'host'); - config.set('userManagement.emails.smtp.auth.user', 'user'); - config.set('userManagement.emails.smtp.auth.pass', 'pass'); + afterAll(() => { + mockVerifyConnection.mockRestore(); + mockInit.mockRestore(); + }); - const email = randomEmail(); - const payload = [{ email }]; - const response = await authOwnerAgent.post('/users').send(payload); + test('not be called when SMTP not set up', async () => { + const userManagementMailer = new UserManagementMailer(); + // NodeMailer.verifyConnection gets called only explicitly + expect(async () => await userManagementMailer.verifyConnection()).rejects.toThrow(); - expect(response.statusCode).toBe(200); + expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(0); + }); - const { data } = response.body; - const invitedUserId = data[0].user.id; - const reinviteResponse = await authOwnerAgent.post(`/users/${invitedUserId}/reinvite`); + test('to be called when SMTP set up', async () => { + // host needs to be set, otherwise smtp is skipped + config.set('userManagement.emails.smtp.host', 'host'); + config.set('userManagement.emails.mode', 'smtp'); - expect(reinviteResponse.statusCode).toBe(200); - - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const reinviteMemberResponse = await authOwnerAgent.post(`/users/${member.id}/reinvite`); - - expect(reinviteMemberResponse.statusCode).toBe(400); -}); - -test('UserManagementMailer expect NodeMailer.verifyConnection not be called when SMTP not set up', async () => { - const mockVerifyConnection = jest.spyOn(NodeMailer.prototype, 'verifyConnection'); - mockVerifyConnection.mockImplementation(async () => {}); - - const userManagementMailer = new UserManagementMailer(); - // NodeMailer.verifyConnection gets called only explicitly - expect(async () => await userManagementMailer.verifyConnection()).rejects.toThrow(); - - expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(0); - - mockVerifyConnection.mockRestore(); -}); - -test('UserManagementMailer expect NodeMailer.verifyConnection to be called when SMTP set up', async () => { - const mockVerifyConnection = jest.spyOn(NodeMailer.prototype, 'verifyConnection'); - mockVerifyConnection.mockImplementation(async () => {}); - const mockInit = jest.spyOn(NodeMailer.prototype, 'init'); - mockInit.mockImplementation(async () => {}); - - // host needs to be set, otherwise smtp is skipped - config.set('userManagement.emails.smtp.host', 'host'); - config.set('userManagement.emails.mode', 'smtp'); - - const userManagementMailer = new UserManagementMailer(); - // NodeMailer.verifyConnection gets called only explicitly - expect(async () => await userManagementMailer.verifyConnection()).not.toThrow(); - - // expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(1); - mockVerifyConnection.mockRestore(); - mockInit.mockRestore(); + const userManagementMailer = new UserManagementMailer(); + // NodeMailer.verifyConnection gets called only explicitly + expect(async () => await userManagementMailer.verifyConnection()).not.toThrow(); + }); }); diff --git a/packages/cli/test/integration/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows.controller.ee.test.ts index d43e9f790e..c544416eee 100644 --- a/packages/cli/test/integration/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows.controller.ee.test.ts @@ -1,89 +1,89 @@ -import express from 'express'; +import type { SuperAgentTest } from 'supertest'; import { v4 as uuid } from 'uuid'; -import { INode } from 'n8n-workflow'; +import type { INode } from 'n8n-workflow'; + +import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; +import type { User } from '@db/entities/User'; +import config from '@/config'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; import { createWorkflow } from './shared/testDb'; -import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; -import type { Role } from '@db/entities/Role'; -import config from '@/config'; -import type { AuthAgent, SaveCredentialFunction } from './shared/types'; +import type { SaveCredentialFunction } from './shared/types'; import { makeWorkflow } from './shared/utils'; import { randomCredentialPayload } from './shared/random'; -import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; -let app: express.Application; -let globalOwnerRole: Role; -let globalMemberRole: Role; -let credentialOwnerRole: Role; -let authAgent: AuthAgent; +let owner: User; +let member: User; +let anotherMember: User; +let authOwnerAgent: SuperAgentTest; +let authMemberAgent: SuperAgentTest; +let authAnotherMemberAgent: SuperAgentTest; let saveCredential: SaveCredentialFunction; -let isSharingEnabled: jest.SpyInstance; -let workflowRunner: ActiveWorkflowRunner; let sharingSpy: jest.SpyInstance; beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['workflows'] }); + const app = await utils.initTestServer({ endpointGroups: ['workflows'] }); - globalOwnerRole = await testDb.getGlobalOwnerRole(); - globalMemberRole = await testDb.getGlobalMemberRole(); - credentialOwnerRole = await testDb.getCredentialOwnerRole(); + const globalOwnerRole = await testDb.getGlobalOwnerRole(); + const globalMemberRole = await testDb.getGlobalMemberRole(); + const credentialOwnerRole = await testDb.getCredentialOwnerRole(); + + owner = await testDb.createUser({ globalRole: globalOwnerRole }); + member = await testDb.createUser({ globalRole: globalMemberRole }); + anotherMember = await testDb.createUser({ globalRole: globalMemberRole }); + + const authAgent = utils.createAuthAgent(app); + authOwnerAgent = authAgent(owner); + authMemberAgent = authAgent(member); + authAnotherMemberAgent = authAgent(anotherMember); saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole); - - authAgent = utils.createAuthAgent(app); - - isSharingEnabled = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); + sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); await utils.initNodeTypes(); - workflowRunner = await utils.initActiveWorkflowRunner(); config.set('enterprise.features.sharing', true); - sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); // @TODO: Remove on release }); beforeEach(async () => { - await testDb.truncate(['User', 'Workflow', 'SharedWorkflow']); + await testDb.truncate(['Workflow', 'SharedWorkflow']); }); afterAll(async () => { await testDb.terminate(); }); -test('Router should switch dynamically', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); +describe('router should switch based on flag', () => { + let savedWorkflowId: string; - const createWorkflowResponse = await authAgent(owner).post('/workflows').send(makeWorkflow()); - const { id } = createWorkflowResponse.body.data; + beforeEach(async () => { + const createWorkflowResponse = await authOwnerAgent.post('/workflows').send(makeWorkflow()); + savedWorkflowId = createWorkflowResponse.body.data.id; + }); - // free router + test('when sharing is disabled', async () => { + sharingSpy.mockReturnValueOnce(false); - isSharingEnabled.mockReturnValueOnce(false); + await authOwnerAgent + .put(`/workflows/${savedWorkflowId}/share`) + .send({ shareWithIds: [member.id] }) + .expect(404); + }); - const freeShareResponse = await authAgent(owner) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member.id] }); - - expect(freeShareResponse.status).toBe(404); - - // EE router - - const paidShareResponse = await authAgent(owner) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member.id] }); - - expect(paidShareResponse.status).toBe(200); + test('when sharing is enabled', async () => { + await authOwnerAgent + .put(`/workflows/${savedWorkflowId}/share`) + .send({ shareWithIds: [member.id] }) + .expect(200); + }); }); describe('PUT /workflows/:id', () => { test('PUT /workflows/:id/share should save sharing with new users', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); const workflow = await createWorkflow({}, owner); - const response = await authAgent(owner) + const response = await authOwnerAgent .put(`/workflows/${workflow.id}/share`) .send({ shareWithIds: [member.id] }); @@ -94,10 +94,9 @@ describe('PUT /workflows/:id', () => { }); test('PUT /workflows/:id/share should succeed when sharing with invalid user-id', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const workflow = await createWorkflow({}, owner); - const response = await authAgent(owner) + const response = await authOwnerAgent .put(`/workflows/${workflow.id}/share`) .send({ shareWithIds: [uuid()] }); @@ -108,12 +107,9 @@ describe('PUT /workflows/:id', () => { }); test('PUT /workflows/:id/share should allow sharing with multiple users', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const anotherMember = await testDb.createUser({ globalRole: globalMemberRole }); const workflow = await createWorkflow({}, owner); - const response = await authAgent(owner) + const response = await authOwnerAgent .put(`/workflows/${workflow.id}/share`) .send({ shareWithIds: [member.id, anotherMember.id] }); @@ -124,13 +120,8 @@ describe('PUT /workflows/:id', () => { }); test('PUT /workflows/:id/share should override sharing', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const anotherMember = await testDb.createUser({ globalRole: globalMemberRole }); const workflow = await createWorkflow({}, owner); - const authOwnerAgent = authAgent(owner); - const response = await authOwnerAgent .put(`/workflows/${workflow.id}/share`) .send({ shareWithIds: [member.id, anotherMember.id] }); @@ -152,8 +143,6 @@ describe('PUT /workflows/:id', () => { describe('GET /workflows', () => { test('should return workflows without nodes, sharing and credential usage details', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); const tag = await testDb.createTag({ name: 'test' }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); @@ -183,7 +172,7 @@ describe('GET /workflows', () => { await testDb.shareWorkflowWithUsers(workflow, [member]); - const response = await authAgent(owner).get('/workflows'); + const response = await authOwnerAgent.get('/workflows'); const [fetchedWorkflow] = response.body.data; @@ -192,42 +181,37 @@ describe('GET /workflows', () => { id: owner.id, }); - expect(fetchedWorkflow.sharedWith).not.toBeDefined() - expect(fetchedWorkflow.usedCredentials).not.toBeDefined() - expect(fetchedWorkflow.nodes).not.toBeDefined() + expect(fetchedWorkflow.sharedWith).not.toBeDefined(); + expect(fetchedWorkflow.usedCredentials).not.toBeDefined(); + expect(fetchedWorkflow.nodes).not.toBeDefined(); expect(fetchedWorkflow.tags).toEqual( expect.arrayContaining([ expect.objectContaining({ id: expect.any(String), - name: expect.any(String) - }) - ]) - ) + name: expect.any(String), + }), + ]), + ); }); }); describe('GET /workflows/:id', () => { test('GET should fail with invalid id due to route rule', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const response = await authAgent(owner).get('/workflows/potatoes'); + const response = await authOwnerAgent.get('/workflows/potatoes'); expect(response.statusCode).toBe(404); }); test('GET should return 404 for non existing workflow', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - - const response = await authAgent(owner).get('/workflows/9001'); + const response = await authOwnerAgent.get('/workflows/9001'); expect(response.statusCode).toBe(404); }); test('GET should return a workflow with owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const workflow = await createWorkflow({}, owner); - const response = await authAgent(owner).get(`/workflows/${workflow.id}`); + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); expect(response.statusCode).toBe(200); expect(response.body.data.ownedBy).toMatchObject({ @@ -241,12 +225,10 @@ describe('GET /workflows/:id', () => { }); test('GET should return shared workflow with user data', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); const workflow = await createWorkflow({}, owner); await testDb.shareWorkflowWithUsers(workflow, [member]); - const response = await authAgent(owner).get(`/workflows/${workflow.id}`); + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); expect(response.statusCode).toBe(200); expect(response.body.data.ownedBy).toMatchObject({ @@ -266,13 +248,10 @@ describe('GET /workflows/:id', () => { }); test('GET should return all sharees', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member1 = await testDb.createUser({ globalRole: globalMemberRole }); - const member2 = await testDb.createUser({ globalRole: globalMemberRole }); const workflow = await createWorkflow({}, owner); - await testDb.shareWorkflowWithUsers(workflow, [member1, member2]); + await testDb.shareWorkflowWithUsers(workflow, [member, anotherMember]); - const response = await authAgent(owner).get(`/workflows/${workflow.id}`); + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); expect(response.statusCode).toBe(200); expect(response.body.data.ownedBy).toMatchObject({ @@ -286,7 +265,6 @@ describe('GET /workflows/:id', () => { }); test('GET should return workflow with credentials owned by user', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const workflowPayload = makeWorkflow({ @@ -295,7 +273,7 @@ describe('GET /workflows/:id', () => { }); const workflow = await createWorkflow(workflowPayload, owner); - const response = await authAgent(owner).get(`/workflows/${workflow.id}`); + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); expect(response.statusCode).toBe(200); expect(response.body.data.usedCredentials).toMatchObject([ @@ -310,8 +288,6 @@ describe('GET /workflows/:id', () => { }); test('GET should return workflow with credentials saying owner does not have access when not shared', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const workflowPayload = makeWorkflow({ @@ -320,7 +296,7 @@ describe('GET /workflows/:id', () => { }); const workflow = await createWorkflow(workflowPayload, owner); - const response = await authAgent(owner).get(`/workflows/${workflow.id}`); + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); expect(response.statusCode).toBe(200); expect(response.body.data.usedCredentials).toMatchObject([ @@ -335,18 +311,16 @@ describe('GET /workflows/:id', () => { }); test('GET should return workflow with credentials for all users with or without access', async () => { - const member1 = await testDb.createUser({ globalRole: globalMemberRole }); - const member2 = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const workflowPayload = makeWorkflow({ withPinData: false, withCredential: { id: savedCredential.id, name: savedCredential.name }, }); - const workflow = await createWorkflow(workflowPayload, member1); - await testDb.shareWorkflowWithUsers(workflow, [member2]); + const workflow = await createWorkflow(workflowPayload, member); + await testDb.shareWorkflowWithUsers(workflow, [anotherMember]); - const responseMember1 = await authAgent(member1).get(`/workflows/${workflow.id}`); + const responseMember1 = await authMemberAgent.get(`/workflows/${workflow.id}`); expect(responseMember1.statusCode).toBe(200); expect(responseMember1.body.data.usedCredentials).toMatchObject([ { @@ -357,7 +331,7 @@ describe('GET /workflows/:id', () => { ]); expect(responseMember1.body.data.sharedWith).toHaveLength(1); - const responseMember2 = await authAgent(member2).get(`/workflows/${workflow.id}`); + const responseMember2 = await authAnotherMemberAgent.get(`/workflows/${workflow.id}`); expect(responseMember2.statusCode).toBe(200); expect(responseMember2.body.data.usedCredentials).toMatchObject([ { @@ -370,20 +344,18 @@ describe('GET /workflows/:id', () => { }); test('GET should return workflow with credentials for all users with access', async () => { - const member1 = await testDb.createUser({ globalRole: globalMemberRole }); - const member2 = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); // Both users have access to the credential (none is owner) - await testDb.shareCredentialWithUsers(savedCredential, [member2]); + await testDb.shareCredentialWithUsers(savedCredential, [anotherMember]); const workflowPayload = makeWorkflow({ withPinData: false, withCredential: { id: savedCredential.id, name: savedCredential.name }, }); - const workflow = await createWorkflow(workflowPayload, member1); - await testDb.shareWorkflowWithUsers(workflow, [member2]); + const workflow = await createWorkflow(workflowPayload, member); + await testDb.shareWorkflowWithUsers(workflow, [anotherMember]); - const responseMember1 = await authAgent(member1).get(`/workflows/${workflow.id}`); + const responseMember1 = await authMemberAgent.get(`/workflows/${workflow.id}`); expect(responseMember1.statusCode).toBe(200); expect(responseMember1.body.data.usedCredentials).toMatchObject([ { @@ -394,7 +366,7 @@ describe('GET /workflows/:id', () => { ]); expect(responseMember1.body.data.sharedWith).toHaveLength(1); - const responseMember2 = await authAgent(member2).get(`/workflows/${workflow.id}`); + const responseMember2 = await authAnotherMemberAgent.get(`/workflows/${workflow.id}`); expect(responseMember2.statusCode).toBe(200); expect(responseMember2.body.data.usedCredentials).toMatchObject([ { @@ -409,33 +381,26 @@ describe('GET /workflows/:id', () => { describe('POST /workflows', () => { it('Should create a workflow that uses no credential', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const workflow = makeWorkflow({ withPinData: false }); - const response = await authAgent(owner).post('/workflows').send(workflow); + const response = await authOwnerAgent.post('/workflows').send(workflow); expect(response.statusCode).toBe(200); }); it('Should save a new workflow with credentials', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const workflow = makeWorkflow({ withPinData: false, withCredential: { id: savedCredential.id, name: savedCredential.name }, }); - const response = await authAgent(owner).post('/workflows').send(workflow); + const response = await authOwnerAgent.post('/workflows').send(workflow); expect(response.statusCode).toBe(200); }); it('Should not allow saving a workflow using credential you have no access', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - // Credential belongs to owner, member cannot use it. const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const workflow = makeWorkflow({ @@ -443,7 +408,7 @@ describe('POST /workflows', () => { withCredential: { id: savedCredential.id, name: savedCredential.name }, }); - const response = await authAgent(member).post('/workflows').send(workflow); + const response = await authMemberAgent.post('/workflows').send(workflow); expect(response.statusCode).toBe(400); expect(response.body.message).toBe( @@ -452,9 +417,6 @@ describe('POST /workflows', () => { }); it('Should allow owner to save a workflow using credential owned by others', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - // Credential belongs to owner, member cannot use it. const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const workflow = makeWorkflow({ @@ -462,32 +424,27 @@ describe('POST /workflows', () => { withCredential: { id: savedCredential.id, name: savedCredential.name }, }); - const response = await authAgent(owner).post('/workflows').send(workflow); + const response = await authOwnerAgent.post('/workflows').send(workflow); expect(response.statusCode).toBe(200); }); it('Should allow saving a workflow using a credential owned by others and shared with you', async () => { - const member1 = await testDb.createUser({ globalRole: globalMemberRole }); - const member2 = await testDb.createUser({ globalRole: globalMemberRole }); - - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); - await testDb.shareCredentialWithUsers(savedCredential, [member2]); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); + await testDb.shareCredentialWithUsers(savedCredential, [anotherMember]); const workflow = makeWorkflow({ withPinData: false, withCredential: { id: savedCredential.id, name: savedCredential.name }, }); - const response = await authAgent(member2).post('/workflows').send(workflow); + const response = await authAnotherMemberAgent.post('/workflows').send(workflow); expect(response.statusCode).toBe(200); }); }); describe('PATCH /workflows/:id - validate credential permissions to user', () => { it('Should succeed when saving unchanged workflow nodes', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const workflow = { name: 'test', @@ -511,10 +468,10 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () => ], }; - const createResponse = await authAgent(owner).post('/workflows').send(workflow); + const createResponse = await authOwnerAgent.post('/workflows').send(workflow); const { id, versionId } = createResponse.body.data; - const response = await authAgent(owner).patch(`/workflows/${id}`).send({ + const response = await authOwnerAgent.patch(`/workflows/${id}`).send({ name: 'new name', versionId, }); @@ -523,9 +480,6 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () => }); it('Should allow owner to add node containing credential not shared with the owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const workflow = { name: 'test', @@ -549,38 +503,33 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () => ], }; - const createResponse = await authAgent(owner).post('/workflows').send(workflow); + const createResponse = await authOwnerAgent.post('/workflows').send(workflow); const { id, versionId } = createResponse.body.data; - const response = await authAgent(owner) - .patch(`/workflows/${id}`) - .send({ - versionId, - nodes: [ - { - id: 'uuid-1234', - name: 'Start', - parameters: {}, - position: [-20, 260], - type: 'n8n-nodes-base.start', - typeVersion: 1, - credentials: { - default: { - id: savedCredential.id, - name: savedCredential.name, - }, + const response = await authOwnerAgent.patch(`/workflows/${id}`).send({ + versionId, + nodes: [ + { + id: 'uuid-1234', + name: 'Start', + parameters: {}, + position: [-20, 260], + type: 'n8n-nodes-base.start', + typeVersion: 1, + credentials: { + default: { + id: savedCredential.id, + name: savedCredential.name, }, }, - ], - }); + }, + ], + }); expect(response.statusCode).toBe(200); }); it('Should prevent member from adding node containing credential inaccessible to member', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const workflow = { @@ -605,48 +554,43 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () => ], }; - const createResponse = await authAgent(owner).post('/workflows').send(workflow); + const createResponse = await authOwnerAgent.post('/workflows').send(workflow); const { id, versionId } = createResponse.body.data; - const response = await authAgent(member) - .patch(`/workflows/${id}`) - .send({ - versionId, - nodes: [ - { - id: 'uuid-1234', - name: 'Start', - parameters: {}, - position: [-20, 260], - type: 'n8n-nodes-base.start', - typeVersion: 1, - credentials: {}, - }, - { - id: 'uuid-12345', - name: 'Start', - parameters: {}, - position: [-20, 260], - type: 'n8n-nodes-base.start', - typeVersion: 1, - credentials: { - default: { - id: savedCredential.id, - name: savedCredential.name, - }, + const response = await authMemberAgent.patch(`/workflows/${id}`).send({ + versionId, + nodes: [ + { + id: 'uuid-1234', + name: 'Start', + parameters: {}, + position: [-20, 260], + type: 'n8n-nodes-base.start', + typeVersion: 1, + credentials: {}, + }, + { + id: 'uuid-12345', + name: 'Start', + parameters: {}, + position: [-20, 260], + type: 'n8n-nodes-base.start', + typeVersion: 1, + credentials: { + default: { + id: savedCredential.id, + name: savedCredential.name, }, }, - ], - }); + }, + ], + }); expect(response.statusCode).toBe(400); }); it('Should succeed but prevent modifying node attributes other than position, name and disabled', async () => { - const member1 = await testDb.createUser({ globalRole: globalMemberRole }); - const member2 = await testDb.createUser({ globalRole: globalMemberRole }); - - const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); + const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const originalNodes: INode[] = [ { @@ -714,14 +658,12 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () => nodes: originalNodes, }; - const createResponse = await authAgent(member1).post('/workflows').send(workflow); + const createResponse = await authMemberAgent.post('/workflows').send(workflow); const { id, versionId } = createResponse.body.data; - await authAgent(member1) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member2.id] }); + await authMemberAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [anotherMember.id] }); - const response = await authAgent(member2).patch(`/workflows/${id}`).send({ + const response = await authAnotherMemberAgent.patch(`/workflows/${id}`).send({ versionId, nodes: changedNodes, }); @@ -733,29 +675,24 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () => describe('PATCH /workflows/:id - validate interim updates', () => { it('should block owner updating workflow nodes on interim update by member', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - // owner creates and shares workflow - const createResponse = await authAgent(owner).post('/workflows').send(makeWorkflow()); + const createResponse = await authOwnerAgent.post('/workflows').send(makeWorkflow()); const { id, versionId: ownerVersionId } = createResponse.body.data; - await authAgent(owner) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member.id] }); + await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] }); // member accesses and updates workflow name - const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); + const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`); const { versionId: memberVersionId } = memberGetResponse.body.data; - await authAgent(member) + await authMemberAgent .patch(`/workflows/${id}`) .send({ name: 'Update by member', versionId: memberVersionId }); // owner blocked from updating workflow nodes - const updateAttemptResponse = await authAgent(owner) + const updateAttemptResponse = await authOwnerAgent .patch(`/workflows/${id}`) .send({ nodes: [], versionId: ownerVersionId }); @@ -764,38 +701,33 @@ describe('PATCH /workflows/:id - validate interim updates', () => { }); it('should block member updating workflow nodes on interim update by owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - // owner creates, updates and shares workflow - const createResponse = await authAgent(owner).post('/workflows').send(makeWorkflow()); + const createResponse = await authOwnerAgent.post('/workflows').send(makeWorkflow()); const { id, versionId: ownerFirstVersionId } = createResponse.body.data; - const updateResponse = await authAgent(owner) + const updateResponse = await authOwnerAgent .patch(`/workflows/${id}`) .send({ name: 'Update by owner', versionId: ownerFirstVersionId }); const { versionId: ownerSecondVersionId } = updateResponse.body.data; - await authAgent(owner) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member.id] }); + await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] }); // member accesses workflow - const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); + const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`); const { versionId: memberVersionId } = memberGetResponse.body.data; // owner re-updates workflow - await authAgent(owner) + await authOwnerAgent .patch(`/workflows/${id}`) .send({ name: 'Owner update again', versionId: ownerSecondVersionId }); // member blocked from updating workflow - const updateAttemptResponse = await authAgent(member) + const updateAttemptResponse = await authMemberAgent .patch(`/workflows/${id}`) .send({ nodes: [], versionId: memberVersionId }); @@ -804,28 +736,23 @@ describe('PATCH /workflows/:id - validate interim updates', () => { }); it('should block owner activation on interim activation by member', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - // owner creates and shares workflow - const createResponse = await authAgent(owner).post('/workflows').send(makeWorkflow()); + const createResponse = await authOwnerAgent.post('/workflows').send(makeWorkflow()); const { id, versionId: ownerVersionId } = createResponse.body.data; - await authAgent(owner) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member.id] }); + await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] }); // member accesses and activates workflow - const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); + const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`); const { versionId: memberVersionId } = memberGetResponse.body.data; - await authAgent(member) + await authMemberAgent .patch(`/workflows/${id}`) .send({ active: true, versionId: memberVersionId }); // owner blocked from activating workflow - const activationAttemptResponse = await authAgent(owner) + const activationAttemptResponse = await authOwnerAgent .patch(`/workflows/${id}`) .send({ active: true, versionId: ownerVersionId }); @@ -834,37 +761,32 @@ describe('PATCH /workflows/:id - validate interim updates', () => { }); it('should block member activation on interim activation by owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - // owner creates, updates and shares workflow - const createResponse = await authAgent(owner).post('/workflows').send(makeWorkflow()); + const createResponse = await authOwnerAgent.post('/workflows').send(makeWorkflow()); const { id, versionId: ownerFirstVersionId } = createResponse.body.data; - const updateResponse = await authAgent(owner) + const updateResponse = await authOwnerAgent .patch(`/workflows/${id}`) .send({ name: 'Update by owner', versionId: ownerFirstVersionId }); const { versionId: ownerSecondVersionId } = updateResponse.body.data; - await authAgent(owner) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member.id] }); + await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] }); // member accesses workflow - const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); + const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`); const { versionId: memberVersionId } = memberGetResponse.body.data; // owner activates workflow - await authAgent(owner) + await authOwnerAgent .patch(`/workflows/${id}`) .send({ active: true, versionId: ownerSecondVersionId }); // member blocked from activating workflow - const updateAttemptResponse = await authAgent(member) + const updateAttemptResponse = await authMemberAgent .patch(`/workflows/${id}`) .send({ active: true, versionId: memberVersionId }); @@ -873,31 +795,26 @@ describe('PATCH /workflows/:id - validate interim updates', () => { }); it('should block member updating workflow settings on interim update by owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - // owner creates and shares workflow - const createResponse = await authAgent(owner).post('/workflows').send(makeWorkflow()); + const createResponse = await authOwnerAgent.post('/workflows').send(makeWorkflow()); const { id, versionId: ownerVersionId } = createResponse.body.data; - await authAgent(owner) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member.id] }); + await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] }); // member accesses workflow - const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); + const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`); const { versionId: memberVersionId } = memberGetResponse.body.data; // owner updates workflow name - await authAgent(owner) + await authOwnerAgent .patch(`/workflows/${id}`) .send({ name: 'Another name', versionId: ownerVersionId }); // member blocked from updating workflow settings - const updateAttemptResponse = await authAgent(member) + const updateAttemptResponse = await authMemberAgent .patch(`/workflows/${id}`) .send({ settings: { saveManualExecutions: true }, versionId: memberVersionId }); @@ -906,31 +823,26 @@ describe('PATCH /workflows/:id - validate interim updates', () => { }); it('should block member updating workflow name on interim update by owner', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - // owner creates and shares workflow - const createResponse = await authAgent(owner).post('/workflows').send(makeWorkflow()); + const createResponse = await authOwnerAgent.post('/workflows').send(makeWorkflow()); const { id, versionId: ownerVersionId } = createResponse.body.data; - await authAgent(owner) - .put(`/workflows/${id}/share`) - .send({ shareWithIds: [member.id] }); + await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] }); // member accesses workflow - const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); + const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`); const { versionId: memberVersionId } = memberGetResponse.body.data; // owner updates workflow settings - await authAgent(owner) + await authOwnerAgent .patch(`/workflows/${id}`) .send({ settings: { saveManualExecutions: true }, versionId: ownerVersionId }); // member blocked from updating workflow name - const updateAttemptResponse = await authAgent(member) + const updateAttemptResponse = await authMemberAgent .patch(`/workflows/${id}`) .send({ settings: { saveManualExecutions: true }, versionId: memberVersionId }); diff --git a/packages/cli/test/integration/workflows.controller.test.ts b/packages/cli/test/integration/workflows.controller.test.ts index 2d56589531..dd0d236ceb 100644 --- a/packages/cli/test/integration/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows.controller.test.ts @@ -1,78 +1,74 @@ -import express from 'express'; +import { SuperAgentTest } from 'supertest'; +import type { IPinData } from 'n8n-workflow'; + +import type { User } from '@db/entities/User'; +import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; -import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; - -import type { Role } from '@db/entities/Role'; -import type { IPinData } from 'n8n-workflow'; import { makeWorkflow, MOCK_PINDATA } from './shared/utils'; -let app: express.Application; -let globalOwnerRole: Role; - -// mock whether sharing is enabled or not -jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(false); +let ownerShell: User; +let authOwnerAgent: SuperAgentTest; beforeAll(async () => { - app = await utils.initTestServer({ endpointGroups: ['workflows'] }); + const app = await utils.initTestServer({ endpointGroups: ['workflows'] }); + const globalOwnerRole = await testDb.getGlobalOwnerRole(); + ownerShell = await testDb.createUserShell(globalOwnerRole); + authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); - globalOwnerRole = await testDb.getGlobalOwnerRole(); + // mock whether sharing is enabled or not + jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(false); }); beforeEach(async () => { - await testDb.truncate(['User', 'Workflow', 'SharedWorkflow']); + await testDb.truncate(['Workflow', 'SharedWorkflow']); }); afterAll(async () => { await testDb.terminate(); }); -test('POST /workflows should store pin data for node in workflow', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); +describe('POST /workflows', () => { + test('should store pin data for node in workflow', async () => { + const workflow = makeWorkflow({ withPinData: true }); - const workflow = makeWorkflow({ withPinData: true }); + const response = await authOwnerAgent.post('/workflows').send(workflow); - const response = await authOwnerAgent.post('/workflows').send(workflow); + expect(response.statusCode).toBe(200); - expect(response.statusCode).toBe(200); + const { pinData } = response.body.data as { pinData: IPinData }; - const { pinData } = response.body.data as { pinData: IPinData }; + expect(pinData).toMatchObject(MOCK_PINDATA); + }); - expect(pinData).toMatchObject(MOCK_PINDATA); + test('should set pin data to null if no pin data', async () => { + const workflow = makeWorkflow({ withPinData: false }); + + const response = await authOwnerAgent.post('/workflows').send(workflow); + + expect(response.statusCode).toBe(200); + + const { pinData } = response.body.data as { pinData: IPinData }; + + expect(pinData).toBeNull(); + }); }); -test('POST /workflows should set pin data to null if no pin data', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); +describe('GET /workflows/:id', () => { + test('should return pin data', async () => { + const workflow = makeWorkflow({ withPinData: true }); - const workflow = makeWorkflow({ withPinData: false }); + const workflowCreationResponse = await authOwnerAgent.post('/workflows').send(workflow); - const response = await authOwnerAgent.post('/workflows').send(workflow); + const { id } = workflowCreationResponse.body.data as { id: string }; - expect(response.statusCode).toBe(200); + const workflowRetrievalResponse = await authOwnerAgent.get(`/workflows/${id}`); - const { pinData } = response.body.data as { pinData: IPinData }; + expect(workflowRetrievalResponse.statusCode).toBe(200); - expect(pinData).toBeNull(); -}); - -test('GET /workflows/:id should return pin data', async () => { - const ownerShell = await testDb.createUserShell(globalOwnerRole); - const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); - - const workflow = makeWorkflow({ withPinData: true }); - - const workflowCreationResponse = await authOwnerAgent.post('/workflows').send(workflow); - - const { id } = workflowCreationResponse.body.data as { id: string }; - - const workflowRetrievalResponse = await authOwnerAgent.get(`/workflows/${id}`); - - expect(workflowRetrievalResponse.statusCode).toBe(200); - - const { pinData } = workflowRetrievalResponse.body.data as { pinData: IPinData }; - - expect(pinData).toMatchObject(MOCK_PINDATA); + const { pinData } = workflowRetrievalResponse.body.data as { pinData: IPinData }; + + expect(pinData).toMatchObject(MOCK_PINDATA); + }); }); diff --git a/packages/cli/test/unit/ActiveWorkflowRunner.test.ts b/packages/cli/test/unit/ActiveWorkflowRunner.test.ts index 557391d1be..e3290446e0 100644 --- a/packages/cli/test/unit/ActiveWorkflowRunner.test.ts +++ b/packages/cli/test/unit/ActiveWorkflowRunner.test.ts @@ -18,7 +18,7 @@ import { User } from '@/databases/entities/User'; import { getLogger } from '@/Logger'; import { randomEmail, randomName } from '../integration/shared/random'; import * as Helpers from './Helpers'; -import { WorkflowExecuteAdditionalData } from '@/index'; +import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import { WorkflowRunner } from '@/WorkflowRunner'; import { mock } from 'jest-mock-extended'; diff --git a/packages/cli/test/unit/CredentialsHelper.test.ts b/packages/cli/test/unit/CredentialsHelper.test.ts index efa43aeb13..8ede15e4fd 100644 --- a/packages/cli/test/unit/CredentialsHelper.test.ts +++ b/packages/cli/test/unit/CredentialsHelper.test.ts @@ -11,20 +11,59 @@ import { } from 'n8n-workflow'; import { CredentialsHelper } from '@/CredentialsHelper'; import { CredentialTypes } from '@/CredentialTypes'; -import * as Helpers from './Helpers'; import { Container } from 'typedi'; import { NodeTypes } from '@/NodeTypes'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -const TEST_ENCRYPTION_KEY = 'test'; -const mockNodesAndCredentials: INodesAndCredentials = { - loaded: { nodes: {}, credentials: {} }, - known: { nodes: {}, credentials: {} }, - credentialTypes: {} as ICredentialTypes, -}; -Container.set(LoadNodesAndCredentials, mockNodesAndCredentials); - describe('CredentialsHelper', () => { + const TEST_ENCRYPTION_KEY = 'test'; + + const mockNodesAndCredentials: INodesAndCredentials = { + loaded: { + nodes: { + 'test.set': { + sourcePath: '', + type: { + description: { + displayName: 'Set', + name: 'set', + group: ['input'], + version: 1, + description: 'Sets a value', + defaults: { + name: 'Set', + color: '#0000FF', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Value1', + name: 'value1', + type: 'string', + default: 'default-value1', + }, + { + displayName: 'Value2', + name: 'value2', + type: 'string', + default: 'default-value2', + }, + ], + }, + }, + }, + }, + credentials: {}, + }, + known: { nodes: {}, credentials: {} }, + credentialTypes: {} as ICredentialTypes, + }; + + Container.set(LoadNodesAndCredentials, mockNodesAndCredentials); + + const nodeTypes = Container.get(NodeTypes); + describe('authenticate', () => { const tests: Array<{ description: string; @@ -219,8 +258,6 @@ describe('CredentialsHelper', () => { qs: {}, }; - const nodeTypes = Helpers.NodeTypes() as unknown as NodeTypes; - const workflow = new Workflow({ nodes: [node], connections: {}, diff --git a/packages/cli/test/unit/Events.test.ts b/packages/cli/test/unit/Events.test.ts index 0c72e8f405..25c2897352 100644 --- a/packages/cli/test/unit/Events.test.ts +++ b/packages/cli/test/unit/Events.test.ts @@ -1,32 +1,37 @@ -import { LoggerProxy, WorkflowExecuteMode } from 'n8n-workflow'; -import { QueryFailedError } from 'typeorm'; +import { IRun, LoggerProxy, WorkflowExecuteMode } from 'n8n-workflow'; +import { QueryFailedError, Repository } from 'typeorm'; +import { mock } from 'jest-mock-extended'; + import config from '@/config'; -import { Db } from '@/index'; +import * as Db from '@/Db'; +import { User } from '@db/entities/User'; +import { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; import { nodeFetchedData, workflowExecutionCompleted } from '@/events/WorkflowStatistics'; -import { getLogger } from '@/Logger'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; +import { getLogger } from '@/Logger'; import { InternalHooks } from '@/InternalHooks'; + import { mockInstance } from '../integration/shared/utils'; -const FAKE_USER_ID = 'abcde-fghij'; - +type WorkflowStatisticsRepository = Repository; jest.mock('@/Db', () => { return { collections: { - WorkflowStatistics: { - insert: jest.fn((...args) => {}), - update: jest.fn((...args) => {}), - }, + WorkflowStatistics: mock(), }, }; }); -jest.spyOn(UserManagementHelper, 'getWorkflowOwner').mockImplementation(async (_workflowId) => { - return { id: FAKE_USER_ID }; -}); describe('Events', () => { + const fakeUser = Object.assign(new User(), { id: 'abcde-fghij' }); const internalHooks = mockInstance(InternalHooks); + jest.spyOn(UserManagementHelper, 'getWorkflowOwner').mockResolvedValue(fakeUser); + + const workflowStatisticsRepository = Db.collections.WorkflowStatistics as ReturnType< + typeof mock + >; + beforeAll(() => { config.set('diagnostics.enabled', true); config.set('deployment.type', 'n8n-testing'); @@ -57,8 +62,9 @@ describe('Events', () => { nodes: [], connections: {}, }; - const runData = { + const runData: IRun = { finished: true, + status: 'success', data: { resultData: { runData: {} } }, mode: 'internal' as WorkflowExecuteMode, startedAt: new Date(), @@ -66,7 +72,7 @@ describe('Events', () => { await workflowExecutionCompleted(workflow, runData); expect(internalHooks.onFirstProductionWorkflowSuccess).toBeCalledTimes(1); expect(internalHooks.onFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, { - user_id: FAKE_USER_ID, + user_id: fakeUser.id, workflow_id: workflow.id, }); }); @@ -82,8 +88,9 @@ describe('Events', () => { nodes: [], connections: {}, }; - const runData = { + const runData: IRun = { finished: false, + status: 'failed', data: { resultData: { runData: {} } }, mode: 'internal' as WorkflowExecuteMode, startedAt: new Date(), @@ -94,7 +101,7 @@ describe('Events', () => { test('should not send metrics for updated entries', async () => { // Call the function with a fail insert, ensure update is called *and* metrics aren't sent - Db.collections.WorkflowStatistics.insert.mockImplementationOnce(() => { + workflowStatisticsRepository.insert.mockImplementationOnce(() => { throw new QueryFailedError('invalid insert', [], ''); }); const workflow = { @@ -106,8 +113,9 @@ describe('Events', () => { nodes: [], connections: {}, }; - const runData = { + const runData: IRun = { finished: true, + status: 'success', data: { resultData: { runData: {} } }, mode: 'internal' as WorkflowExecuteMode, startedAt: new Date(), @@ -132,7 +140,7 @@ describe('Events', () => { await nodeFetchedData(workflowId, node); expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(1); expect(internalHooks.onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, { - user_id: FAKE_USER_ID, + user_id: fakeUser.id, workflow_id: workflowId, node_type: node.type, node_id: node.id, @@ -159,7 +167,7 @@ describe('Events', () => { await nodeFetchedData(workflowId, node); expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(1); expect(internalHooks.onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, { - user_id: FAKE_USER_ID, + user_id: fakeUser.id, workflow_id: workflowId, node_type: node.type, node_id: node.id, @@ -170,7 +178,7 @@ describe('Events', () => { test('should not send metrics for entries that already have the flag set', async () => { // Fetch data for workflow 2 which is set up to not be altered in the mocks - Db.collections.WorkflowStatistics.insert.mockImplementationOnce(() => { + workflowStatisticsRepository.insert.mockImplementationOnce(() => { throw new QueryFailedError('invalid insert', [], ''); }); const workflowId = '1'; diff --git a/packages/cli/test/unit/Helpers.ts b/packages/cli/test/unit/Helpers.ts index 1f2f9b49b7..2d9f43ebf5 100644 --- a/packages/cli/test/unit/Helpers.ts +++ b/packages/cli/test/unit/Helpers.ts @@ -1,108 +1,4 @@ -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { - INodeType, - INodeTypeData, - INodeTypes, - IVersionedNodeType, - NodeHelpers, -} from 'n8n-workflow'; - -// TODO: delete this -class NodeTypesClass implements INodeTypes { - nodeTypes: INodeTypeData = { - 'test.set': { - sourcePath: '', - type: { - description: { - displayName: 'Set', - name: 'set', - group: ['input'], - version: 1, - description: 'Sets a value', - defaults: { - name: 'Set', - color: '#0000FF', - }, - inputs: ['main'], - outputs: ['main'], - properties: [ - { - displayName: 'Value1', - name: 'value1', - type: 'string', - default: 'default-value1', - }, - { - displayName: 'Value2', - name: 'value2', - type: 'string', - default: 'default-value2', - }, - ], - }, - }, - }, - 'fake-scheduler': { - sourcePath: '', - type: { - description: { - displayName: 'Schedule', - name: 'set', - group: ['input'], - version: 1, - description: 'Schedules execuitons', - defaults: { - name: 'Set', - color: '#0000FF', - }, - inputs: ['main'], - outputs: ['main'], - properties: [ - { - displayName: 'Value1', - name: 'value1', - type: 'string', - default: 'default-value1', - }, - { - displayName: 'Value2', - name: 'value2', - type: 'string', - default: 'default-value2', - }, - ], - }, - trigger: () => { - return Promise.resolve(undefined); - }, - }, - }, - }; - - constructor(nodesAndCredentials?: LoadNodesAndCredentials) { - if (nodesAndCredentials?.loaded?.nodes) { - this.nodeTypes = nodesAndCredentials?.loaded?.nodes; - } - } - - getByName(nodeType: string): INodeType | IVersionedNodeType { - return this.nodeTypes[nodeType].type; - } - - getByNameAndVersion(nodeType: string, version?: number): INodeType { - return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version); - } -} - -let nodeTypesInstance: NodeTypesClass | undefined; - -export function NodeTypes(nodesAndCredentials?: LoadNodesAndCredentials): NodeTypesClass { - if (nodeTypesInstance === undefined) { - nodeTypesInstance = new NodeTypesClass(nodesAndCredentials); - } - - return nodeTypesInstance; -} +import { INodeTypeData } from 'n8n-workflow'; /** * Ensure all pending promises settle. The promise's `resolve` is placed in diff --git a/packages/cli/test/unit/PermissionChecker.test.ts b/packages/cli/test/unit/PermissionChecker.test.ts index 7fbe355351..71b550c50f 100644 --- a/packages/cli/test/unit/PermissionChecker.test.ts +++ b/packages/cli/test/unit/PermissionChecker.test.ts @@ -1,46 +1,47 @@ import { v4 as uuid } from 'uuid'; -import { - ICredentialTypes, - INodeTypeData, - INodeTypes, - SubworkflowOperationError, - Workflow, -} from 'n8n-workflow'; +import { Container } from 'typedi'; +import { ICredentialTypes, INodeTypes, SubworkflowOperationError, Workflow } from 'n8n-workflow'; import config from '@/config'; import * as Db from '@/Db'; -import * as testDb from '../integration/shared/testDb'; -import { mockNodeTypesData, NodeTypes as MockNodeTypes } from './Helpers'; +import { Role } from '@db/entities/Role'; +import { User } from '@db/entities/User'; +import { SharedWorkflow } from '@db/entities/SharedWorkflow'; +import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { NodeTypes } from '@/NodeTypes'; import { UserService } from '@/user/user.service'; import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; import { WorkflowsService } from '@/workflows/workflows.services'; + import { randomCredentialPayload as randomCred, randomPositiveDigit, } from '../integration/shared/random'; - -import { Role } from '@db/entities/Role'; +import * as testDb from '../integration/shared/testDb'; +import { mockNodeTypesData } from './Helpers'; import type { SaveCredentialFunction } from '../integration/shared/types'; -import { User } from '@db/entities/User'; -import { SharedWorkflow } from '@db/entities/SharedWorkflow'; +import { mockInstance } from '../integration/shared/utils'; let mockNodeTypes: INodeTypes; let credentialOwnerRole: Role; let workflowOwnerRole: Role; let saveCredential: SaveCredentialFunction; +const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']); +mockInstance(LoadNodesAndCredentials, { + loaded: { + nodes: MOCK_NODE_TYPES_DATA, + credentials: {}, + }, + known: { nodes: {}, credentials: {} }, + credentialTypes: {} as ICredentialTypes, +}); + beforeAll(async () => { await testDb.init(); - mockNodeTypes = MockNodeTypes({ - loaded: { - nodes: MOCK_NODE_TYPES_DATA, - credentials: {}, - }, - known: { nodes: {}, credentials: {} }, - credentialTypes: {} as ICredentialTypes, - }); + mockNodeTypes = Container.get(NodeTypes); credentialOwnerRole = await testDb.getCredentialOwnerRole(); workflowOwnerRole = await testDb.getWorkflowOwnerRole(); @@ -241,7 +242,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => { nodes: [], connections: {}, active: false, - nodeTypes: MockNodeTypes(), + nodeTypes: mockNodeTypes, id: '2', }); await expect( @@ -263,7 +264,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => { nodes: [], connections: {}, active: false, - nodeTypes: MockNodeTypes(), + nodeTypes: mockNodeTypes, id: '2', }); await expect( @@ -301,7 +302,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => { nodes: [], connections: {}, active: false, - nodeTypes: MockNodeTypes(), + nodeTypes: mockNodeTypes, id: '2', settings: { callerPolicy: 'workflowsFromAList', @@ -327,7 +328,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => { nodes: [], connections: {}, active: false, - nodeTypes: MockNodeTypes(), + nodeTypes: mockNodeTypes, id: '2', }); await expect( @@ -350,7 +351,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => { nodes: [], connections: {}, active: false, - nodeTypes: MockNodeTypes(), + nodeTypes: mockNodeTypes, id: '2', settings: { callerPolicy: 'workflowsFromAList', @@ -376,7 +377,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => { nodes: [], connections: {}, active: false, - nodeTypes: MockNodeTypes(), + nodeTypes: mockNodeTypes, id: '2', settings: { callerPolicy: 'any', @@ -387,5 +388,3 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => { ).resolves.not.toThrow(); }); }); - -const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4bd3329da..06fdd35d03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,15 +31,15 @@ importers: specifiers: '@n8n_io/eslint-config': workspace:* '@ngneat/falso': ^6.1.0 - '@types/jest': ^29.2.2 + '@types/jest': ^29.5.0 '@types/supertest': ^2.0.12 cross-env: ^7.0.3 cypress: ^12.7.0 cypress-real-events: ^1.7.6 - jest: ^29.4.2 - jest-environment-jsdom: ^29.4.2 - jest-mock: ^29.4.2 - jest-mock-extended: ^3.0.1 + jest: ^29.5.0 + jest-environment-jsdom: ^29.5.0 + jest-mock: ^29.5.0 + jest-mock-extended: ^3.0.3 n8n: workspace:* nock: ^13.2.9 node-fetch: ^2.6.7 @@ -57,15 +57,15 @@ importers: devDependencies: '@n8n_io/eslint-config': link:packages/@n8n_io/eslint-config '@ngneat/falso': 6.1.0 - '@types/jest': 29.2.2 + '@types/jest': 29.5.0 '@types/supertest': 2.0.12 cross-env: 7.0.3 cypress: 12.7.0 cypress-real-events: 1.7.6_cypress@12.7.0 - jest: 29.4.2 - jest-environment-jsdom: 29.4.2 - jest-mock: 29.4.2 - jest-mock-extended: 3.0.1_gelezmms3va3pnkofxaadhcvma + jest: 29.5.0 + jest-environment-jsdom: 29.5.0 + jest-mock: 29.5.0 + jest-mock-extended: 3.0.3_doipufordlnvh5g4adbwayvyvy nock: 13.2.9 node-fetch: 2.6.7 p-limit: 3.1.0 @@ -74,7 +74,7 @@ importers: run-script-os: 1.1.6 start-server-and-test: 1.14.0 supertest: 6.3.3 - ts-jest: 29.0.5_zgilbyuumz7xbrerspmrtjuxsi + ts-jest: 29.0.5_rvjmdqhqjqm2mi2o3slrod4dxm tsc-watch: 6.0.0_typescript@4.9.5 turbo: 1.7.4 @@ -3227,20 +3227,20 @@ packages: engines: {node: '>=8'} dev: true - /@jest/console/29.4.2: - resolution: {integrity: sha512-0I/rEJwMpV9iwi9cDEnT71a5nNGK9lj8Z4+1pRAU2x/thVXCDnaTGrvxyK+cAqZTFVFCiR+hfVrP4l2m+dCmQg==} + /@jest/console/29.5.0: + resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.2 + '@jest/types': 29.5.0 '@types/node': 16.18.12 chalk: 4.1.2 - jest-message-util: 29.4.2 - jest-util: 29.4.2 + jest-message-util: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 dev: true - /@jest/core/29.4.2: - resolution: {integrity: sha512-KGuoQah0P3vGNlaS/l9/wQENZGNKGoWb+OPxh3gz+YzG7/XExvYu34MzikRndQCdM2S0tzExN4+FL37i6gZmCQ==} + /@jest/core/29.5.0: + resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -3248,32 +3248,32 @@ packages: node-notifier: optional: true dependencies: - '@jest/console': 29.4.2 - '@jest/reporters': 29.4.2 - '@jest/test-result': 29.4.2 - '@jest/transform': 29.4.2 - '@jest/types': 29.4.2 + '@jest/console': 29.5.0 + '@jest/reporters': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.18.12 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.10 - jest-changed-files: 29.4.2 - jest-config: 29.4.2_@types+node@16.18.12 - jest-haste-map: 29.4.2 - jest-message-util: 29.4.2 - jest-regex-util: 29.4.2 - jest-resolve: 29.4.2 - jest-resolve-dependencies: 29.4.2 - jest-runner: 29.4.2 - jest-runtime: 29.4.2 - jest-snapshot: 29.4.2 - jest-util: 29.4.2 - jest-validate: 29.4.2 - jest-watcher: 29.4.2 + jest-changed-files: 29.5.0 + jest-config: 29.5.0_@types+node@16.18.12 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-resolve-dependencies: 29.5.0 + jest-runner: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + jest-watcher: 29.5.0 micromatch: 4.0.5 - pretty-format: 29.4.2 + pretty-format: 29.5.0 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: @@ -3281,14 +3281,14 @@ packages: - ts-node dev: true - /@jest/environment/29.4.2: - resolution: {integrity: sha512-JKs3VUtse0vQfCaFGJRX1bir9yBdtasxziSyu+pIiEllAQOe4oQhdCYIf3+Lx+nGglFktSKToBnRJfD5QKp+NQ==} + /@jest/environment/29.5.0: + resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.4.2 - '@jest/types': 29.4.2 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.18.12 - jest-mock: 29.4.2 + jest-mock: 29.5.0 dev: true /@jest/expect-utils/29.3.1: @@ -3305,42 +3305,49 @@ packages: jest-get-type: 29.4.2 dev: true - /@jest/expect/29.4.2: - resolution: {integrity: sha512-NUAeZVApzyaeLjfWIV/64zXjA2SS+NuUPHpAlO7IwVMGd5Vf9szTl9KEDlxY3B4liwLO31os88tYNHl6cpjtKQ==} + /@jest/expect-utils/29.5.0: + resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.4.2 - jest-snapshot: 29.4.2 + jest-get-type: 29.4.3 + dev: true + + /@jest/expect/29.5.0: + resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.5.0 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color dev: true - /@jest/fake-timers/29.4.2: - resolution: {integrity: sha512-Ny1u0Wg6kCsHFWq7A/rW/tMhIedq2siiyHyLpHCmIhP7WmcAmd2cx95P+0xtTZlj5ZbJxIRQi4OPydZZUoiSQQ==} + /@jest/fake-timers/29.5.0: + resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.2 + '@jest/types': 29.5.0 '@sinonjs/fake-timers': 10.0.2 '@types/node': 16.18.12 - jest-message-util: 29.4.2 - jest-mock: 29.4.2 - jest-util: 29.4.2 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-util: 29.5.0 dev: true - /@jest/globals/29.4.2: - resolution: {integrity: sha512-zCk70YGPzKnz/I9BNFDPlK+EuJLk21ur/NozVh6JVM86/YYZtZHqxFFQ62O9MWq7uf3vIZnvNA0BzzrtxD9iyg==} + /@jest/globals/29.5.0: + resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.2 - '@jest/expect': 29.4.2 - '@jest/types': 29.4.2 - jest-mock: 29.4.2 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/types': 29.5.0 + jest-mock: 29.5.0 transitivePeerDependencies: - supports-color dev: true - /@jest/reporters/29.4.2: - resolution: {integrity: sha512-10yw6YQe75zCgYcXgEND9kw3UZZH5tJeLzWv4vTk/2mrS1aY50A37F+XT2hPO5OqQFFnUWizXD8k1BMiATNfUw==} + /@jest/reporters/29.5.0: + resolution: {integrity: sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -3349,10 +3356,10 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.4.2 - '@jest/test-result': 29.4.2 - '@jest/transform': 29.4.2 - '@jest/types': 29.4.2 + '@jest/console': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 '@types/node': 16.18.12 chalk: 4.1.2 @@ -3365,9 +3372,9 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 29.4.2 - jest-util: 29.4.2 - jest-worker: 29.4.2 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + jest-worker: 29.5.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 @@ -3376,13 +3383,6 @@ packages: - supports-color dev: true - /@jest/schemas/29.0.0: - resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.24.51 - dev: true - /@jest/schemas/29.4.2: resolution: {integrity: sha512-ZrGzGfh31NtdVH8tn0mgJw4khQuNHiKqdzJAFbCaERbyCP9tHlxWuL/mnMu8P7e/+k4puWjI1NOzi/sFsjce/g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3390,8 +3390,15 @@ packages: '@sinclair/typebox': 0.25.21 dev: true - /@jest/source-map/29.4.2: - resolution: {integrity: sha512-tIoqV5ZNgYI9XCKXMqbYe5JbumcvyTgNN+V5QW4My033lanijvCD0D4PI9tBw4pRTqWOc00/7X3KVvUh+qnF4Q==} + /@jest/schemas/29.4.3: + resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.25.21 + dev: true + + /@jest/source-map/29.4.3: + resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jridgewell/trace-mapping': 0.3.17 @@ -3399,23 +3406,23 @@ packages: graceful-fs: 4.2.10 dev: true - /@jest/test-result/29.4.2: - resolution: {integrity: sha512-HZsC3shhiHVvMtP+i55MGR5bPcc3obCFbA5bzIOb8pCjwBZf11cZliJncCgaVUbC5yoQNuGqCkC0Q3t6EItxZA==} + /@jest/test-result/29.5.0: + resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.4.2 - '@jest/types': 29.4.2 + '@jest/console': 29.5.0 + '@jest/types': 29.5.0 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 dev: true - /@jest/test-sequencer/29.4.2: - resolution: {integrity: sha512-9Z2cVsD6CcObIVrWigHp2McRJhvCxL27xHtrZFgNC1RwnoSpDx6fZo8QYjJmziFlW9/hr78/3sxF54S8B6v8rg==} + /@jest/test-sequencer/29.5.0: + resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.4.2 + '@jest/test-result': 29.5.0 graceful-fs: 4.2.10 - jest-haste-map: 29.4.2 + jest-haste-map: 29.5.0 slash: 3.0.0 dev: true @@ -3424,16 +3431,39 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.20.12 - '@jest/types': 29.4.2 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 - jest-haste-map: 29.4.2 - jest-regex-util: 29.4.2 - jest-util: 29.4.2 + jest-haste-map: 29.5.0 + jest-regex-util: 29.4.3 + jest-util: 29.5.0 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/transform/29.5.0: + resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.20.12 + '@jest/types': 29.5.0 + '@jridgewell/trace-mapping': 0.3.17 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.10 + jest-haste-map: 29.5.0 + jest-regex-util: 29.4.3 + jest-util: 29.5.0 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 @@ -3465,6 +3495,18 @@ packages: chalk: 4.1.2 dev: true + /@jest/types/29.5.0: + resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.4.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 16.18.12 + '@types/yargs': 17.0.19 + chalk: 4.1.2 + dev: true + /@jridgewell/gen-mapping/0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} @@ -4125,10 +4167,6 @@ packages: resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} dev: true - /@sinclair/typebox/0.24.51: - resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} - dev: true - /@sinclair/typebox/0.25.21: resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==} dev: true @@ -4257,7 +4295,7 @@ packages: '@storybook/csf-plugin': 7.0.0-beta.46 '@storybook/csf-tools': 7.0.0-beta.46 '@storybook/global': 5.0.0 - '@storybook/mdx2-csf': 1.0.0-next.5 + '@storybook/mdx2-csf': 1.0.0-next.6 '@storybook/node-logger': 7.0.0-beta.46 '@storybook/postinstall': 7.0.0-beta.46 '@storybook/preview-api': 7.0.0-beta.46 @@ -5006,8 +5044,8 @@ packages: resolution: {integrity: sha512-0Tsm47YM3SU9rvPpXxp6/toQ1DDUrIbZt1pXcj72szLZvi7U/fXTMpsBX9gOB1MNVYIYRqS2V+jcO8UjFd4qyQ==} dev: true - /@storybook/mdx2-csf/1.0.0-next.5: - resolution: {integrity: sha512-02w0sgGZaK1agT050yCVhJ+o4rLHANWvLKWjQjeAsYbjneLC5ITt+3GDB4jRiWwJboZ8dHW1fGSK1Vg5fA34aQ==} + /@storybook/mdx2-csf/1.0.0-next.6: + resolution: {integrity: sha512-m6plojocU/rmrqWd26yvm8D+oHZPZ6PtSSFmZIgpNDEPVmc8s4fBD6LXOAB5MiPI5f8KLUr2HVhOMZ97o5pDTw==} dev: true /@storybook/node-logger/6.5.15: @@ -5652,13 +5690,6 @@ packages: '@types/istanbul-lib-report': 3.0.0 dev: true - /@types/jest/29.2.2: - resolution: {integrity: sha512-og1wAmdxKoS71K2ZwSVqWPX6OVn3ihZ6ZT2qvZvZQm90lJVDyXIjYcu4Khx2CNIeaFv12rOU/YObOsI3VOkzog==} - dependencies: - expect: 29.3.1 - pretty-format: 29.3.1 - dev: true - /@types/jest/29.2.5: resolution: {integrity: sha512-H2cSxkKgVmqNHXP7TC2L/WUorrZu8ZigyRywfVzv6EyBlxj39n4C00hjXYQWsbwqgElaj/CiAeSRmk5GoaKTgw==} dependencies: @@ -5666,6 +5697,13 @@ packages: pretty-format: 29.4.2 dev: true + /@types/jest/29.5.0: + resolution: {integrity: sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==} + dependencies: + expect: 29.4.2 + pretty-format: 29.4.2 + dev: true + /@types/jmespath/0.15.0: resolution: {integrity: sha512-uaht4XcYSq5ZrPriQW8C+g5DhptewRd1E84ph7L167sCyzLObz+U3JTpmYq/CNkvjNsz2mtyQoHPNEYQYTzWmg==} dev: true @@ -7734,17 +7772,17 @@ packages: resolution: {integrity: sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==} dev: false - /babel-jest/29.4.2_@babel+core@7.20.12: - resolution: {integrity: sha512-vcghSqhtowXPG84posYkkkzcZsdayFkubUgbE3/1tuGbX7AQtwCkkNA/wIbB0BMjuCPoqTkiDyKN7Ty7d3uwNQ==} + /babel-jest/29.5.0_@babel+core@7.20.12: + resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.20.12 - '@jest/transform': 29.4.2 + '@jest/transform': 29.5.0 '@types/babel__core': 7.20.0 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.4.2_@babel+core@7.20.12 + babel-preset-jest: 29.5.0_@babel+core@7.20.12 chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 @@ -7778,8 +7816,8 @@ packages: - supports-color dev: true - /babel-plugin-jest-hoist/29.4.2: - resolution: {integrity: sha512-5HZRCfMeWypFEonRbEkwWXtNS1sQK159LhRVyRuLzyfVBxDy/34Tr/rg4YVi0SScSJ4fqeaR/OIeceJ/LaQ0pQ==} + /babel-plugin-jest-hoist/29.5.0: + resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.20.7 @@ -7848,14 +7886,14 @@ packages: '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.20.12 dev: true - /babel-preset-jest/29.4.2_@babel+core@7.20.12: - resolution: {integrity: sha512-ecWdaLY/8JyfUDr0oELBMpj3R5I1L6ZqG+kRJmwqfHtLWuPrJStR0LUkvUhfykJWTsXXMnohsayN/twltBbDrQ==} + /babel-preset-jest/29.5.0_@babel+core@7.20.12: + resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.20.12 - babel-plugin-jest-hoist: 29.4.2 + babel-plugin-jest-hoist: 29.5.0 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.12 dev: true @@ -9929,6 +9967,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff-sequences/29.4.3: + resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff/5.1.0: resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} engines: {node: '>=0.3.1'} @@ -11031,6 +11074,17 @@ packages: jest-util: 29.4.2 dev: true + /expect/29.5.0: + resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.5.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + dev: true + /express-async-errors/3.1.1_express@4.18.2: resolution: {integrity: sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==} peerDependencies: @@ -13227,43 +13281,44 @@ packages: filelist: 1.0.4 minimatch: 3.1.2 - /jest-changed-files/29.4.2: - resolution: {integrity: sha512-Qdd+AXdqD16PQa+VsWJpxR3kN0JyOCX1iugQfx5nUgAsI4gwsKviXkpclxOK9ZnwaY2IQVHz+771eAvqeOlfuw==} + /jest-changed-files/29.5.0: + resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: execa: 5.1.1 p-limit: 3.1.0 dev: true - /jest-circus/29.4.2: - resolution: {integrity: sha512-wW3ztp6a2P5c1yOc1Cfrt5ozJ7neWmqeXm/4SYiqcSriyisgq63bwFj1NuRdSR5iqS0CMEYwSZd89ZA47W9zUg==} + /jest-circus/29.5.0: + resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.2 - '@jest/expect': 29.4.2 - '@jest/test-result': 29.4.2 - '@jest/types': 29.4.2 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.18.12 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 is-generator-fn: 2.1.0 - jest-each: 29.4.2 - jest-matcher-utils: 29.4.2 - jest-message-util: 29.4.2 - jest-runtime: 29.4.2 - jest-snapshot: 29.4.2 - jest-util: 29.4.2 + jest-each: 29.5.0 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 p-limit: 3.1.0 - pretty-format: 29.4.2 + pretty-format: 29.5.0 + pure-rand: 6.0.1 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: - supports-color dev: true - /jest-cli/29.4.2: - resolution: {integrity: sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q==} + /jest-cli/29.5.0: + resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -13272,16 +13327,16 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.4.2 - '@jest/test-result': 29.4.2 - '@jest/types': 29.4.2 + '@jest/core': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.4.2 - jest-util: 29.4.2 - jest-validate: 29.4.2 + jest-config: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 prompts: 2.4.2 yargs: 17.6.2 transitivePeerDependencies: @@ -13290,8 +13345,8 @@ packages: - ts-node dev: true - /jest-config/29.4.2: - resolution: {integrity: sha512-919CtnXic52YM0zW4C1QxjG6aNueX1kBGthuMtvFtRTAxhKfJmiXC9qwHmi6o2josjbDz8QlWyY55F1SIVmCWA==} + /jest-config/29.5.0: + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -13303,33 +13358,33 @@ packages: optional: true dependencies: '@babel/core': 7.20.12 - '@jest/test-sequencer': 29.4.2 - '@jest/types': 29.4.2 - babel-jest: 29.4.2_@babel+core@7.20.12 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 + babel-jest: 29.5.0_@babel+core@7.20.12 chalk: 4.1.2 ci-info: 3.7.1 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.10 - jest-circus: 29.4.2 - jest-environment-node: 29.4.2 - jest-get-type: 29.4.2 - jest-regex-util: 29.4.2 - jest-resolve: 29.4.2 - jest-runner: 29.4.2 - jest-util: 29.4.2 - jest-validate: 29.4.2 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.4.2 + pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color dev: true - /jest-config/29.4.2_@types+node@16.18.12: - resolution: {integrity: sha512-919CtnXic52YM0zW4C1QxjG6aNueX1kBGthuMtvFtRTAxhKfJmiXC9qwHmi6o2josjbDz8QlWyY55F1SIVmCWA==} + /jest-config/29.5.0_@types+node@16.18.12: + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -13341,26 +13396,26 @@ packages: optional: true dependencies: '@babel/core': 7.20.12 - '@jest/test-sequencer': 29.4.2 - '@jest/types': 29.4.2 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.18.12 - babel-jest: 29.4.2_@babel+core@7.20.12 + babel-jest: 29.5.0_@babel+core@7.20.12 chalk: 4.1.2 ci-info: 3.7.1 deepmerge: 4.2.2 glob: 7.2.3 graceful-fs: 4.2.10 - jest-circus: 29.4.2 - jest-environment-node: 29.4.2 - jest-get-type: 29.4.2 - jest-regex-util: 29.4.2 - jest-resolve: 29.4.2 - jest-runner: 29.4.2 - jest-util: 29.4.2 - jest-validate: 29.4.2 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.4.2 + pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -13387,26 +13442,36 @@ packages: pretty-format: 29.4.2 dev: true - /jest-docblock/29.4.2: - resolution: {integrity: sha512-dV2JdahgClL34Y5vLrAHde3nF3yo2jKRH+GIYJuCpfqwEJZcikzeafVTGAjbOfKPG17ez9iWXwUYp7yefeCRag==} + /jest-diff/29.5.0: + resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.4.3 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 + dev: true + + /jest-docblock/29.4.3: + resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 dev: true - /jest-each/29.4.2: - resolution: {integrity: sha512-trvKZb0JYiCndc55V1Yh0Luqi7AsAdDWpV+mKT/5vkpnnFQfuQACV72IoRV161aAr6kAVIBpmYzwhBzm34vQkA==} + /jest-each/29.5.0: + resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.2 + '@jest/types': 29.5.0 chalk: 4.1.2 - jest-get-type: 29.4.2 - jest-util: 29.4.2 - pretty-format: 29.4.2 + jest-get-type: 29.4.3 + jest-util: 29.5.0 + pretty-format: 29.5.0 dev: true - /jest-environment-jsdom/29.4.2: - resolution: {integrity: sha512-v1sH4Q0JGM+LPEGqHNM+m+uTMf3vpXpKiuDYqWUAh+0c9+nc7scGE+qTR5JuE+OOTDnwfzPgcv9sMq6zWAOaVg==} + /jest-environment-jsdom/29.5.0: + resolution: {integrity: sha512-/KG8yEK4aN8ak56yFVdqFDzKNHgF4BAymCx2LbPNPsUshUlfAl0eX402Xm1pt+eoG9SLZEUVifqXtX8SK74KCw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: canvas: ^2.5.0 @@ -13414,13 +13479,13 @@ packages: canvas: optional: true dependencies: - '@jest/environment': 29.4.2 - '@jest/fake-timers': 29.4.2 - '@jest/types': 29.4.2 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/jsdom': 20.0.1 '@types/node': 16.18.12 - jest-mock: 29.4.2 - jest-util: 29.4.2 + jest-mock: 29.5.0 + jest-util: 29.5.0 jsdom: 20.0.2 transitivePeerDependencies: - bufferutil @@ -13428,16 +13493,16 @@ packages: - utf-8-validate dev: true - /jest-environment-node/29.4.2: - resolution: {integrity: sha512-MLPrqUcOnNBc8zTOfqBbxtoa8/Ee8tZ7UFW7hRDQSUT+NGsvS96wlbHGTf+EFAT9KC3VNb7fWEM6oyvmxtE/9w==} + /jest-environment-node/29.5.0: + resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.2 - '@jest/fake-timers': 29.4.2 - '@jest/types': 29.4.2 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.18.12 - jest-mock: 29.4.2 - jest-util: 29.4.2 + jest-mock: 29.5.0 + jest-util: 29.5.0 dev: true /jest-get-type/29.2.0: @@ -13450,31 +13515,36 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-haste-map/29.4.2: - resolution: {integrity: sha512-WkUgo26LN5UHPknkezrBzr7lUtV1OpGsp+NfXbBwHztsFruS3gz+AMTTBcEklvi8uPzpISzYjdKXYZQJXBnfvw==} + /jest-get-type/29.4.3: + resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + + /jest-haste-map/29.5.0: + resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.2 + '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.6 '@types/node': 16.18.12 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 - jest-regex-util: 29.4.2 - jest-util: 29.4.2 - jest-worker: 29.4.2 + jest-regex-util: 29.4.3 + jest-util: 29.5.0 + jest-worker: 29.5.0 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 dev: true - /jest-leak-detector/29.4.2: - resolution: {integrity: sha512-Wa62HuRJmWXtX9F00nUpWlrbaH5axeYCdyRsOs/+Rb1Vb6+qWTlB5rKwCCRKtorM7owNwKsyJ8NRDUcZ8ghYUA==} + /jest-leak-detector/29.5.0: + resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.4.2 - pretty-format: 29.4.2 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 dev: true /jest-matcher-utils/29.3.1: @@ -13497,6 +13567,16 @@ packages: pretty-format: 29.4.2 dev: true + /jest-matcher-utils/29.5.0: + resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.5.0 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 + dev: true + /jest-message-util/29.3.1: resolution: {integrity: sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -13527,27 +13607,42 @@ packages: stack-utils: 2.0.6 dev: true - /jest-mock-extended/3.0.1_gelezmms3va3pnkofxaadhcvma: - resolution: {integrity: sha512-RF4Ow8pXvbRuEcCTj56oYHmig5311BSFvbEGxPNYL51wGKGu93MvVQgx0UpFmjqyBXIcElkZo2Rke88kR1iSKQ==} + /jest-message-util/29.5.0: + resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 29.5.0 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.10 + micromatch: 4.0.5 + pretty-format: 29.5.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock-extended/3.0.3_doipufordlnvh5g4adbwayvyvy: + resolution: {integrity: sha512-yqpzvwFr2JWFArj4sPco4hPSanYH3erFgdkv7ahP8EMiAbVH+Rgjc8/cpAHJVG7+sZnQgl0AuTkxofD7eLJN+g==} peerDependencies: jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 typescript: ^3.0.0 || ^4.0.0 dependencies: - jest: 29.4.2 + jest: 29.5.0 ts-essentials: 7.0.3_typescript@4.9.5 typescript: 4.9.5 dev: true - /jest-mock/29.4.2: - resolution: {integrity: sha512-x1FSd4Gvx2yIahdaIKoBjwji6XpboDunSJ95RpntGrYulI1ByuYQCKN/P7hvk09JB74IonU3IPLdkutEWYt++g==} + /jest-mock/29.5.0: + resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.2 + '@jest/types': 29.5.0 '@types/node': 16.18.12 - jest-util: 29.4.2 + jest-util: 29.5.0 dev: true - /jest-pnp-resolver/1.2.2_jest-resolve@29.4.2: + /jest-pnp-resolver/1.2.2_jest-resolve@29.5.0: resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} engines: {node: '>=6'} peerDependencies: @@ -13556,101 +13651,100 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 29.4.2 + jest-resolve: 29.5.0 dev: true - /jest-regex-util/29.4.2: - resolution: {integrity: sha512-XYZXOqUl1y31H6VLMrrUL1ZhXuiymLKPz0BO1kEeR5xER9Tv86RZrjTm74g5l9bPJQXA/hyLdaVPN/sdqfteig==} + /jest-regex-util/29.4.3: + resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /jest-resolve-dependencies/29.4.2: - resolution: {integrity: sha512-6pL4ptFw62rjdrPk7rRpzJYgcRqRZNsZTF1VxVTZMishbO6ObyWvX57yHOaNGgKoADtAHRFYdHQUEvYMJATbDg==} + /jest-resolve-dependencies/29.5.0: + resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-regex-util: 29.4.2 - jest-snapshot: 29.4.2 + jest-regex-util: 29.4.3 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color dev: true - /jest-resolve/29.4.2: - resolution: {integrity: sha512-RtKWW0mbR3I4UdkOrW7552IFGLYQ5AF9YrzD0FnIOkDu0rAMlA5/Y1+r7lhCAP4nXSBTaE7ueeqj6IOwZpgoqw==} + /jest-resolve/29.5.0: + resolution: {integrity: sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 graceful-fs: 4.2.10 - jest-haste-map: 29.4.2 - jest-pnp-resolver: 1.2.2_jest-resolve@29.4.2 - jest-util: 29.4.2 - jest-validate: 29.4.2 + jest-haste-map: 29.5.0 + jest-pnp-resolver: 1.2.2_jest-resolve@29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 resolve: 1.22.1 resolve.exports: 2.0.0 slash: 3.0.0 dev: true - /jest-runner/29.4.2: - resolution: {integrity: sha512-wqwt0drm7JGjwdH+x1XgAl+TFPH7poowMguPQINYxaukCqlczAcNLJiK+OLxUxQAEWMdy+e6nHZlFHO5s7EuRg==} + /jest-runner/29.5.0: + resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.4.2 - '@jest/environment': 29.4.2 - '@jest/test-result': 29.4.2 - '@jest/transform': 29.4.2 - '@jest/types': 29.4.2 + '@jest/console': 29.5.0 + '@jest/environment': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.18.12 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.10 - jest-docblock: 29.4.2 - jest-environment-node: 29.4.2 - jest-haste-map: 29.4.2 - jest-leak-detector: 29.4.2 - jest-message-util: 29.4.2 - jest-resolve: 29.4.2 - jest-runtime: 29.4.2 - jest-util: 29.4.2 - jest-watcher: 29.4.2 - jest-worker: 29.4.2 + jest-docblock: 29.4.3 + jest-environment-node: 29.5.0 + jest-haste-map: 29.5.0 + jest-leak-detector: 29.5.0 + jest-message-util: 29.5.0 + jest-resolve: 29.5.0 + jest-runtime: 29.5.0 + jest-util: 29.5.0 + jest-watcher: 29.5.0 + jest-worker: 29.5.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color dev: true - /jest-runtime/29.4.2: - resolution: {integrity: sha512-3fque9vtpLzGuxT9eZqhxi+9EylKK/ESfhClv4P7Y9sqJPs58LjVhTt8jaMp/pRO38agll1CkSu9z9ieTQeRrw==} + /jest-runtime/29.5.0: + resolution: {integrity: sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.2 - '@jest/fake-timers': 29.4.2 - '@jest/globals': 29.4.2 - '@jest/source-map': 29.4.2 - '@jest/test-result': 29.4.2 - '@jest/transform': 29.4.2 - '@jest/types': 29.4.2 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/globals': 29.5.0 + '@jest/source-map': 29.4.3 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.18.12 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 glob: 7.2.3 graceful-fs: 4.2.10 - jest-haste-map: 29.4.2 - jest-message-util: 29.4.2 - jest-mock: 29.4.2 - jest-regex-util: 29.4.2 - jest-resolve: 29.4.2 - jest-snapshot: 29.4.2 - jest-util: 29.4.2 - semver: 7.3.8 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /jest-snapshot/29.4.2: - resolution: {integrity: sha512-PdfubrSNN5KwroyMH158R23tWcAXJyx4pvSvWls1dHoLCaUhGul9rsL3uVjtqzRpkxlkMavQjGuWG1newPgmkw==} + /jest-snapshot/29.5.0: + resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.20.12 @@ -13659,23 +13753,22 @@ packages: '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.20.12 '@babel/traverse': 7.20.12 '@babel/types': 7.20.7 - '@jest/expect-utils': 29.4.2 - '@jest/transform': 29.4.2 - '@jest/types': 29.4.2 + '@jest/expect-utils': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/babel__traverse': 7.18.2 '@types/prettier': 2.7.1 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.20.12 chalk: 4.1.2 - expect: 29.4.2 + expect: 29.5.0 graceful-fs: 4.2.10 - jest-diff: 29.4.2 - jest-get-type: 29.4.2 - jest-haste-map: 29.4.2 - jest-matcher-utils: 29.4.2 - jest-message-util: 29.4.2 - jest-util: 29.4.2 + jest-diff: 29.5.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 natural-compare: 1.4.0 - pretty-format: 29.4.2 + pretty-format: 29.5.0 semver: 7.3.8 transitivePeerDependencies: - supports-color @@ -13685,7 +13778,7 @@ packages: resolution: {integrity: sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.2 + '@jest/types': 29.5.0 '@types/node': 16.18.12 chalk: 4.1.2 ci-info: 3.7.1 @@ -13705,29 +13798,41 @@ packages: picomatch: 2.3.1 dev: true - /jest-validate/29.4.2: - resolution: {integrity: sha512-tto7YKGPJyFbhcKhIDFq8B5od+eVWD/ySZ9Tvcp/NGCvYA4RQbuzhbwYWtIjMT5W5zA2W0eBJwu4HVw34d5G6Q==} + /jest-util/29.5.0: + resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.2 - camelcase: 6.3.0 + '@jest/types': 29.5.0 + '@types/node': 16.18.12 chalk: 4.1.2 - jest-get-type: 29.4.2 - leven: 3.1.0 - pretty-format: 29.4.2 + ci-info: 3.7.1 + graceful-fs: 4.2.10 + picomatch: 2.3.1 dev: true - /jest-watcher/29.4.2: - resolution: {integrity: sha512-onddLujSoGiMJt+tKutehIidABa175i/Ays+QvKxCqBwp7fvxP3ZhKsrIdOodt71dKxqk4sc0LN41mWLGIK44w==} + /jest-validate/29.5.0: + resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.4.2 - '@jest/types': 29.4.2 + '@jest/types': 29.5.0 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.4.3 + leven: 3.1.0 + pretty-format: 29.5.0 + dev: true + + /jest-watcher/29.5.0: + resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 16.18.12 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 29.4.2 + jest-util: 29.5.0 string-length: 4.0.2 dev: true @@ -13740,18 +13845,18 @@ packages: supports-color: 8.1.1 dev: true - /jest-worker/29.4.2: - resolution: {integrity: sha512-VIuZA2hZmFyRbchsUCHEehoSf2HEl0YVF8SDJqtPnKorAaBuh42V8QsLnde0XP5F6TyCynGPEGgBOn3Fc+wZGw==} + /jest-worker/29.5.0: + resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 16.18.12 - jest-util: 29.4.2 + jest-util: 29.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest/29.4.2: - resolution: {integrity: sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg==} + /jest/29.5.0: + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -13760,10 +13865,10 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.4.2 - '@jest/types': 29.4.2 + '@jest/core': 29.5.0 + '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.4.2 + jest-cli: 29.5.0 transitivePeerDependencies: - '@types/node' - supports-color @@ -17011,15 +17116,6 @@ packages: react-is: 17.0.2 dev: true - /pretty-format/29.3.1: - resolution: {integrity: sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/schemas': 29.0.0 - ansi-styles: 5.2.0 - react-is: 18.2.0 - dev: true - /pretty-format/29.4.2: resolution: {integrity: sha512-qKlHR8yFVCbcEWba0H0TOC8dnLlO4vPlyEjRPw31FZ2Rupy9nLa8ZLbYny8gWEl8CkEhJqAE6IzdNELTBVcBEg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -17029,6 +17125,15 @@ packages: react-is: 18.2.0 dev: true + /pretty-format/29.5.0: + resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.4.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /pretty-hrtime/1.0.3: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} @@ -17342,6 +17447,10 @@ packages: - utf-8-validate dev: true + /pure-rand/6.0.1: + resolution: {integrity: sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==} + dev: true + /python-struct/1.1.3: resolution: {integrity: sha512-UsI/mNvk25jRpGKYI38Nfbv84z48oiIWwG67DLVvjRhy8B/0aIK+5Ju5WOHgw/o9rnEmbAS00v4rgKFQeC332Q==} dependencies: @@ -19854,7 +19963,7 @@ packages: resolution: {integrity: sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==} dev: false - /ts-jest/29.0.5_zgilbyuumz7xbrerspmrtjuxsi: + /ts-jest/29.0.5_rvjmdqhqjqm2mi2o3slrod4dxm: resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -19878,7 +19987,7 @@ packages: '@babel/core': 7.20.12 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.4.2 + jest: 29.5.0 jest-util: 29.3.1 json5: 2.2.3 lodash.memoize: 4.1.2