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
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-03-17 17:24:05 +01:00 committed by GitHub
parent be172cb720
commit 6242cac53b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 4229 additions and 4762 deletions

View file

@ -28,8 +28,7 @@ const config = {
};
if (process.env.CI === 'true') {
config.maxWorkers = 2;
config.workerIdleMemoryLimit = 2048;
config.workerIdleMemoryLimit = 1024;
config.coverageReporters = ['cobertura'];
}

View file

@ -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",

View file

@ -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 {};

View file

@ -1,55 +1,54 @@
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({
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,
@ -85,10 +84,10 @@ test('POST /login should log user in', async () => {
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
});
});
test('GET /login should return 401 Unauthorized if no cookie', async () => {
const authlessAgent = utils.createAgent(app);
describe('GET /login', () => {
test('should return 401 Unauthorized if no cookie', async () => {
const response = await authlessAgent.get('/login');
expect(response.statusCode).toBe(401);
@ -97,16 +96,9 @@ test('GET /login should return 401 Unauthorized if no cookie', async () => {
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);
test('should return cookie if UM is disabled and no cookie is already set', async () => {
await testDb.createUserShell(globalOwnerRole);
config.set('userManagement.isInstanceOwnerSetUp', false);
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(false) },
);
await utils.setInstanceOwnerSetUp(false);
const response = await authlessAgent.get('/login');
@ -116,11 +108,10 @@ test('GET /login should return cookie if UM is disabled and no cookie is already
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`);
test('should return 401 Unauthorized if invalid cookie', async () => {
authlessAgent.jar.setCookie(`${AUTH_COOKIE_NAME}=invalid`);
const response = await invalidAuthAgent.get('/login');
const response = await authlessAgent.get('/login');
expect(response.statusCode).toBe(401);
@ -128,7 +119,7 @@ test('GET /login should return 401 Unauthorized if invalid cookie', async () =>
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in owner shell', async () => {
test('should return logged-in owner shell', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).get('/login');
@ -164,7 +155,7 @@ test('GET /login should return logged-in owner shell', async () => {
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in member shell', async () => {
test('should return logged-in member shell', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(memberShell).get('/login');
@ -200,7 +191,7 @@ test('GET /login should return logged-in member shell', async () => {
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in owner', async () => {
test('should return logged-in owner', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const response = await authAgent(owner).get('/login');
@ -236,7 +227,7 @@ test('GET /login should return logged-in owner', async () => {
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in member', async () => {
test('should return logged-in member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const response = await authAgent(member).get('/login');
@ -271,13 +262,21 @@ test('GET /login should return logged-in member', async () => {
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 });
describe('GET /resolve-signup-token', () => {
beforeEach(async () => {
owner = await testDb.createUser({
password: ownerPassword,
globalRole: globalOwnerRole,
});
authOwnerAgent = authAgent(owner);
});
test('should validate invite token', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(owner)
const response = await authOwnerAgent
.get('/resolve-signup-token')
.query({ inviterId: owner.id })
.query({ inviteeId: memberShell.id });
@ -293,10 +292,7 @@ test('GET /resolve-signup-token should validate invite token', async () => {
});
});
test('GET /resolve-signup-token should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = authAgent(owner);
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 });
@ -325,8 +321,10 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => {
expect(response.statusCode).toBe(400);
}
});
});
test('POST /logout should log user out', async () => {
describe('POST /logout', () => {
test('should log user out', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const response = await authAgent(owner).post('/logout');
@ -337,3 +335,4 @@ test('POST /logout should log user out', async () => {
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
});

View file

@ -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);
});
});

View file

@ -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<boolean>;
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,52 +52,40 @@ 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 });
beforeEach(async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
savedCredentialId = savedCredential.id;
});
// free router
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 });
describe('GET /credentials', () => {
test('should return all creds for owner', async () => {
const [member1, member2, member3] = await testDb.createManyUsers(3, {
globalRole: globalMemberRole,
});
@ -103,7 +96,7 @@ test('GET /credentials should return all creds for owner', async () => {
const sharedWith = [member1, member2, member3];
await testDb.shareCredentialWithUsers(savedCredential, sharedWith);
const response = await authAgent(owner).get('/credentials');
const response = await authOwnerAgent.get('/credentials');
expect(response.statusCode).toBe(200);
expect(response.body.data).toHaveLength(2); // owner retrieved owner cred and member cred
@ -152,13 +145,15 @@ test('GET /credentials should return all creds for owner', async () => {
expect(memberCredential.sharedWith).toHaveLength(0);
});
test('GET /credentials should return only relevant creds for member', async () => {
test('should return only relevant creds for member', async () => {
const [member1, member2] = await testDb.createManyUsers(2, {
globalRole: globalMemberRole,
});
await saveCredential(randomCredentialPayload(), { user: member2 });
const savedMemberCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
const savedMemberCredential = await saveCredential(randomCredentialPayload(), {
user: member1,
});
await testDb.shareCredentialWithUsers(savedMemberCredential, [member2]);
@ -191,15 +186,14 @@ test('GET /credentials should return only relevant creds for member', async () =
lastName: member2.lastName,
});
});
});
// ----------------------------------------
// GET /credentials/:id - fetch a certain credential
// ----------------------------------------
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}`);
@ -209,10 +203,10 @@ test('GET /credentials/:id should retrieve owned cred for owner', async () => {
validateMainCredentialData(firstCredential);
expect(firstCredential.data).toBeUndefined();
expect(firstCredential.ownedBy).toMatchObject({
id: ownerShell.id,
email: ownerShell.email,
firstName: ownerShell.firstName,
lastName: ownerShell.lastName,
id: owner.id,
email: owner.email,
firstName: owner.firstName,
lastName: owner.lastName,
});
expect(firstCredential.sharedWith).toHaveLength(0);
@ -227,9 +221,7 @@ test('GET /credentials/:id should retrieve owned cred for owner', async () => {
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);
test('should retrieve non-owned cred for owner', async () => {
const [member1, member2] = await testDb.createManyUsers(2, {
globalRole: globalMemberRole,
});
@ -268,7 +260,7 @@ test('GET /credentials/:id should retrieve non-owned cred for owner', async () =
expect(response2.body.data.sharedWith).toHaveLength(1);
});
test('GET /credentials/:id should retrieve owned cred for member', async () => {
test('should retrieve owned cred for member', async () => {
const [member1, member2, member3] = await testDb.createManyUsers(3, {
globalRole: globalMemberRole,
});
@ -306,10 +298,8 @@ test('GET /credentials/:id should retrieve owned cred for member', async () => {
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 });
test('should not retrieve non-owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(member).get(`/credentials/${savedCredential.id}`);
@ -317,14 +307,13 @@ test('GET /credentials/:id should not retrieve non-owned cred for member', async
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 });
test('should fail with missing encryption key', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authAgent(ownerShell)
const response = await authOwnerAgent
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
@ -333,26 +322,24 @@ test('GET /credentials/:id should fail with missing encryption key', async () =>
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');
test('should return 404 if cred not found', async () => {
const response = await authOwnerAgent.get('/credentials/789');
expect(response.statusCode).toBe(404);
const responseAbc = await authAgent(ownerShell).get('/credentials/abc');
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 authAgent(ownerShell).get('/credentials/new');
const responseNew = await authOwnerAgent.get('/credentials/new');
expect(responseNew.statusCode).toBe(200);
});
});
// ----------------------------------------
// indempotent share/unshare
// idempotent 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 });
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 });
const [member1, member2, member3, member4, member5] = await testDb.createManyUsers(5, {
@ -362,7 +349,7 @@ test('PUT /credentials/:id/share should share the credential with the provided u
await testDb.shareCredentialWithUsers(savedCredential, [member4, member5]);
const response = await authAgent(owner)
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds });
@ -389,19 +376,14 @@ test('PUT /credentials/:id/share should share the credential with the provided u
});
});
// ----------------------------------------
// share
// ----------------------------------------
test('PUT /credentials/:id/share should share the credential with the provided userIds', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
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)
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: memberIds });
@ -431,35 +413,29 @@ test('PUT /credentials/:id/share should share the credential with the provided u
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)
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('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 });
test('should respond 403 for non-owned credentials', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const response = await authAgent(owner)
const response = await authOwnerAgent
.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 });
test('should ignore pending sharee', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(owner)
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [memberShell.id] });
@ -473,11 +449,10 @@ test('PUT /credentials/:id/share should ignore pending sharee', async () => {
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 });
test('should ignore non-existing sharee', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(owner)
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: ['bce38a11-5e45-4d1c-a9ee-36e4a20ab0fc'] });
@ -491,26 +466,17 @@ test('PUT /credentials/:id/share should ignore non-existing sharee', async () =>
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 });
test('should respond 400 if invalid payload is provided', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const responses = await Promise.all([
authAgent(owner).put(`/credentials/${savedCredential.id}/share`).send(),
authAgent(owner)
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [1] }),
authOwnerAgent.put(`/credentials/${savedCredential.id}/share`).send(),
authOwnerAgent.put(`/credentials/${savedCredential.id}/share`).send({ shareWithIds: [1] }),
]);
responses.forEach((response) => expect(response.statusCode).toBe(400));
});
// ----------------------------------------
// unshare
// ----------------------------------------
test('PUT /credentials/:id/share should unshare the credential', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
test('should unshare the credential', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2] = await testDb.createManyUsers(2, {
@ -519,7 +485,7 @@ test('PUT /credentials/:id/share should unshare the credential', async () => {
await testDb.shareCredentialWithUsers(savedCredential, [member1, member2]);
const response = await authAgent(owner)
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [] });
@ -532,6 +498,7 @@ test('PUT /credentials/:id/share should unshare the credential', async () => {
expect(sharedCredentials).toHaveLength(1);
expect(sharedCredentials[0].userId).toBe(owner.id);
});
});
function validateMainCredentialData(credential: CredentialWithSharings) {
expect(typeof credential.name).toBe('string');

View file

@ -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,19 +59,14 @@ afterAll(async () => {
// ----------------------------------------
// GET /credentials - fetch all credentials
// ----------------------------------------
test('GET /credentials should return all creds for owner', async () => {
const [owner, member] = await Promise.all([
testDb.createUser({ globalRole: globalOwnerRole }),
testDb.createUser({ globalRole: globalMemberRole }),
]);
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 }),
]);
const response = await authAgent(owner).get('/credentials');
const response = await authOwnerAgent.get('/credentials');
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(2); // owner retrieved owner cred and member cred
@ -74,7 +79,7 @@ test('GET /credentials should return all creds for owner', async () => {
});
});
test('GET /credentials should return only own creds for member', async () => {
test('should return only own creds for member', async () => {
const [member1, member2] = await testDb.createManyUsers(2, {
globalRole: globalMemberRole,
});
@ -95,13 +100,13 @@ test('GET /credentials should return only own creds for member', async () => {
expect(member1Credential.data).toBeUndefined();
expect(member1Credential.id).toBe(savedCredential1.id);
});
});
test('POST /credentials should create cred', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
describe('POST /credentials', () => {
test('should create cred', async () => {
const payload = randomCredentialPayload();
const response = await authAgent(ownerShell).post('/credentials').send(payload);
const response = await authOwnerAgent.post('/credentials').send(payload);
expect(response.statusCode).toBe(200);
@ -127,14 +132,11 @@ test('POST /credentials should create cred', async () => {
where: { credentialsId: credential.id },
});
expect(sharedCredential.user.id).toBe(ownerShell.id);
expect(sharedCredential.user.id).toBe(owner.id);
expect(sharedCredential.credentials.name).toBe(payload.name);
});
test('POST /credentials should fail with invalid inputs', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = authAgent(ownerShell);
test('should fail with invalid inputs', async () => {
await Promise.all(
INVALID_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
@ -143,23 +145,18 @@ test('POST /credentials should fail with invalid inputs', async () => {
);
});
test('POST /credentials should fail with missing encryption key', async () => {
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 ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).post('/credentials').send(randomCredentialPayload());
const response = await authOwnerAgent.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);
test('should ignore ID in payload', async () => {
const firstResponse = await authOwnerAgent
.post('/credentials')
.send({ id: '8', ...randomCredentialPayload() });
@ -172,17 +169,20 @@ test('POST /credentials should ignore ID in payload', async () => {
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 });
describe('DELETE /credentials/:id', () => {
test('should delete owned cred for owner', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(ownerShell).delete(`/credentials/${savedCredential.id}`);
const response = await authOwnerAgent.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 });
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
@ -191,17 +191,17 @@ test('DELETE /credentials/:id should delete owned cred for owner', async () => {
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 });
test('should delete non-owned cred for owner', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const response = await authAgent(ownerShell).delete(`/credentials/${savedCredential.id}`);
const response = await authOwnerAgent.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 });
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
@ -210,16 +210,17 @@ test('DELETE /credentials/:id should delete non-owned cred for owner', async ()
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('DELETE /credentials/:id should delete owned cred for member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
test('should delete owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const response = await authAgent(member).delete(`/credentials/${savedCredential.id}`);
const response = await authMemberAgent.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 });
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
@ -228,12 +229,10 @@ test('DELETE /credentials/:id should delete owned cred for member', async () =>
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 });
test('should not delete non-owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(member).delete(`/credentials/${savedCredential.id}`);
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(404);
@ -246,20 +245,19 @@ test('DELETE /credentials/:id should not delete non-owned cred for member', asyn
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');
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 update owned cred for owner', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
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();
const response = await authAgent(ownerShell)
const response = await authOwnerAgent
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
@ -291,13 +289,11 @@ test('PATCH /credentials/:id should update owned cred for owner', async () => {
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated
});
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 });
test('should update non-owned cred for owner', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const patchPayload = randomCredentialPayload();
const response = await authAgent(ownerShell)
const response = await authOwnerAgent
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
@ -330,12 +326,11 @@ test('PATCH /credentials/:id should update non-owned cred for owner', async () =
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated
});
test('PATCH /credentials/:id should update owned cred for member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
test('should update owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const patchPayload = randomCredentialPayload();
const response = await authAgent(member)
const response = await authMemberAgent
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
@ -368,13 +363,11 @@ test('PATCH /credentials/:id should update owned cred for member', async () => {
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated
});
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 });
test('should not update non-owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const patchPayload = randomCredentialPayload();
const response = await authAgent(member)
const response = await authMemberAgent
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
@ -387,10 +380,8 @@ test('PATCH /credentials/:id should not update non-owned cred for member', async
expect(shellCredential.name).not.toBe(patchPayload.name); // not updated
});
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 });
test('should fail with invalid inputs', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
await Promise.all(
INVALID_PAYLOADS.map(async (invalidPayload) => {
@ -406,32 +397,26 @@ test('PATCH /credentials/:id should fail with invalid inputs', async () => {
);
});
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());
test('should fail if cred not found', async () => {
const response = await authOwnerAgent.patch('/credentials/123').send(randomCredentialPayload());
expect(response.statusCode).toBe(404);
});
test('PATCH /credentials/:id should fail with missing encryption key', async () => {
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 ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).post('/credentials').send(randomCredentialPayload());
const response = await authOwnerAgent.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);
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;
@ -445,13 +430,11 @@ test('GET /credentials/new should return default name for new credential or its
tempName = name + ' ' + (i + 1);
expect(response.body.data.name).toBe(tempName);
}
await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: ownerShell });
await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: owner });
}
});
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);
test('should return name from query for new credential or its increment', async () => {
const name = 'special credential name';
let tempName = name;
@ -465,14 +448,14 @@ test('GET /credentials/new should return name from query for new credential or i
tempName = name + ' ' + (i + 1);
expect(response.body.data.name).toBe(tempName);
}
await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: ownerShell });
await saveCredential({ ...randomCredentialPayload(), name: tempName }, { 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 });
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}`);
@ -489,9 +472,7 @@ test('GET /credentials/:id should retrieve owned cred for owner', async () => {
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);
test('should retrieve owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`);
@ -511,11 +492,7 @@ test('GET /credentials/:id should retrieve owned cred for member', async () => {
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 response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`);
@ -535,25 +512,22 @@ test('GET /credentials/:id should retrieve non-owned cred for owner', async () =
expect(response2.body.data.data).toBeDefined();
});
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 });
test('should not retrieve non-owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(member).get(`/credentials/${savedCredential.id}`);
const response = await authMemberAgent.get(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(404);
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 });
test('should fail with missing encryption key', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authAgent(ownerShell)
const response = await authOwnerAgent
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
@ -562,14 +536,14 @@ test('GET /credentials/:id should fail with missing encryption key', async () =>
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');
test('should return 404 if cred not found', async () => {
const response = await authOwnerAgent.get('/credentials/789');
expect(response.statusCode).toBe(404);
const responseAbc = await authAgent(ownerShell).get('/credentials/abc');
const responseAbc = await authOwnerAgent.get('/credentials/abc');
expect(responseAbc.statusCode).toBe(404);
});
});
function validateMainCredentialData(credential: CredentialsEntity) {
expect(typeof credential.name).toBe('string');

View file

@ -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 () => {

View file

@ -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');

View file

@ -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);
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
expect(response.body).toStrictEqual(DEFAULT_LICENSE_RESPONSE);
await authOwnerAgent.get('/license').expect(200, 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);
test('should return license information to a regular user', async () => {
// No license defined so we just expect the result to be the defaults
expect(response.body).toStrictEqual(DEFAULT_LICENSE_RESPONSE);
await authMemberAgent.get('/license').expect(200, DEFAULT_LICENSE_RESPONSE);
});
});
test('POST /license/activate should work for instance owner', async () => {
const userShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(userShell)
describe('POST /license/activate', () => {
test('should work for instance owner', async () => {
await authOwnerAgent
.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);
.send({ activationKey: 'abcde' })
.expect(200, 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)
test('does not work for regular users', async () => {
await authMemberAgent
.post('/license/activate')
.send({ activationKey: 'abcde' });
expect(response.statusCode).toBe(403);
expect(response.body.message).toBe(NON_OWNER_ACTIVATE_RENEW_MESSAGE);
.send({ activationKey: 'abcde' })
.expect(403, { code: 403, message: NON_OWNER_ACTIVATE_RENEW_MESSAGE });
});
test('POST /license/activate errors out properly', async () => {
test('errors out properly', async () => {
License.prototype.activate = jest.fn().mockImplementation(() => {
throw new Error(INVALID_ACIVATION_KEY_MESSAGE);
});
const userShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(userShell)
await authOwnerAgent
.post('/license/activate')
.send({ activationKey: 'abcde' });
expect(response.statusCode).toBe(400);
expect(response.body.message).toBe(INVALID_ACIVATION_KEY_MESSAGE);
.send({ activationKey: 'abcde' })
.expect(400, { code: 400, message: INVALID_ACIVATION_KEY_MESSAGE });
});
});
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);
describe('POST /license/renew', () => {
test('should work for instance owner', async () => {
// No license defined so we just expect the result to be the defaults
expect(response.body).toMatchObject(DEFAULT_POST_RESPONSE);
await authOwnerAgent.post('/license/renew').expect(200, 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('does not work for regular users', async () => {
await authMemberAgent
.post('/license/renew')
.expect(403, { code: 403, message: NON_OWNER_ACTIVATE_RENEW_MESSAGE });
});
test('POST /license/renew errors out properly', async () => {
test('errors out properly', async () => {
License.prototype.renew = jest.fn().mockImplementation(() => {
throw new Error(RENEW_ERROR_MESSAGE);
});
const userShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(userShell).post('/license/renew');
expect(response.statusCode).toBe(400);
expect(response.body.message).toBe(RENEW_ERROR_MESSAGE);
await authOwnerAgent
.post('/license/renew')
.expect(400, { code: 400, message: RENEW_ERROR_MESSAGE });
});
});
const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = {

View file

@ -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 () => {

View file

@ -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,41 +68,32 @@ afterAll(async () => {
await testDb.terminate();
});
/**
* GET /nodes
*/
test('GET /nodes should respond 200 if no nodes are installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
describe('GET /nodes', () => {
test('should respond 200 if no nodes are installed', async () => {
const {
statusCode,
body: { data },
} = await authAgent(ownerShell).get('/nodes');
} = await authOwnerShellAgent.get('/nodes');
expect(statusCode).toBe(200);
expect(data).toHaveLength(0);
});
test('GET /nodes should return list of one installed package and node', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should return list of one installed package and node', async () => {
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
const {
statusCode,
body: { data },
} = await authAgent(ownerShell).get('/nodes');
} = await authOwnerShellAgent.get('/nodes');
expect(statusCode).toBe(200);
expect(data).toHaveLength(1);
expect(data[0].installedNodes).toHaveLength(1);
});
test('GET /nodes should return list of multiple installed packages and nodes', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
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));
@ -118,7 +104,7 @@ test('GET /nodes should return list of multiple installed packages and nodes', a
const {
statusCode,
body: { data },
} = await authAgent(ownerShell).get('/nodes');
} = await authOwnerShellAgent.get('/nodes');
expect(statusCode).toBe(200);
expect(data).toHaveLength(2);
@ -131,30 +117,24 @@ test('GET /nodes should return list of multiple installed packages and nodes', a
expect(allNodes).toHaveLength(3);
});
test('GET /nodes should not check for updates if no packages installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
await authAgent(ownerShell).get('/nodes');
test('should not check for updates if no packages installed', async () => {
await authOwnerShellAgent.get('/nodes');
expect(mocked(executeCommand)).toHaveBeenCalledTimes(0);
});
test('GET /nodes should check for updates if packages installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should check for updates if packages installed', async () => {
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
await authAgent(ownerShell).get('/nodes');
await authOwnerShellAgent.get('/nodes');
expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', {
doNotHandleError: true,
});
});
test('GET /nodes should report package updates if available', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should report package updates if available', async () => {
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
@ -176,27 +156,21 @@ test('GET /nodes should report package updates if available', async () => {
const {
body: { data },
} = await authAgent(ownerShell).get('/nodes');
} = await authOwnerShellAgent.get('/nodes');
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');
describe('POST /nodes', () => {
test('should reject if package name is missing', async () => {
const { statusCode } = await authOwnerShellAgent.post('/nodes');
expect(statusCode).toBe(400);
});
test('POST /nodes should reject if package is duplicate', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should reject if package is duplicate', async () => {
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
mocked(isPackageInstalled).mockResolvedValueOnce(true);
mocked(hasPackageLoaded).mockReturnValueOnce(true);
@ -204,7 +178,7 @@ test('POST /nodes should reject if package is duplicate', async () => {
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).post('/nodes').send({
} = await authOwnerShellAgent.post('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
@ -212,16 +186,14 @@ test('POST /nodes should reject if package is duplicate', async () => {
expect(message).toContain('already installed');
});
test('POST /nodes should allow installing packages that could not be loaded', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
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' });
mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authAgent(ownerShell).post('/nodes').send({
const { statusCode } = await authOwnerShellAgent.post('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
@ -229,40 +201,33 @@ test('POST /nodes should allow installing packages that could not be loaded', as
expect(mocked(removePackageFromMissingList)).toHaveBeenCalled();
});
test('POST /nodes should not install a banned package', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should not install a banned package', async () => {
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' });
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).post('/nodes').send({
} = await authOwnerShellAgent.post('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
expect(statusCode).toBe(400);
expect(message).toContain('banned');
});
});
/**
* 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');
describe('DELETE /nodes', () => {
test('should not delete if package name is empty', async () => {
const response = await authOwnerShellAgent.delete('/nodes');
expect(response.statusCode).toBe(400);
});
test('DELETE /nodes should reject if package is not installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should reject if package is not installed', async () => {
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).delete('/nodes').query({
} = await authOwnerShellAgent.delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
});
@ -270,40 +235,32 @@ test('DELETE /nodes should reject if package is not installed', async () => {
expect(message).toContain('not installed');
});
test('DELETE /nodes should uninstall package', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should uninstall package', async () => {
const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn());
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authAgent(ownerShell).delete('/nodes').query({
const { statusCode } = await authOwnerShellAgent.delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
});
expect(statusCode).toBe(200);
expect(removeSpy).toHaveBeenCalledTimes(1);
});
});
/**
* 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');
describe('PATCH /nodes', () => {
test('should reject if package name is empty', async () => {
const response = await authOwnerShellAgent.patch('/nodes');
expect(response.statusCode).toBe(400);
});
test('PATCH /nodes reject if package is not installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('reject if package is not installed', async () => {
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).patch('/nodes').send({
} = await authOwnerShellAgent.patch('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
@ -311,17 +268,16 @@ test('PATCH /nodes reject if package is not installed', async () => {
expect(message).toContain('not installed');
});
test('PATCH /nodes should update a package', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should update a package', async () => {
const updateSpy =
mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage);
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
await authAgent(ownerShell).patch('/nodes').send({
await authOwnerShellAgent.patch('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
expect(updateSpy).toHaveBeenCalledTimes(1);
});
});

View file

@ -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,9 +39,8 @@ 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(),
@ -48,7 +48,7 @@ test('POST /owner/setup should create owner and enable isInstanceOwnerSetUp', as
password: randomValidPassword(),
};
const response = await authAgent(ownerShell).post('/owner/setup').send(newOwnerData);
const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData);
expect(response.statusCode).toBe(200);
@ -90,9 +90,7 @@ test('POST /owner/setup should create owner and enable isInstanceOwnerSetUp', as
expect(isInstanceOwnerSetUpSetting).toBe(true);
});
test('POST /owner/setup should create owner with lowercased email', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
test('should create owner with lowercased email', async () => {
const newOwnerData = {
email: randomEmail().toUpperCase(),
firstName: randomName(),
@ -100,7 +98,7 @@ test('POST /owner/setup should create owner with lowercased email', async () =>
password: randomValidPassword(),
};
const response = await authAgent(ownerShell).post('/owner/setup').send(newOwnerData);
const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData);
expect(response.statusCode).toBe(200);
@ -113,34 +111,6 @@ test('POST /owner/setup should create owner with lowercased email', async () =>
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',
});
expect(value).toBe('true');
});
const INVALID_POST_OWNER_PAYLOADS = [
{
email: '',
@ -187,3 +157,31 @@ const INVALID_POST_OWNER_PAYLOADS = [
lastName: randomName(),
},
];
test('should fail with invalid inputs', async () => {
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);
}),
);
});
});
describe('POST /owner/skip-setup', () => {
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');
});
});

View file

@ -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,38 +14,36 @@ 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 });
const authlessAgent = utils.createAgent(app);
describe('POST /forgot-password', () => {
test('should send password reset email', async () => {
const member = await testDb.createUser({
email: 'test@test.com',
globalRole: globalMemberRole,
@ -65,24 +65,16 @@ test('POST /forgot-password should send password reset email', async () => {
);
});
test('POST /forgot-password should fail if emailing is not set up', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
test('should fail if emailing is not set up', async () => {
config.set('userManagement.emails.mode', '');
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.post('/forgot-password').send({ email: owner.email });
expect(response.statusCode).toBe(500);
await authlessAgent.post('/forgot-password').send({ email: owner.email }).expect(500);
const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email });
expect(storedOwner.resetPasswordToken).toBeNull();
});
test('POST /forgot-password should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
test('should fail with invalid inputs', async () => {
config.set('userManagement.emails.mode', 'smtp');
const invalidPayloads = [
@ -104,21 +96,21 @@ test('POST /forgot-password should fail with invalid inputs', async () => {
);
});
test('POST /forgot-password should fail if user is not found', async () => {
const authlessAgent = utils.createAgent(app);
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('GET /resolve-password-token should succeed with valid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
describe('GET /resolve-password-token', () => {
beforeEach(() => {
config.set('userManagement.emails.mode', 'smtp');
});
test('should succeed with valid inputs', async () => {
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
@ -134,13 +126,7 @@ test('GET /resolve-password-token should succeed with valid inputs', async () =>
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');
test('should fail with invalid inputs', async () => {
const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() });
const second = await authlessAgent.get('/resolve-password-token').query({ userId: owner.id });
@ -150,13 +136,7 @@ test('GET /resolve-password-token should fail with invalid inputs', async () =>
}
});
test('GET /resolve-password-token should fail if user is not found', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
config.set('userManagement.emails.mode', 'smtp');
test('should fail if user is not found', async () => {
const response = await authlessAgent
.get('/resolve-password-token')
.query({ userId: owner.id, token: uuid() });
@ -164,11 +144,7 @@ test('GET /resolve-password-token should fail if user is not found', async () =>
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);
test('should fail if token is expired', async () => {
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
@ -177,21 +153,19 @@ test('GET /resolve-password-token should fail if token is expired', async () =>
resetPasswordTokenExpiration,
});
config.set('userManagement.emails.mode', 'smtp');
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 passwordToStore = randomValidPassword();
test('should succeed with valid inputs', async () => {
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User.update(owner.id, {
@ -199,8 +173,6 @@ test('POST /change-password should succeed with valid inputs', async () => {
resetPasswordTokenExpiration,
});
const passwordToStore = randomValidPassword();
const response = await authlessAgent.post('/change-password').send({
token: resetPasswordToken,
userId: owner.id,
@ -212,19 +184,16 @@ test('POST /change-password should succeed with valid inputs', async () => {
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
const { password: storedPassword } = await Db.collections.User.findOneByOrFail({ id: owner.id });
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();
test('should fail with invalid inputs', async () => {
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User.update(owner.id, {
@ -262,12 +231,7 @@ test('POST /change-password should fail with invalid inputs', async () => {
);
});
test('POST /change-password should fail when token has expired', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid();
test('should fail when token has expired', async () => {
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
await Db.collections.User.update(owner.id, {
@ -275,8 +239,6 @@ test('POST /change-password should fail when token has expired', async () => {
resetPasswordTokenExpiration,
});
const passwordToStore = randomValidPassword();
const response = await authlessAgent.post('/change-password').send({
token: resetPasswordToken,
userId: owner.id,
@ -285,3 +247,4 @@ test('POST /change-password should fail when token has expired', async () => {
expect(response.statusCode).toBe(404);
});
});

View file

@ -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,36 +27,43 @@ 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);
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
version: 1,
auth: true,
user: ownerShell,
});
describe('POST /credentials', () => {
test('should create credentials', async () => {
const payload = {
name: 'test credential',
type: 'githubApi',
@ -83,24 +91,14 @@ test('POST /credentials should create credentials', async () => {
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['user', 'credentials', 'role'],
where: { credentialsId: credential.id, userId: ownerShell.id },
where: { credentialsId: credential.id, userId: owner.id },
});
expect(sharedCredential.role).toEqual(credentialOwnerRole);
expect(sharedCredential.credentials.name).toBe(payload.name);
});
test('POST /credentials should fail with invalid inputs', 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,
});
test('should fail with invalid inputs', async () => {
await Promise.all(
INVALID_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
@ -109,39 +107,21 @@ test('POST /credentials should fail with invalid inputs', async () => {
);
});
test('POST /credentials should fail with missing encryption key', async () => {
test('should fail with missing encryption key', async () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
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.post('/credentials').send(credentialPayload());
expect(response.statusCode).toBe(500);
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,
});
const savedCredential = await saveCredential(dbCredential(), { user: ownerShell });
describe('DELETE /credentials/:id', () => {
test('should delete owned cred for owner', async () => {
const savedCredential = await saveCredential(dbCredential(), { user: owner });
const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
@ -152,7 +132,9 @@ test('DELETE /credentials/:id should delete owned cred for owner', async () => {
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
@ -161,26 +143,16 @@ test('DELETE /credentials/:id should delete owned cred for owner', async () => {
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 });
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 });
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
@ -189,16 +161,7 @@ test('DELETE /credentials/:id should delete non-owned cred for owner', async ()
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,
});
test('should delete owned cred for member', async () => {
const savedCredential = await saveCredential(dbCredential(), { user: member });
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
@ -210,7 +173,9 @@ test('DELETE /credentials/:id should delete owned cred for member', async () =>
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
@ -219,19 +184,16 @@ test('DELETE /credentials/:id should delete owned cred for member', async () =>
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() });
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: 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 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}`);
@ -243,7 +205,9 @@ test('DELETE /credentials/:id should delete owned cred for member but leave othe
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
@ -257,7 +221,9 @@ test('DELETE /credentials/:id should delete owned cred for member but leave othe
await Promise.all(
[notToBeChangedCredential, notToBeChangedCredential2].map(async (credential) => {
const untouchedCredential = await Db.collections.Credentials.findOneBy({ id: credential.id });
const untouchedCredential = await Db.collections.Credentials.findOneBy({
id: credential.id,
});
expect(untouchedCredential).toEqual(credential); // not deleted
@ -272,23 +238,16 @@ test('DELETE /credentials/:id should delete owned cred for member but leave othe
);
});
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 });
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 });
const shellCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(shellCredential).toBeDefined(); // not deleted
@ -297,49 +256,21 @@ test('DELETE /credentials/:id should not delete non-owned cred for member', asyn
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,
});
test('should fail if cred not found', async () => {
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,
});
describe('GET /credentials/schema/:credentialType', () => {
test('should fail due to not found type', async () => {
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,
});
test('should retrieve credential type', async () => {
const response = await authOwnerAgent.get('/credentials/schema/githubApi');
const { additionalProperties, type, properties, required } = response.body;
@ -355,6 +286,7 @@ test('GET /credentials/schema/:credentialType should retrieve credential type',
expect(required).toEqual(expect.arrayContaining(['server', 'user', 'accessToken']));
expect(response.statusCode).toBe(200);
});
});
const credentialPayload = (): CredentialPayload => ({
name: randomName(),

View file

@ -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,47 +58,19 @@ 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 authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const response = await authOwnerAgent.get('/executions/1');
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);
});
};
test('GET /executions/:id should fail due to invalid API Key', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
owner.apiKey = 'abcXYZ';
describe('GET /executions/:id', () => {
test('should fail due to missing API Key', testWithAPIKey('get', '/executions/1', null));
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const response = await authOwnerAgent.get('/executions/1');
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,
});
test('should fail due to invalid API Key', testWithAPIKey('get', '/executions/1', 'abcXYZ'));
test('should get an execution', async () => {
const workflow = await testDb.createWorkflow({}, owner);
const execution = await testDb.createSuccessfulExecution(workflow);
@ -121,50 +101,15 @@ test('GET /executions/:id should get an execution', async () => {
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');
describe('DELETE /executions/:id', () => {
test('should fail due to missing API Key', testWithAPIKey('delete', '/executions/1', null));
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,
});
test('should fail due to invalid API Key', testWithAPIKey('delete', '/executions/1', 'abcXYZ'));
test('should delete an execution', async () => {
const workflow = await testDb.createWorkflow({}, owner);
const execution = await testDb.createSuccessfulExecution(workflow);
const response = await authOwnerAgent.delete(`/executions/${execution.id}`);
@ -193,51 +138,17 @@ test('DELETE /executions/:id should delete an execution', async () => {
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');
describe('GET /executions', () => {
test('should fail due to missing API Key', testWithAPIKey('get', '/executions', null));
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,
});
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 successfullExecution = await testDb.createSuccessfulExecution(workflow);
const successfulExecution = await testDb.createSuccessfulExecution(workflow);
await testDb.createErrorExecution(workflow);
@ -263,26 +174,17 @@ test('GET /executions should retrieve all successful executions', async () => {
expect(id).toBeDefined();
expect(finished).toBe(true);
expect(mode).toEqual(successfullExecution.mode);
expect(mode).toEqual(successfulExecution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(successfullExecution.workflowId);
expect(workflowId).toBe(successfulExecution.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,
});
test.skip('should paginate two executions', async () => {
const workflow = await testDb.createWorkflow({}, owner);
const firstSuccessfulExecution = await testDb.createSuccessfulExecution(workflow);
@ -338,16 +240,7 @@ test.skip('GET /executions should paginate two executions', async () => {
}
});
test('GET /executions should retrieve all error executions', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
test('should retrieve all error executions', async () => {
const workflow = await testDb.createWorkflow({}, owner);
await testDb.createSuccessfulExecution(workflow);
@ -385,16 +278,7 @@ test('GET /executions should retrieve all error executions', async () => {
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,
});
test('should return all waiting executions', async () => {
const workflow = await testDb.createWorkflow({}, owner);
await testDb.createSuccessfulExecution(workflow);
@ -434,16 +318,7 @@ test('GET /executions should return all waiting executions', async () => {
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,
});
test('should retrieve all executions of specific workflow', async () => {
const [workflow, workflow2] = await testDb.createManyWorkflows(2, {}, owner);
const savedExecutions = await testDb.createManyExecutions(
@ -487,3 +362,4 @@ test('GET /executions should retrieve all executions of specific workflow', asyn
expect(waitTill).toBeNull();
}
});
});

View file

@ -2,19 +2,16 @@ 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;
@ -29,11 +26,8 @@ describe('Workflows Public API', () => {
enablePublicAPI: true,
});
const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole, fetchedWorkflowOwnerRole] =
await testDb.getAllRoles();
const [globalOwnerRole, globalMemberRole, fetchedWorkflowOwnerRole] = await testDb.getAllRoles();
globalOwnerRole = fetchedGlobalOwnerRole;
globalMemberRole = fetchedGlobalMemberRole;
workflowOwnerRole = fetchedWorkflowOwnerRole;
owner = await testDb.createUser({
@ -52,13 +46,7 @@ describe('Workflows Public API', () => {
});
beforeEach(async () => {
await testDb.truncate([
'SharedCredentials',
'SharedWorkflow',
'Tag',
'Workflow',
'Credentials',
]);
await testDb.truncate(['SharedCredentials', 'SharedWorkflow', 'Tag', 'Workflow', 'Credentials']);
authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
@ -451,10 +439,7 @@ describe('Workflows Public API', () => {
});
describe('POST /workflows/:id/activate', () => {
test(
'should fail due to missing API Key',
testWithAPIKey('post', '/workflows/2/activate', null),
);
test('should fail due to missing API Key', testWithAPIKey('post', '/workflows/2/activate', null));
test(
'should fail due to invalid API Key',
@ -921,4 +906,3 @@ describe('Workflows Public API', () => {
expect(sharedWorkflow?.role).toEqual(workflowOwnerRole);
});
});
});

View file

@ -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({
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(response.statusCode).toBe(200);
})
.expect(200);
});
test('PATCH /me should throw BadRequestError if email is changed when SAML is enabled', async () => {
test('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({
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(400);
expect(response.body.message).toContain('SAML');
enableSaml(false);
});
test('PATCH /password should throw BadRequestError if password is changed when SAML is enabled', async () => {
describe('PATCH /password', () => {
test('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({
await authOwnerAgent
.patch('/me/password')
.send({
password: randomValidPassword(),
});
expect(response.statusCode).toBe(400);
expect(response.body.message).toContain('SAML');
enableSaml(false);
})
.expect(400, {
code: 400,
message: 'With SAML enabled, users need to use their SAML provider to change passwords',
});
});
});
});

View file

@ -1,5 +1,4 @@
import superagent = require('superagent');
import type { ObjectLiteral } from 'typeorm';
/**
* Make `SuperTest<T>` string-indexable.

View file

@ -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 = <T>(
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
// ----------------------------------

View file

@ -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 });
const response = await authAgent(owner).get('/users');
const response = await authOwnerAgent.get('/users');
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,13 +101,12 @@ 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 newWorkflow = new WorkflowEntity();
@ -143,7 +143,7 @@ test('DELETE /users/:id should delete the user', async () => {
credentials: savedCredential,
});
const response = await authAgent(owner).delete(`/users/${userToDelete.id}`);
const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
@ -172,10 +172,8 @@ test('DELETE /users/:id should delete the user', async () => {
expect(credential).toBeNull(); // deleted
});
test('DELETE /users/:id should fail to delete self', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const response = await authAgent(owner).delete(`/users/${owner.id}`);
test('should fail to delete self', async () => {
const response = await authOwnerAgent.delete(`/users/${owner.id}`);
expect(response.statusCode).toBe(400);
@ -183,12 +181,10 @@ test('DELETE /users/:id should fail to delete self', async () => {
expect(user).toBeDefined();
});
test('DELETE /users/:id should fail if user to delete is transferee', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
test('should fail if user to delete is transferee', async () => {
const { id: idToDelete } = await testDb.createUser({ globalRole: globalMemberRole });
const response = await authAgent(owner).delete(`/users/${idToDelete}`).query({
const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({
transferId: idToDelete,
});
@ -198,9 +194,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () =
expect(user).toBeDefined();
});
test('DELETE /users/:id with transferId should perform transfer', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
test('with transferId should perform transfer', async () => {
const userToDelete = await testDb.createUser({ globalRole: globalMemberRole });
const savedWorkflow = await testDb.createWorkflow(undefined, userToDelete);
@ -210,7 +204,7 @@ test('DELETE /users/:id with transferId should perform transfer', async () => {
role: credentialOwnerRole,
});
const response = await authAgent(owner).delete(`/users/${userToDelete.id}`).query({
const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`).query({
transferId: owner.id,
});
@ -236,10 +230,10 @@ test('DELETE /users/:id with transferId should perform transfer', async () => {
expect(deletedUser).toBeNull();
});
});
test('POST /users/:id should fill out a user shell', 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 memberData = {
@ -249,8 +243,6 @@ test('POST /users/:id should fill out a user shell', async () => {
password: randomValidPassword(),
};
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.post(`/users/${memberShell.id}`).send(memberData);
const {
@ -286,11 +278,7 @@ test('POST /users/:id should fill out a user shell', async () => {
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);
test('should fail with invalid inputs', async () => {
const memberShellEmail = randomEmail();
const memberShell = await Db.collections.User.save({
@ -343,8 +331,7 @@ test('POST /users/:id should fail with invalid inputs', async () => {
);
});
test('POST /users/:id should fail with already accepted invite', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
test('should fail with already accepted invite', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const newMemberData = {
@ -354,8 +341,6 @@ test('POST /users/:id should fail with already accepted invite', async () => {
password: randomValidPassword(),
};
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.post(`/users/${member.id}`).send(newMemberData);
expect(response.statusCode).toBe(400);
@ -370,43 +355,43 @@ test('POST /users/:id should fail with already accepted invite', async () => {
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 });
describe('POST /users', () => {
beforeEach(() => {
config.set('userManagement.emails.mode', 'smtp');
});
const response = await authAgent(owner)
.post('/users')
.send([{ email: randomEmail() }]);
test('should succeed if emailing is not set up', async () => {
const response = await authOwnerAgent.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 });
test('should fail if user management is disabled', async () => {
config.set('userManagement.disabled', true);
config.set('userManagement.isInstanceOwnerSetUp', false);
const response = await authAgent(owner)
.post('/users')
.send([{ email: randomEmail() }]);
const response = await authOwnerAgent.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 });
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);
config.set('userManagement.emails.mode', 'smtp');
const testEmails = [randomEmail(), randomEmail().toUpperCase(), memberShell.email, member.email];
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);
const response = await authOwnerAgent.post('/users').send(payload);
expect(response.statusCode).toBe(200);
@ -437,12 +422,7 @@ test('POST /users should email invites and create user shells but ignore existin
}
});
test('POST /users should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = authAgent(owner);
config.set('userManagement.emails.mode', 'smtp');
test('should fail with invalid inputs', async () => {
const invalidPayloads = [
randomEmail(),
[randomEmail()],
@ -462,12 +442,8 @@ test('POST /users should fail with invalid inputs', async () => {
);
});
test('POST /users should ignore an empty payload', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
config.set('userManagement.emails.mode', 'smtp');
const response = await authAgent(owner).post('/users').send([]);
test('should ignore an empty payload', async () => {
const response = await authOwnerAgent.post('/users').send([]);
const { data } = response.body;
@ -478,18 +454,18 @@ test('POST /users should ignore an empty payload', async () => {
const users = await Db.collections.User.find();
expect(users.length).toBe(1);
});
});
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('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');
});
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);
@ -507,26 +483,33 @@ test('POST /users/:id/reinvite should send reinvite, but fail if user already ac
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 () => {});
describe('UserManagementMailer expect NodeMailer.verifyConnection', () => {
let mockInit: jest.SpyInstance<Promise<void>, []>;
let mockVerifyConnection: jest.SpyInstance<Promise<void>, []>;
beforeAll(() => {
mockVerifyConnection = jest
.spyOn(NodeMailer.prototype, 'verifyConnection')
.mockImplementation(async () => {});
mockInit = jest.spyOn(NodeMailer.prototype, 'init').mockImplementation(async () => {});
});
afterAll(() => {
mockVerifyConnection.mockRestore();
mockInit.mockRestore();
});
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(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 () => {});
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');
@ -534,8 +517,5 @@ test('UserManagementMailer expect NodeMailer.verifyConnection to be called when
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();
});
});

View file

@ -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<boolean>;
let workflowRunner: ActiveWorkflowRunner;
let sharingSpy: jest.SpyInstance<boolean>;
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,12 +503,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({
versionId,
nodes: [
{
@ -578,9 +530,6 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
});
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,12 +554,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(member)
.patch(`/workflows/${id}`)
.send({
const response = await authMemberAgent.patch(`/workflows/${id}`).send({
versionId,
nodes: [
{
@ -643,10 +590,7 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
});
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 });

View file

@ -1,37 +1,36 @@
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;
let ownerShell: User;
let authOwnerAgent: SuperAgentTest;
beforeAll(async () => {
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 });
// mock whether sharing is enabled or not
jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(false);
beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['workflows'] });
globalOwnerRole = await testDb.getGlobalOwnerRole();
});
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 response = await authOwnerAgent.post('/workflows').send(workflow);
@ -43,10 +42,7 @@ test('POST /workflows should store pin data for node in workflow', async () => {
expect(pinData).toMatchObject(MOCK_PINDATA);
});
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 });
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);
@ -57,11 +53,10 @@ test('POST /workflows should set pin data to null if no pin data', async () => {
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 });
describe('GET /workflows/:id', () => {
test('should return pin data', async () => {
const workflow = makeWorkflow({ withPinData: true });
const workflowCreationResponse = await authOwnerAgent.post('/workflows').send(workflow);
@ -76,3 +71,4 @@ test('GET /workflows/:id should return pin data', async () => {
expect(pinData).toMatchObject(MOCK_PINDATA);
});
});

View file

@ -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';

View file

@ -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';
describe('CredentialsHelper', () => {
const TEST_ENCRYPTION_KEY = 'test';
const mockNodesAndCredentials: INodesAndCredentials = {
loaded: { nodes: {}, credentials: {} },
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);
describe('CredentialsHelper', () => {
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: {},

View file

@ -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<WorkflowStatistics>;
jest.mock('@/Db', () => {
return {
collections: {
WorkflowStatistics: {
insert: jest.fn((...args) => {}),
update: jest.fn((...args) => {}),
},
WorkflowStatistics: mock<WorkflowStatisticsRepository>(),
},
};
});
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<WorkflowStatisticsRepository>
>;
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';

View file

@ -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

View file

@ -1,39 +1,35 @@
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;
beforeAll(async () => {
await testDb.init();
mockNodeTypes = MockNodeTypes({
const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']);
mockInstance(LoadNodesAndCredentials, {
loaded: {
nodes: MOCK_NODE_TYPES_DATA,
credentials: {},
@ -42,6 +38,11 @@ beforeAll(async () => {
credentialTypes: {} as ICredentialTypes,
});
beforeAll(async () => {
await testDb.init();
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']);

File diff suppressed because it is too large Load diff