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') { if (process.env.CI === 'true') {
config.maxWorkers = 2; config.workerIdleMemoryLimit = 1024;
config.workerIdleMemoryLimit = 2048;
config.coverageReporters = ['cobertura']; config.coverageReporters = ['cobertura'];
} }

View file

@ -39,15 +39,15 @@
"devDependencies": { "devDependencies": {
"@n8n_io/eslint-config": "workspace:*", "@n8n_io/eslint-config": "workspace:*",
"@ngneat/falso": "^6.1.0", "@ngneat/falso": "^6.1.0",
"@types/jest": "^29.2.2", "@types/jest": "^29.5.0",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^12.7.0", "cypress": "^12.7.0",
"cypress-real-events": "^1.7.6", "cypress-real-events": "^1.7.6",
"jest": "^29.4.2", "jest": "^29.5.0",
"jest-environment-jsdom": "^29.4.2", "jest-environment-jsdom": "^29.5.0",
"jest-mock": "^29.4.2", "jest-mock": "^29.5.0",
"jest-mock-extended": "^3.0.1", "jest-mock-extended": "^3.0.3",
"nock": "^13.2.9", "nock": "^13.2.9",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"p-limit": "^3.1.0", "p-limit": "^3.1.0",

View file

@ -1,30 +1 @@
/* eslint-disable import/first */ export {};
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,
};

View file

@ -1,339 +1,338 @@
import express from 'express'; import type { Application } from 'express';
import type { SuperAgentTest } from 'supertest';
import validator from 'validator'; import validator from 'validator';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME } from '@/constants';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants';
import { randomValidPassword } from './shared/random'; import { randomValidPassword } from './shared/random';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types'; import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
let app: express.Application; let app: Application;
let globalOwnerRole: Role; let globalOwnerRole: Role;
let globalMemberRole: Role; let globalMemberRole: Role;
let owner: User;
let authAgent: AuthAgent; let authAgent: AuthAgent;
let authlessAgent: SuperAgentTest;
let authOwnerAgent: SuperAgentTest;
const ownerPassword = randomValidPassword();
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['auth'] }); app = await utils.initTestServer({ endpointGroups: ['auth'] });
authAgent = utils.createAuthAgent(app);
globalOwnerRole = await testDb.getGlobalOwnerRole(); globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole(); globalMemberRole = await testDb.getGlobalMemberRole();
authAgent = utils.createAuthAgent(app);
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['User']); await testDb.truncate(['User']);
authlessAgent = utils.createAgent(app);
config.set('ldap.disabled', true); config.set('ldap.disabled', true);
await utils.setInstanceOwnerSetUp(true);
config.set('userManagement.isInstanceOwnerSetUp', true);
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(true) },
);
}); });
afterAll(async () => { afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('POST /login should log user in', async () => { describe('POST /login', () => {
const ownerPassword = randomValidPassword(); beforeEach(async () => {
const owner = await testDb.createUser({ owner = await testDb.createUser({
password: ownerPassword, password: ownerPassword,
globalRole: globalOwnerRole, globalRole: globalOwnerRole,
});
}); });
const authlessAgent = utils.createAgent(app); test('should log user in', async () => {
const response = await authlessAgent.post('/login').send({
email: owner.email,
password: ownerPassword,
});
const response = await authlessAgent.post('/login').send({ expect(response.statusCode).toBe(200);
email: owner.email,
password: ownerPassword, const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(owner.email);
expect(firstName).toBe(owner.firstName);
expect(lastName).toBe(owner.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
});
});
describe('GET /login', () => {
test('should return 401 Unauthorized if no cookie', async () => {
const response = await authlessAgent.get('/login');
expect(response.statusCode).toBe(401);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
}); });
expect(response.statusCode).toBe(200); test('should return cookie if UM is disabled and no cookie is already set', async () => {
await testDb.createUserShell(globalOwnerRole);
await utils.setInstanceOwnerSetUp(false);
const { const response = await authlessAgent.get('/login');
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true); expect(response.statusCode).toBe(200);
expect(email).toBe(owner.email);
expect(firstName).toBe(owner.firstName);
expect(lastName).toBe(owner.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response); const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined(); expect(authToken).toBeDefined();
});
test('should return 401 Unauthorized if invalid cookie', async () => {
authlessAgent.jar.setCookie(`${AUTH_COOKIE_NAME}=invalid`);
const response = await authlessAgent.get('/login');
expect(response.statusCode).toBe(401);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('should return logged-in owner shell', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBeDefined();
expect(firstName).toBeNull();
expect(lastName).toBeNull();
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('should return logged-in member shell', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(memberShell).get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBeDefined();
expect(firstName).toBeNull();
expect(lastName).toBeNull();
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('should return logged-in owner', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const response = await authAgent(owner).get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(owner.email);
expect(firstName).toBe(owner.firstName);
expect(lastName).toBe(owner.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('should return logged-in member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const response = await authAgent(member).get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(member.email);
expect(firstName).toBe(member.firstName);
expect(lastName).toBe(member.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
}); });
test('GET /login should return 401 Unauthorized if no cookie', async () => { describe('GET /resolve-signup-token', () => {
const authlessAgent = utils.createAgent(app); beforeEach(async () => {
owner = await testDb.createUser({
password: ownerPassword,
globalRole: globalOwnerRole,
});
authOwnerAgent = authAgent(owner);
});
const response = await authlessAgent.get('/login'); test('should validate invite token', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
expect(response.statusCode).toBe(401); const response = await authOwnerAgent
.get('/resolve-signup-token')
.query({ inviterId: owner.id })
.query({ inviteeId: memberShell.id });
const authToken = utils.getAuthToken(response); expect(response.statusCode).toBe(200);
expect(authToken).toBeUndefined(); expect(response.body).toEqual({
}); data: {
inviter: {
test('GET /login should return cookie if UM is disabled and no cookie is already set', async () => { firstName: owner.firstName,
const authlessAgent = utils.createAgent(app); lastName: owner.lastName,
await testDb.createUserShell(globalOwnerRole); },
config.set('userManagement.isInstanceOwnerSetUp', false);
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(false) },
);
const response = await authlessAgent.get('/login');
expect(response.statusCode).toBe(200);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
});
test('GET /login should return 401 Unauthorized if invalid cookie', async () => {
const invalidAuthAgent = utils.createAgent(app);
invalidAuthAgent.jar.setCookie(`${AUTH_COOKIE_NAME}=invalid`);
const response = await invalidAuthAgent.get('/login');
expect(response.statusCode).toBe(401);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in owner shell', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBeDefined();
expect(firstName).toBeNull();
expect(lastName).toBeNull();
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in member shell', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(memberShell).get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBeDefined();
expect(firstName).toBeNull();
expect(lastName).toBeNull();
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in owner', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const response = await authAgent(owner).get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(owner.email);
expect(firstName).toBe(owner.firstName);
expect(lastName).toBe(owner.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /login should return logged-in member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const response = await authAgent(member).get('/login');
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
password,
personalizationAnswers,
globalRole,
resetPasswordToken,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(member.email);
expect(firstName).toBe(member.firstName);
expect(lastName).toBe(member.lastName);
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
});
test('GET /resolve-signup-token should validate invite token', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const memberShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(owner)
.get('/resolve-signup-token')
.query({ inviterId: owner.id })
.query({ inviteeId: memberShell.id });
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({
data: {
inviter: {
firstName: owner.firstName,
lastName: owner.lastName,
}, },
}, });
});
test('should fail with invalid inputs', async () => {
const { id: inviteeId } = await testDb.createUser({ globalRole: globalMemberRole });
const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id });
const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId });
const third = await authOwnerAgent.get('/resolve-signup-token').query({
inviterId: '5531199e-b7ae-425b-a326-a95ef8cca59d',
inviteeId: 'cb133beb-7729-4c34-8cd1-a06be8834d9d',
});
// user is already set up, so call should error
const fourth = await authOwnerAgent
.get('/resolve-signup-token')
.query({ inviterId: owner.id })
.query({ inviteeId });
// cause inconsistent DB state
await Db.collections.User.update(owner.id, { email: '' });
const fifth = await authOwnerAgent
.get('/resolve-signup-token')
.query({ inviterId: owner.id })
.query({ inviteeId });
for (const response of [first, second, third, fourth, fifth]) {
expect(response.statusCode).toBe(400);
}
}); });
}); });
test('GET /resolve-signup-token should fail with invalid inputs', async () => { describe('POST /logout', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); test('should log user out', async () => {
const authOwnerAgent = authAgent(owner); const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const { id: inviteeId } = await testDb.createUser({ globalRole: globalMemberRole }); const response = await authAgent(owner).post('/logout');
const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id }); expect(response.statusCode).toBe(200);
expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY);
const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId }); const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
const third = await authOwnerAgent.get('/resolve-signup-token').query({
inviterId: '5531199e-b7ae-425b-a326-a95ef8cca59d',
inviteeId: 'cb133beb-7729-4c34-8cd1-a06be8834d9d',
}); });
// user is already set up, so call should error
const fourth = await authOwnerAgent
.get('/resolve-signup-token')
.query({ inviterId: owner.id })
.query({ inviteeId });
// cause inconsistent DB state
await Db.collections.User.update(owner.id, { email: '' });
const fifth = await authOwnerAgent
.get('/resolve-signup-token')
.query({ inviterId: owner.id })
.query({ inviteeId });
for (const response of [first, second, third, fourth, fifth]) {
expect(response.statusCode).toBe(400);
}
});
test('POST /logout should log user out', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const response = await authAgent(owner).post('/logout');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(LOGGED_OUT_RESPONSE_BODY);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeUndefined();
}); });

View file

@ -1,26 +1,21 @@
import express from 'express'; import type { SuperAgentTest } from 'supertest';
import request from 'supertest';
import type { Role } from '@db/entities/Role';
import { import {
REST_PATH_SEGMENT,
ROUTES_REQUIRING_AUTHENTICATION, ROUTES_REQUIRING_AUTHENTICATION,
ROUTES_REQUIRING_AUTHORIZATION, ROUTES_REQUIRING_AUTHORIZATION,
} from './shared/constants'; } from './shared/constants';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
let app: express.Application; let authlessAgent: SuperAgentTest;
let globalMemberRole: Role; let authMemberAgent: SuperAgentTest;
let authAgent: AuthAgent;
beforeAll(async () => { 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(); authlessAgent = utils.createAgent(app);
authMemberAgent = utils.createAuthAgent(app)(member);
authAgent = utils.createAuthAgent(app);
}); });
afterAll(async () => { afterAll(async () => {
@ -31,9 +26,8 @@ ROUTES_REQUIRING_AUTHENTICATION.concat(ROUTES_REQUIRING_AUTHORIZATION).forEach((
const [method, endpoint] = getMethodAndEndpoint(route); const [method, endpoint] = getMethodAndEndpoint(route);
test(`${route} should return 401 Unauthorized if no cookie`, async () => { test(`${route} should return 401 Unauthorized if no cookie`, async () => {
const response = await request(app)[method](endpoint).use(utils.prefix(REST_PATH_SEGMENT)); const { statusCode } = await authlessAgent[method](endpoint);
expect(statusCode).toBe(401);
expect(response.statusCode).toBe(401);
}); });
}); });
@ -41,10 +35,8 @@ ROUTES_REQUIRING_AUTHORIZATION.forEach(async (route) => {
const [method, endpoint] = getMethodAndEndpoint(route); const [method, endpoint] = getMethodAndEndpoint(route);
test(`${route} should return 403 Forbidden for member`, async () => { test(`${route} should return 403 Forbidden for member`, async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole }); const { statusCode } = await authMemberAgent[method](endpoint);
const response = await authAgent(member)[method](endpoint); expect(statusCode).toBe(403);
expect(response.statusCode).toBe(403);
}); });
}); });

View file

@ -1,43 +1,48 @@
import express from 'express'; import type { SuperAgentTest } from 'supertest';
import { UserSettings } from 'n8n-core';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { UserSettings } from 'n8n-core';
import type { IUser } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import type { CredentialWithSharings } from '@/credentials/credentials.types'; import type { CredentialWithSharings } from '@/credentials/credentials.types';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { randomCredentialPayload } from './shared/random'; import { randomCredentialPayload } from './shared/random';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { AuthAgent, SaveCredentialFunction } from './shared/types'; import type { AuthAgent, SaveCredentialFunction } from './shared/types';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import type { IUser } from 'n8n-workflow';
let app: express.Application;
let globalOwnerRole: Role;
let globalMemberRole: Role; let globalMemberRole: Role;
let credentialOwnerRole: Role; let owner: User;
let saveCredential: SaveCredentialFunction; let member: User;
let authOwnerAgent: SuperAgentTest;
let authAgent: AuthAgent; let authAgent: AuthAgent;
let saveCredential: SaveCredentialFunction;
let sharingSpy: jest.SpyInstance<boolean>; let sharingSpy: jest.SpyInstance<boolean>;
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['credentials'] }); const app = await utils.initTestServer({ endpointGroups: ['credentials'] });
utils.initConfigFile(); utils.initConfigFile();
globalOwnerRole = await testDb.getGlobalOwnerRole(); const globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole(); 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); saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole);
authAgent = utils.createAuthAgent(app);
sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['User', 'SharedCredentials', 'Credentials']); await testDb.truncate(['SharedCredentials', 'Credentials']);
}); });
afterAll(async () => { afterAll(async () => {
@ -47,490 +52,452 @@ afterAll(async () => {
// ---------------------------------------- // ----------------------------------------
// dynamic router switching // dynamic router switching
// ---------------------------------------- // ----------------------------------------
describe('router should switch based on flag', () => {
let savedCredentialId: string;
test('router should switch based on flag', async () => { beforeEach(async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const member = await testDb.createUser({ globalRole: globalMemberRole }); savedCredentialId = savedCredential.id;
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); });
// free router test('when sharing is disabled', async () => {
sharingSpy.mockReturnValueOnce(false); sharingSpy.mockReturnValueOnce(false);
const freeShareResponse = authAgent(owner) await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`) .put(`/credentials/${savedCredentialId}/share`)
.send({ shareWithIds: [member.id] }); .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([ test('when sharing is enabled', async () => {
freeShareResponse, await authOwnerAgent
freeGetResponse, .put(`/credentials/${savedCredentialId}/share`)
]); .send({ shareWithIds: [member.id] })
.expect(200);
expect(freeShareStatus).toBe(404); await authOwnerAgent.get(`/credentials/${savedCredentialId}`).send().expect(200);
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);
}); });
// ---------------------------------------- // ----------------------------------------
// GET /credentials - fetch all credentials // GET /credentials - fetch all credentials
// ---------------------------------------- // ----------------------------------------
describe('GET /credentials', () => {
test('GET /credentials should return all creds for owner', async () => { test('should return all creds for owner', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const [member1, member2, member3] = await testDb.createManyUsers(3, {
const [member1, member2, member3] = await testDb.createManyUsers(3, { globalRole: globalMemberRole,
globalRole: globalMemberRole,
});
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
await saveCredential(randomCredentialPayload(), { user: member1 });
const sharedWith = [member1, member2, member3];
await testDb.shareCredentialWithUsers(savedCredential, sharedWith);
const response = await authAgent(owner).get('/credentials');
expect(response.statusCode).toBe(200);
expect(response.body.data).toHaveLength(2); // owner retrieved owner cred and member cred
const [ownerCredential, memberCredential] = response.body.data as CredentialWithSharings[];
validateMainCredentialData(ownerCredential);
expect(ownerCredential.data).toBeUndefined();
validateMainCredentialData(memberCredential);
expect(memberCredential.data).toBeUndefined();
expect(ownerCredential.ownedBy).toMatchObject({
id: owner.id,
email: owner.email,
firstName: owner.firstName,
lastName: owner.lastName,
});
expect(Array.isArray(ownerCredential.sharedWith)).toBe(true);
expect(ownerCredential.sharedWith).toHaveLength(3);
// Fix order issue (MySQL might return items in any order)
const ownerCredentialsSharedWithOrdered = [...ownerCredential.sharedWith!].sort(
(a: IUser, b: IUser) => (a.email < b.email ? -1 : 1),
);
const orderedSharedWith = [...sharedWith].sort((a, b) => (a.email < b.email ? -1 : 1));
ownerCredentialsSharedWithOrdered.forEach((sharee: IUser, idx: number) => {
expect(sharee).toMatchObject({
id: orderedSharedWith[idx].id,
email: orderedSharedWith[idx].email,
firstName: orderedSharedWith[idx].firstName,
lastName: orderedSharedWith[idx].lastName,
}); });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
await saveCredential(randomCredentialPayload(), { user: member1 });
const sharedWith = [member1, member2, member3];
await testDb.shareCredentialWithUsers(savedCredential, sharedWith);
const response = await authOwnerAgent.get('/credentials');
expect(response.statusCode).toBe(200);
expect(response.body.data).toHaveLength(2); // owner retrieved owner cred and member cred
const [ownerCredential, memberCredential] = response.body.data as CredentialWithSharings[];
validateMainCredentialData(ownerCredential);
expect(ownerCredential.data).toBeUndefined();
validateMainCredentialData(memberCredential);
expect(memberCredential.data).toBeUndefined();
expect(ownerCredential.ownedBy).toMatchObject({
id: owner.id,
email: owner.email,
firstName: owner.firstName,
lastName: owner.lastName,
});
expect(Array.isArray(ownerCredential.sharedWith)).toBe(true);
expect(ownerCredential.sharedWith).toHaveLength(3);
// Fix order issue (MySQL might return items in any order)
const ownerCredentialsSharedWithOrdered = [...ownerCredential.sharedWith!].sort(
(a: IUser, b: IUser) => (a.email < b.email ? -1 : 1),
);
const orderedSharedWith = [...sharedWith].sort((a, b) => (a.email < b.email ? -1 : 1));
ownerCredentialsSharedWithOrdered.forEach((sharee: IUser, idx: number) => {
expect(sharee).toMatchObject({
id: orderedSharedWith[idx].id,
email: orderedSharedWith[idx].email,
firstName: orderedSharedWith[idx].firstName,
lastName: orderedSharedWith[idx].lastName,
});
});
expect(memberCredential.ownedBy).toMatchObject({
id: member1.id,
email: member1.email,
firstName: member1.firstName,
lastName: member1.lastName,
});
expect(Array.isArray(memberCredential.sharedWith)).toBe(true);
expect(memberCredential.sharedWith).toHaveLength(0);
}); });
expect(memberCredential.ownedBy).toMatchObject({ test('should return only relevant creds for member', async () => {
id: member1.id, const [member1, member2] = await testDb.createManyUsers(2, {
email: member1.email, globalRole: globalMemberRole,
firstName: member1.firstName, });
lastName: member1.lastName,
});
expect(Array.isArray(memberCredential.sharedWith)).toBe(true); await saveCredential(randomCredentialPayload(), { user: member2 });
expect(memberCredential.sharedWith).toHaveLength(0); const savedMemberCredential = await saveCredential(randomCredentialPayload(), {
}); user: member1,
});
test('GET /credentials should return only relevant creds for member', async () => { await testDb.shareCredentialWithUsers(savedMemberCredential, [member2]);
const [member1, member2] = await testDb.createManyUsers(2, {
globalRole: globalMemberRole,
});
await saveCredential(randomCredentialPayload(), { user: member2 }); const response = await authAgent(member1).get('/credentials');
const savedMemberCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
await testDb.shareCredentialWithUsers(savedMemberCredential, [member2]); expect(response.statusCode).toBe(200);
expect(response.body.data).toHaveLength(1); // member retrieved only member cred
const response = await authAgent(member1).get('/credentials'); const [member1Credential] = response.body.data;
expect(response.statusCode).toBe(200); validateMainCredentialData(member1Credential);
expect(response.body.data).toHaveLength(1); // member retrieved only member cred expect(member1Credential.data).toBeUndefined();
const [member1Credential] = response.body.data; expect(member1Credential.ownedBy).toMatchObject({
id: member1.id,
email: member1.email,
firstName: member1.firstName,
lastName: member1.lastName,
});
validateMainCredentialData(member1Credential); expect(Array.isArray(member1Credential.sharedWith)).toBe(true);
expect(member1Credential.data).toBeUndefined(); expect(member1Credential.sharedWith).toHaveLength(1);
expect(member1Credential.ownedBy).toMatchObject({ const [sharee] = member1Credential.sharedWith;
id: member1.id,
email: member1.email,
firstName: member1.firstName,
lastName: member1.lastName,
});
expect(Array.isArray(member1Credential.sharedWith)).toBe(true); expect(sharee).toMatchObject({
expect(member1Credential.sharedWith).toHaveLength(1); id: member2.id,
email: member2.email,
const [sharee] = member1Credential.sharedWith; firstName: member2.firstName,
lastName: member2.lastName,
expect(sharee).toMatchObject({ });
id: member2.id,
email: member2.email,
firstName: member2.firstName,
lastName: member2.lastName,
}); });
}); });
// ---------------------------------------- // ----------------------------------------
// GET /credentials/:id - fetch a certain credential // GET /credentials/:id - fetch a certain credential
// ---------------------------------------- // ----------------------------------------
describe('GET /credentials/:id', () => {
test('should retrieve owned cred for owner', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
test('GET /credentials/:id should retrieve owned cred for owner', async () => { const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`);
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = authAgent(ownerShell);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); expect(firstResponse.statusCode).toBe(200);
expect(firstResponse.statusCode).toBe(200); const { data: firstCredential } = firstResponse.body;
validateMainCredentialData(firstCredential);
expect(firstCredential.data).toBeUndefined();
expect(firstCredential.ownedBy).toMatchObject({
id: owner.id,
email: owner.email,
firstName: owner.firstName,
lastName: owner.lastName,
});
expect(firstCredential.sharedWith).toHaveLength(0);
const { data: firstCredential } = firstResponse.body; const secondResponse = await authOwnerAgent
validateMainCredentialData(firstCredential); .get(`/credentials/${savedCredential.id}`)
expect(firstCredential.data).toBeUndefined(); .query({ includeData: true });
expect(firstCredential.ownedBy).toMatchObject({
id: ownerShell.id,
email: ownerShell.email,
firstName: ownerShell.firstName,
lastName: ownerShell.lastName,
});
expect(firstCredential.sharedWith).toHaveLength(0);
const secondResponse = await authOwnerAgent expect(secondResponse.statusCode).toBe(200);
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
expect(secondResponse.statusCode).toBe(200); const { data: secondCredential } = secondResponse.body;
validateMainCredentialData(secondCredential);
const { data: secondCredential } = secondResponse.body; expect(secondCredential.data).toBeDefined();
validateMainCredentialData(secondCredential);
expect(secondCredential.data).toBeDefined();
});
test('GET /credentials/:id should retrieve non-owned cred for owner', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = authAgent(owner);
const [member1, member2] = await testDb.createManyUsers(2, {
globalRole: globalMemberRole,
}); });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); test('should retrieve non-owned cred for owner', async () => {
await testDb.shareCredentialWithUsers(savedCredential, [member2]); const [member1, member2] = await testDb.createManyUsers(2, {
globalRole: globalMemberRole,
});
const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
await testDb.shareCredentialWithUsers(savedCredential, [member2]);
expect(response1.statusCode).toBe(200); const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`);
validateMainCredentialData(response1.body.data); expect(response1.statusCode).toBe(200);
expect(response1.body.data.data).toBeUndefined();
expect(response1.body.data.ownedBy).toMatchObject({ validateMainCredentialData(response1.body.data);
id: member1.id, expect(response1.body.data.data).toBeUndefined();
email: member1.email, expect(response1.body.data.ownedBy).toMatchObject({
firstName: member1.firstName, id: member1.id,
lastName: member1.lastName, email: member1.email,
}); firstName: member1.firstName,
expect(response1.body.data.sharedWith).toHaveLength(1); lastName: member1.lastName,
expect(response1.body.data.sharedWith[0]).toMatchObject({ });
id: member2.id, expect(response1.body.data.sharedWith).toHaveLength(1);
email: member2.email, expect(response1.body.data.sharedWith[0]).toMatchObject({
firstName: member2.firstName, id: member2.id,
lastName: member2.lastName, email: member2.email,
firstName: member2.firstName,
lastName: member2.lastName,
});
const response2 = await authOwnerAgent
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
expect(response2.statusCode).toBe(200);
validateMainCredentialData(response2.body.data);
expect(response2.body.data.data).toBeUndefined();
expect(response2.body.data.sharedWith).toHaveLength(1);
}); });
const response2 = await authOwnerAgent test('should retrieve owned cred for member', async () => {
.get(`/credentials/${savedCredential.id}`) const [member1, member2, member3] = await testDb.createManyUsers(3, {
.query({ includeData: true }); globalRole: globalMemberRole,
});
const authMemberAgent = authAgent(member1);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
await testDb.shareCredentialWithUsers(savedCredential, [member2, member3]);
expect(response2.statusCode).toBe(200); const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`);
validateMainCredentialData(response2.body.data); expect(firstResponse.statusCode).toBe(200);
expect(response2.body.data.data).toBeUndefined();
expect(response2.body.data.sharedWith).toHaveLength(1);
});
test('GET /credentials/:id should retrieve owned cred for member', async () => { const { data: firstCredential } = firstResponse.body;
const [member1, member2, member3] = await testDb.createManyUsers(3, { validateMainCredentialData(firstCredential);
globalRole: globalMemberRole, expect(firstCredential.data).toBeUndefined();
}); expect(firstCredential.ownedBy).toMatchObject({
const authMemberAgent = authAgent(member1); id: member1.id,
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); email: member1.email,
await testDb.shareCredentialWithUsers(savedCredential, [member2, member3]); firstName: member1.firstName,
lastName: member1.lastName,
});
expect(firstCredential.sharedWith).toHaveLength(2);
firstCredential.sharedWith.forEach((sharee: IUser, idx: number) => {
expect([member2.id, member3.id]).toContain(sharee.id);
});
const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`); const secondResponse = await authMemberAgent
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
expect(firstResponse.statusCode).toBe(200); expect(secondResponse.statusCode).toBe(200);
const { data: firstCredential } = firstResponse.body; const { data: secondCredential } = secondResponse.body;
validateMainCredentialData(firstCredential); validateMainCredentialData(secondCredential);
expect(firstCredential.data).toBeUndefined(); expect(secondCredential.data).toBeDefined();
expect(firstCredential.ownedBy).toMatchObject({ expect(firstCredential.sharedWith).toHaveLength(2);
id: member1.id,
email: member1.email,
firstName: member1.firstName,
lastName: member1.lastName,
});
expect(firstCredential.sharedWith).toHaveLength(2);
firstCredential.sharedWith.forEach((sharee: IUser, idx: number) => {
expect([member2.id, member3.id]).toContain(sharee.id);
}); });
const secondResponse = await authMemberAgent test('should not retrieve non-owned cred for member', async () => {
.get(`/credentials/${savedCredential.id}`) const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
.query({ includeData: true });
expect(secondResponse.statusCode).toBe(200); const response = await authAgent(member).get(`/credentials/${savedCredential.id}`);
const { data: secondCredential } = secondResponse.body; expect(response.statusCode).toBe(403);
validateMainCredentialData(secondCredential); expect(response.body.data).toBeUndefined(); // owner's cred not returned
expect(secondCredential.data).toBeDefined();
expect(firstCredential.sharedWith).toHaveLength(2);
});
test('GET /credentials/:id should not retrieve non-owned cred for member', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const response = await authAgent(member).get(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(403);
expect(response.body.data).toBeUndefined(); // owner's cred not returned
});
test('GET /credentials/:id should fail with missing encryption key', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authAgent(ownerShell)
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
test('GET /credentials/:id should return 404 if cred not found', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).get('/credentials/789');
expect(response.statusCode).toBe(404);
const responseAbc = await authAgent(ownerShell).get('/credentials/abc');
expect(responseAbc.statusCode).toBe(404);
// because EE router has precedence, check if forwards this route
const responseNew = await authAgent(ownerShell).get('/credentials/new');
expect(responseNew.statusCode).toBe(200);
});
// ----------------------------------------
// indempotent share/unshare
// ----------------------------------------
test('PUT /credentials/:id/share should share the credential with the provided userIds and unshare it for missing ones', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2, member3, member4, member5] = await testDb.createManyUsers(5, {
globalRole: globalMemberRole,
});
const shareWithIds = [member1.id, member2.id, member3.id];
await testDb.shareCredentialWithUsers(savedCredential, [member4, member5]);
const response = await authAgent(owner)
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds });
expect(response.statusCode).toBe(200);
expect(response.body.data).toBeUndefined();
const sharedCredentials = await Db.collections.SharedCredentials.find({
relations: ['role'],
where: { credentialsId: savedCredential.id },
}); });
// check that sharings have been removed/added correctly test('should fail with missing encryption key', async () => {
expect(sharedCredentials.length).toBe(shareWithIds.length + 1); // +1 for the owner const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
sharedCredentials.forEach((sharedCredential) => { const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
if (sharedCredential.userId === owner.id) { mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
expect(sharedCredential.role.name).toBe('owner');
expect(sharedCredential.role.scope).toBe('credential'); const response = await authOwnerAgent
return; .get(`/credentials/${savedCredential.id}`)
} .query({ includeData: true });
expect(shareWithIds).toContain(sharedCredential.userId);
expect(sharedCredential.role.name).toBe('user'); expect(response.statusCode).toBe(500);
expect(sharedCredential.role.scope).toBe('credential');
mock.mockRestore();
});
test('should return 404 if cred not found', async () => {
const response = await authOwnerAgent.get('/credentials/789');
expect(response.statusCode).toBe(404);
const responseAbc = await authOwnerAgent.get('/credentials/abc');
expect(responseAbc.statusCode).toBe(404);
// because EE router has precedence, check if forwards this route
const responseNew = await authOwnerAgent.get('/credentials/new');
expect(responseNew.statusCode).toBe(200);
}); });
}); });
// ---------------------------------------- // ----------------------------------------
// share // idempotent share/unshare
// ---------------------------------------- // ----------------------------------------
describe('PUT /credentials/:id/share', () => {
test('should share the credential with the provided userIds and unshare it for missing ones', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
test('PUT /credentials/:id/share should share the credential with the provided userIds', async () => { const [member1, member2, member3, member4, member5] = await testDb.createManyUsers(5, {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); globalRole: globalMemberRole,
const [member1, member2, member3] = await testDb.createManyUsers(3, { });
globalRole: globalMemberRole, const shareWithIds = [member1.id, member2.id, member3.id];
});
const memberIds = [member1.id, member2.id, member3.id];
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(owner) await testDb.shareCredentialWithUsers(savedCredential, [member4, member5]);
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: memberIds });
expect(response.statusCode).toBe(200); const response = await authOwnerAgent
expect(response.body.data).toBeUndefined();
// check that sharings got correctly set in DB
const sharedCredentials = await Db.collections.SharedCredentials.find({
relations: ['role'],
where: { credentialsId: savedCredential.id, userId: In([...memberIds]) },
});
expect(sharedCredentials.length).toBe(memberIds.length);
sharedCredentials.forEach((sharedCredential) => {
expect(sharedCredential.role.name).toBe('user');
expect(sharedCredential.role.scope).toBe('credential');
});
// check that owner still exists
const ownerSharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['role'],
where: { credentialsId: savedCredential.id, userId: owner.id },
});
expect(ownerSharedCredential.role.name).toBe('owner');
expect(ownerSharedCredential.role.scope).toBe('credential');
});
test('PUT /credentials/:id/share should respond 403 for non-existing credentials', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const member = await testDb.createUser({ globalRole: globalMemberRole });
const response = await authAgent(owner)
.put(`/credentials/1234567/share`)
.send({ shareWithIds: [member.id] });
expect(response.statusCode).toBe(403);
});
test('PUT /credentials/:id/share should respond 403 for non-owned credentials', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const response = await authAgent(owner)
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [member.id] });
expect(response.statusCode).toBe(403);
});
test('PUT /credentials/:id/share should ignore pending sharee', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const memberShell = await testDb.createUserShell(globalMemberRole);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(owner)
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [memberShell.id] });
expect(response.statusCode).toBe(200);
const sharedCredentials = await Db.collections.SharedCredentials.find({
where: { credentialsId: savedCredential.id },
});
expect(sharedCredentials).toHaveLength(1);
expect(sharedCredentials[0].userId).toBe(owner.id);
});
test('PUT /credentials/:id/share should ignore non-existing sharee', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(owner)
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: ['bce38a11-5e45-4d1c-a9ee-36e4a20ab0fc'] });
expect(response.statusCode).toBe(200);
const sharedCredentials = await Db.collections.SharedCredentials.find({
where: { credentialsId: savedCredential.id },
});
expect(sharedCredentials).toHaveLength(1);
expect(sharedCredentials[0].userId).toBe(owner.id);
});
test('PUT /credentials/:id/share should respond 400 if invalid payload is provided', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const responses = await Promise.all([
authAgent(owner).put(`/credentials/${savedCredential.id}/share`).send(),
authAgent(owner)
.put(`/credentials/${savedCredential.id}/share`) .put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [1] }), .send({ shareWithIds });
]);
responses.forEach((response) => expect(response.statusCode).toBe(400)); expect(response.statusCode).toBe(200);
}); expect(response.body.data).toBeUndefined();
// ---------------------------------------- const sharedCredentials = await Db.collections.SharedCredentials.find({
// unshare relations: ['role'],
// ---------------------------------------- where: { credentialsId: savedCredential.id },
});
test('PUT /credentials/:id/share should unshare the credential', async () => { // check that sharings have been removed/added correctly
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); expect(sharedCredentials.length).toBe(shareWithIds.length + 1); // +1 for the owner
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2] = await testDb.createManyUsers(2, { sharedCredentials.forEach((sharedCredential) => {
globalRole: globalMemberRole, if (sharedCredential.userId === owner.id) {
expect(sharedCredential.role.name).toBe('owner');
expect(sharedCredential.role.scope).toBe('credential');
return;
}
expect(shareWithIds).toContain(sharedCredential.userId);
expect(sharedCredential.role.name).toBe('user');
expect(sharedCredential.role.scope).toBe('credential');
});
}); });
await testDb.shareCredentialWithUsers(savedCredential, [member1, member2]); test('should share the credential with the provided userIds', async () => {
const [member1, member2, member3] = await testDb.createManyUsers(3, {
globalRole: globalMemberRole,
});
const memberIds = [member1.id, member2.id, member3.id];
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authAgent(owner) const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`) .put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [] }); .send({ shareWithIds: memberIds });
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body.data).toBeUndefined();
const sharedCredentials = await Db.collections.SharedCredentials.find({ // check that sharings got correctly set in DB
where: { credentialsId: savedCredential.id }, const sharedCredentials = await Db.collections.SharedCredentials.find({
relations: ['role'],
where: { credentialsId: savedCredential.id, userId: In([...memberIds]) },
});
expect(sharedCredentials.length).toBe(memberIds.length);
sharedCredentials.forEach((sharedCredential) => {
expect(sharedCredential.role.name).toBe('user');
expect(sharedCredential.role.scope).toBe('credential');
});
// check that owner still exists
const ownerSharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['role'],
where: { credentialsId: savedCredential.id, userId: owner.id },
});
expect(ownerSharedCredential.role.name).toBe('owner');
expect(ownerSharedCredential.role.scope).toBe('credential');
}); });
expect(sharedCredentials).toHaveLength(1); test('should respond 403 for non-existing credentials', async () => {
expect(sharedCredentials[0].userId).toBe(owner.id); const response = await authOwnerAgent
.put(`/credentials/1234567/share`)
.send({ shareWithIds: [member.id] });
expect(response.statusCode).toBe(403);
});
test('should respond 403 for non-owned credentials', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [member.id] });
expect(response.statusCode).toBe(403);
});
test('should ignore pending sharee', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [memberShell.id] });
expect(response.statusCode).toBe(200);
const sharedCredentials = await Db.collections.SharedCredentials.find({
where: { credentialsId: savedCredential.id },
});
expect(sharedCredentials).toHaveLength(1);
expect(sharedCredentials[0].userId).toBe(owner.id);
});
test('should ignore non-existing sharee', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: ['bce38a11-5e45-4d1c-a9ee-36e4a20ab0fc'] });
expect(response.statusCode).toBe(200);
const sharedCredentials = await Db.collections.SharedCredentials.find({
where: { credentialsId: savedCredential.id },
});
expect(sharedCredentials).toHaveLength(1);
expect(sharedCredentials[0].userId).toBe(owner.id);
});
test('should respond 400 if invalid payload is provided', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const responses = await Promise.all([
authOwnerAgent.put(`/credentials/${savedCredential.id}/share`).send(),
authOwnerAgent.put(`/credentials/${savedCredential.id}/share`).send({ shareWithIds: [1] }),
]);
responses.forEach((response) => expect(response.statusCode).toBe(400));
});
test('should unshare the credential', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2] = await testDb.createManyUsers(2, {
globalRole: globalMemberRole,
});
await testDb.shareCredentialWithUsers(savedCredential, [member1, member2]);
const response = await authOwnerAgent
.put(`/credentials/${savedCredential.id}/share`)
.send({ shareWithIds: [] });
expect(response.statusCode).toBe(200);
const sharedCredentials = await Db.collections.SharedCredentials.find({
where: { credentialsId: savedCredential.id },
});
expect(sharedCredentials).toHaveLength(1);
expect(sharedCredentials[0].userId).toBe(owner.id);
});
}); });
function validateMainCredentialData(credential: CredentialWithSharings) { function validateMainCredentialData(credential: CredentialWithSharings) {

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 { UserSettings } from 'n8n-core';
import * as Db from '@/Db'; import * as Db from '@/Db';
import config from '@/config';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { randomCredentialPayload, randomName, randomString } from './shared/random'; import { randomCredentialPayload, randomName, randomString } from './shared/random';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { SaveCredentialFunction } from './shared/types'; import type { SaveCredentialFunction } from './shared/types';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import config from '@/config';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { AuthAgent } from './shared/types'; import type { AuthAgent } from './shared/types';
// mock that credentialsSharing is not enabled // mock that credentialsSharing is not enabled
const mockIsCredentialsSharingEnabled = jest.spyOn(UserManagementHelpers, 'isSharingEnabled'); const mockIsCredentialsSharingEnabled = jest.spyOn(UserManagementHelpers, 'isSharingEnabled');
mockIsCredentialsSharingEnabled.mockReturnValue(false); mockIsCredentialsSharingEnabled.mockReturnValue(false);
let app: express.Application; let app: Application;
let globalOwnerRole: Role; let globalOwnerRole: Role;
let globalMemberRole: Role; let globalMemberRole: Role;
let owner: User;
let member: User;
let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest;
let saveCredential: SaveCredentialFunction; let saveCredential: SaveCredentialFunction;
let authAgent: AuthAgent; let authAgent: AuthAgent;
@ -33,13 +38,18 @@ beforeAll(async () => {
globalMemberRole = await testDb.getGlobalMemberRole(); globalMemberRole = await testDb.getGlobalMemberRole();
const credentialOwnerRole = await testDb.getCredentialOwnerRole(); const credentialOwnerRole = await testDb.getCredentialOwnerRole();
owner = await testDb.createUser({ globalRole: globalOwnerRole });
member = await testDb.createUser({ globalRole: globalMemberRole });
saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole); saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole);
authAgent = utils.createAuthAgent(app); authAgent = utils.createAuthAgent(app);
authOwnerAgent = authAgent(owner);
authMemberAgent = authAgent(member);
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['User', 'SharedCredentials', 'Credentials']); await testDb.truncate(['SharedCredentials', 'Credentials']);
}); });
afterAll(async () => { afterAll(async () => {
@ -49,526 +59,490 @@ afterAll(async () => {
// ---------------------------------------- // ----------------------------------------
// GET /credentials - fetch all credentials // GET /credentials - fetch all credentials
// ---------------------------------------- // ----------------------------------------
describe('GET /credentials', () => {
test('should return all creds for owner', async () => {
const [{ id: savedOwnerCredentialId }, { id: savedMemberCredentialId }] = await Promise.all([
saveCredential(randomCredentialPayload(), { user: owner }),
saveCredential(randomCredentialPayload(), { user: member }),
]);
test('GET /credentials should return all creds for owner', async () => { const response = await authOwnerAgent.get('/credentials');
const [owner, member] = await Promise.all([
testDb.createUser({ globalRole: globalOwnerRole }),
testDb.createUser({ globalRole: globalMemberRole }),
]);
const [{ id: savedOwnerCredentialId }, { id: savedMemberCredentialId }] = await Promise.all([ expect(response.statusCode).toBe(200);
saveCredential(randomCredentialPayload(), { user: owner }), expect(response.body.data.length).toBe(2); // owner retrieved owner cred and member cred
saveCredential(randomCredentialPayload(), { user: member }),
]);
const response = await authAgent(owner).get('/credentials'); const savedCredentialsIds = [savedOwnerCredentialId, savedMemberCredentialId];
response.body.data.forEach((credential: CredentialsEntity) => {
validateMainCredentialData(credential);
expect(credential.data).toBeUndefined();
expect(savedCredentialsIds).toContain(credential.id);
});
});
expect(response.statusCode).toBe(200); test('should return only own creds for member', async () => {
expect(response.body.data.length).toBe(2); // owner retrieved owner cred and member cred const [member1, member2] = await testDb.createManyUsers(2, {
globalRole: globalMemberRole,
});
const savedCredentialsIds = [savedOwnerCredentialId, savedMemberCredentialId]; const [savedCredential1] = await Promise.all([
response.body.data.forEach((credential: CredentialsEntity) => { saveCredential(randomCredentialPayload(), { user: member1 }),
validateMainCredentialData(credential); saveCredential(randomCredentialPayload(), { user: member2 }),
expect(credential.data).toBeUndefined(); ]);
expect(savedCredentialsIds).toContain(credential.id);
const response = await authAgent(member1).get('/credentials');
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(1); // member retrieved only own cred
const [member1Credential] = response.body.data;
validateMainCredentialData(member1Credential);
expect(member1Credential.data).toBeUndefined();
expect(member1Credential.id).toBe(savedCredential1.id);
}); });
}); });
test('GET /credentials should return only own creds for member', async () => { describe('POST /credentials', () => {
const [member1, member2] = await testDb.createManyUsers(2, { test('should create cred', async () => {
globalRole: globalMemberRole, const payload = randomCredentialPayload();
const response = await authOwnerAgent.post('/credentials').send(payload);
expect(response.statusCode).toBe(200);
const { id, name, type, nodesAccess, data: encryptedData } = response.body.data;
expect(name).toBe(payload.name);
expect(type).toBe(payload.type);
if (!payload.nodesAccess) {
fail('Payload did not contain a nodesAccess array');
}
expect(nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(payload.data);
const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(credential.name).toBe(payload.name);
expect(credential.type).toBe(payload.type);
expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess![0].nodeType);
expect(credential.data).not.toBe(payload.data);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['user', 'credentials'],
where: { credentialsId: credential.id },
});
expect(sharedCredential.user.id).toBe(owner.id);
expect(sharedCredential.credentials.name).toBe(payload.name);
}); });
const [savedCredential1] = await Promise.all([ test('should fail with invalid inputs', async () => {
saveCredential(randomCredentialPayload(), { user: member1 }), await Promise.all(
saveCredential(randomCredentialPayload(), { user: member2 }), INVALID_PAYLOADS.map(async (invalidPayload) => {
]); const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
expect(response.statusCode).toBe(400);
const response = await authAgent(member1).get('/credentials'); }),
);
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(1); // member retrieved only own cred
const [member1Credential] = response.body.data;
validateMainCredentialData(member1Credential);
expect(member1Credential.data).toBeUndefined();
expect(member1Credential.id).toBe(savedCredential1.id);
});
test('POST /credentials should create cred', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const payload = randomCredentialPayload();
const response = await authAgent(ownerShell).post('/credentials').send(payload);
expect(response.statusCode).toBe(200);
const { id, name, type, nodesAccess, data: encryptedData } = response.body.data;
expect(name).toBe(payload.name);
expect(type).toBe(payload.type);
if (!payload.nodesAccess) {
fail('Payload did not contain a nodesAccess array');
}
expect(nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(payload.data);
const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(credential.name).toBe(payload.name);
expect(credential.type).toBe(payload.type);
expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess![0].nodeType);
expect(credential.data).not.toBe(payload.data);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['user', 'credentials'],
where: { credentialsId: credential.id },
}); });
expect(sharedCredential.user.id).toBe(ownerShell.id); test('should fail with missing encryption key', async () => {
expect(sharedCredential.credentials.name).toBe(payload.name); const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
}); mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
test('POST /credentials should fail with invalid inputs', async () => { const response = await authOwnerAgent.post('/credentials').send(randomCredentialPayload());
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = authAgent(ownerShell);
await Promise.all( expect(response.statusCode).toBe(500);
INVALID_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
expect(response.statusCode).toBe(400);
}),
);
});
test('POST /credentials should fail with missing encryption key', async () => { mock.mockRestore();
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).post('/credentials').send(randomCredentialPayload());
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
test('POST /credentials should ignore ID in payload', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = authAgent(ownerShell);
const firstResponse = await authOwnerAgent
.post('/credentials')
.send({ id: '8', ...randomCredentialPayload() });
expect(firstResponse.body.data.id).not.toBe('8');
const secondResponse = await authOwnerAgent
.post('/credentials')
.send({ id: 8, ...randomCredentialPayload() });
expect(secondResponse.body.data.id).not.toBe(8);
});
test('DELETE /credentials/:id should delete owned cred for owner', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const response = await authAgent(ownerShell).delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ data: true });
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('DELETE /credentials/:id should delete non-owned cred for owner', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const response = await authAgent(ownerShell).delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ data: true });
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('DELETE /credentials/:id should delete owned cred for member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const response = await authAgent(member).delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ data: true });
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('DELETE /credentials/:id should not delete non-owned cred for member', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const response = await authAgent(member).delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(404);
const shellCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(shellCredential).toBeDefined(); // not deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeDefined(); // not deleted
});
test('DELETE /credentials/:id should fail if cred not found', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).delete('/credentials/123');
expect(response.statusCode).toBe(404);
});
test('PATCH /credentials/:id should update owned cred for owner', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const patchPayload = randomCredentialPayload();
const response = await authAgent(ownerShell)
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
expect(response.statusCode).toBe(200);
const { id, name, type, nodesAccess, data: encryptedData } = response.body.data;
expect(name).toBe(patchPayload.name);
expect(type).toBe(patchPayload.type);
if (!patchPayload.nodesAccess) {
fail('Payload did not contain a nodesAccess array');
}
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data);
const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentialsId: credential.id },
}); });
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated test('should ignore ID in payload', async () => {
const firstResponse = await authOwnerAgent
.post('/credentials')
.send({ id: '8', ...randomCredentialPayload() });
expect(firstResponse.body.data.id).not.toBe('8');
const secondResponse = await authOwnerAgent
.post('/credentials')
.send({ id: 8, ...randomCredentialPayload() });
expect(secondResponse.body.data.id).not.toBe(8);
});
}); });
test('PATCH /credentials/:id should update non-owned cred for owner', async () => { describe('DELETE /credentials/:id', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should delete owned cred for owner', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const patchPayload = randomCredentialPayload();
const response = await authAgent(ownerShell) const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ data: true });
const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(name).toBe(patchPayload.name); expect(deletedCredential).toBeNull(); // deleted
expect(type).toBe(patchPayload.type);
if (!patchPayload.nodesAccess) { const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
fail('Payload did not contain a nodesAccess array');
}
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data); expect(deletedSharedCredential).toBeNull(); // deleted
const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentialsId: credential.id },
}); });
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated test('should delete non-owned cred for owner', async () => {
}); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
test('PATCH /credentials/:id should update owned cred for member', async () => { const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const patchPayload = randomCredentialPayload();
const response = await authAgent(member) expect(response.statusCode).toBe(200);
.patch(`/credentials/${savedCredential.id}`) expect(response.body).toEqual({ data: true });
.send(patchPayload);
expect(response.statusCode).toBe(200); const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
const { id, name, type, nodesAccess, data: encryptedData } = response.body.data; expect(deletedCredential).toBeNull(); // deleted
expect(name).toBe(patchPayload.name); const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(type).toBe(patchPayload.type);
if (!patchPayload.nodesAccess) { expect(deletedSharedCredential).toBeNull(); // deleted
fail('Payload did not contain a nodesAccess array');
}
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data);
const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentialsId: credential.id },
}); });
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated test('should delete owned cred for member', async () => {
}); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
test('PATCH /credentials/:id should not update non-owned cred for member', async () => { const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const patchPayload = randomCredentialPayload();
const response = await authAgent(member) expect(response.statusCode).toBe(200);
.patch(`/credentials/${savedCredential.id}`) expect(response.body).toEqual({ data: true });
.send(patchPayload);
expect(response.statusCode).toBe(404); const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
const shellCredential = await Db.collections.Credentials.findOneByOrFail({ expect(deletedCredential).toBeNull(); // deleted
id: savedCredential.id,
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
}); });
expect(shellCredential.name).not.toBe(patchPayload.name); // not updated test('should not delete non-owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(404);
const shellCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(shellCredential).toBeDefined(); // not deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeDefined(); // not deleted
});
test('should fail if cred not found', async () => {
const response = await authOwnerAgent.delete('/credentials/123');
expect(response.statusCode).toBe(404);
});
}); });
test('PATCH /credentials/:id should fail with invalid inputs', async () => { describe('PATCH /credentials/:id', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should update owned cred for owner', async () => {
const authOwnerAgent = authAgent(ownerShell); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell }); const patchPayload = randomCredentialPayload();
await Promise.all( const response = await authOwnerAgent
INVALID_PAYLOADS.map(async (invalidPayload) => { .patch(`/credentials/${savedCredential.id}`)
const response = await authOwnerAgent .send(patchPayload);
.patch(`/credentials/${savedCredential.id}`)
.send(invalidPayload);
if (response.statusCode === 500) { expect(response.statusCode).toBe(200);
console.log(response.statusCode, response.body);
const { id, name, type, nodesAccess, data: encryptedData } = response.body.data;
expect(name).toBe(patchPayload.name);
expect(type).toBe(patchPayload.type);
if (!patchPayload.nodesAccess) {
fail('Payload did not contain a nodesAccess array');
}
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data);
const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentialsId: credential.id },
});
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated
});
test('should update non-owned cred for owner', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const patchPayload = randomCredentialPayload();
const response = await authOwnerAgent
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
expect(response.statusCode).toBe(200);
const { id, name, type, nodesAccess, data: encryptedData } = response.body.data;
expect(name).toBe(patchPayload.name);
expect(type).toBe(patchPayload.type);
if (!patchPayload.nodesAccess) {
fail('Payload did not contain a nodesAccess array');
}
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data);
const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentialsId: credential.id },
});
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated
});
test('should update owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const patchPayload = randomCredentialPayload();
const response = await authMemberAgent
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
expect(response.statusCode).toBe(200);
const { id, name, type, nodesAccess, data: encryptedData } = response.body.data;
expect(name).toBe(patchPayload.name);
expect(type).toBe(patchPayload.type);
if (!patchPayload.nodesAccess) {
fail('Payload did not contain a nodesAccess array');
}
expect(nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType);
expect(encryptedData).not.toBe(patchPayload.data);
const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(credential.name).toBe(patchPayload.name);
expect(credential.type).toBe(patchPayload.type);
expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType);
expect(credential.data).not.toBe(patchPayload.data);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { credentialsId: credential.id },
});
expect(sharedCredential.credentials.name).toBe(patchPayload.name); // updated
});
test('should not update non-owned cred for member', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const patchPayload = randomCredentialPayload();
const response = await authMemberAgent
.patch(`/credentials/${savedCredential.id}`)
.send(patchPayload);
expect(response.statusCode).toBe(404);
const shellCredential = await Db.collections.Credentials.findOneByOrFail({
id: savedCredential.id,
});
expect(shellCredential.name).not.toBe(patchPayload.name); // not updated
});
test('should fail with invalid inputs', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
await Promise.all(
INVALID_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent
.patch(`/credentials/${savedCredential.id}`)
.send(invalidPayload);
if (response.statusCode === 500) {
console.log(response.statusCode, response.body);
}
expect(response.statusCode).toBe(400);
}),
);
});
test('should fail if cred not found', async () => {
const response = await authOwnerAgent.patch('/credentials/123').send(randomCredentialPayload());
expect(response.statusCode).toBe(404);
});
test('should fail with missing encryption key', async () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authOwnerAgent.post('/credentials').send(randomCredentialPayload());
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
});
describe('GET /credentials/new', () => {
test('should return default name for new credential or its increment', async () => {
const name = config.getEnv('credentials.defaultName');
let tempName = name;
for (let i = 0; i < 4; i++) {
const response = await authOwnerAgent.get(`/credentials/new?name=${name}`);
expect(response.statusCode).toBe(200);
if (i === 0) {
expect(response.body.data.name).toBe(name);
} else {
tempName = name + ' ' + (i + 1);
expect(response.body.data.name).toBe(tempName);
} }
expect(response.statusCode).toBe(400); await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: owner });
}),
);
});
test('PATCH /credentials/:id should fail if cred not found', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell)
.patch('/credentials/123')
.send(randomCredentialPayload());
expect(response.statusCode).toBe(404);
});
test('PATCH /credentials/:id should fail with missing encryption key', async () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).post('/credentials').send(randomCredentialPayload());
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
test('GET /credentials/new should return default name for new credential or its increment', async () => {
const ownerShell = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = authAgent(ownerShell);
const name = config.getEnv('credentials.defaultName');
let tempName = name;
for (let i = 0; i < 4; i++) {
const response = await authOwnerAgent.get(`/credentials/new?name=${name}`);
expect(response.statusCode).toBe(200);
if (i === 0) {
expect(response.body.data.name).toBe(name);
} else {
tempName = name + ' ' + (i + 1);
expect(response.body.data.name).toBe(tempName);
} }
await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: ownerShell }); });
}
});
test('GET /credentials/new should return name from query for new credential or its increment', async () => { test('should return name from query for new credential or its increment', async () => {
const ownerShell = await testDb.createUser({ globalRole: globalOwnerRole }); const name = 'special credential name';
const authOwnerAgent = authAgent(ownerShell); let tempName = name;
const name = 'special credential name';
let tempName = name;
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const response = await authOwnerAgent.get(`/credentials/new?name=${name}`); const response = await authOwnerAgent.get(`/credentials/new?name=${name}`);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
if (i === 0) { if (i === 0) {
expect(response.body.data.name).toBe(name); expect(response.body.data.name).toBe(name);
} else { } else {
tempName = name + ' ' + (i + 1); tempName = name + ' ' + (i + 1);
expect(response.body.data.name).toBe(tempName); expect(response.body.data.name).toBe(tempName);
}
await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: owner });
} }
await saveCredential({ ...randomCredentialPayload(), name: tempName }, { user: ownerShell }); });
}
}); });
test('GET /credentials/:id should retrieve owned cred for owner', async () => { describe('GET /credentials/:id', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should retrieve owned cred for owner', async () => {
const authOwnerAgent = authAgent(ownerShell); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); const firstResponse = await authOwnerAgent.get(`/credentials/${savedCredential.id}`);
expect(firstResponse.statusCode).toBe(200); expect(firstResponse.statusCode).toBe(200);
validateMainCredentialData(firstResponse.body.data); validateMainCredentialData(firstResponse.body.data);
expect(firstResponse.body.data.data).toBeUndefined(); expect(firstResponse.body.data.data).toBeUndefined();
const secondResponse = await authOwnerAgent const secondResponse = await authOwnerAgent
.get(`/credentials/${savedCredential.id}`) .get(`/credentials/${savedCredential.id}`)
.query({ includeData: true }); .query({ includeData: true });
validateMainCredentialData(secondResponse.body.data); validateMainCredentialData(secondResponse.body.data);
expect(secondResponse.body.data.data).toBeDefined(); expect(secondResponse.body.data.data).toBeDefined();
}); });
test('GET /credentials/:id should retrieve owned cred for member', async () => { test('should retrieve owned cred for member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const authMemberAgent = authAgent(member);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`); const firstResponse = await authMemberAgent.get(`/credentials/${savedCredential.id}`);
expect(firstResponse.statusCode).toBe(200); expect(firstResponse.statusCode).toBe(200);
validateMainCredentialData(firstResponse.body.data); validateMainCredentialData(firstResponse.body.data);
expect(firstResponse.body.data.data).toBeUndefined(); expect(firstResponse.body.data.data).toBeUndefined();
const secondResponse = await authMemberAgent const secondResponse = await authMemberAgent
.get(`/credentials/${savedCredential.id}`) .get(`/credentials/${savedCredential.id}`)
.query({ includeData: true }); .query({ includeData: true });
expect(secondResponse.statusCode).toBe(200); expect(secondResponse.statusCode).toBe(200);
validateMainCredentialData(secondResponse.body.data); validateMainCredentialData(secondResponse.body.data);
expect(secondResponse.body.data.data).toBeDefined(); expect(secondResponse.body.data.data).toBeDefined();
}); });
test('GET /credentials/:id should retrieve non-owned cred for owner', async () => { test('should retrieve non-owned cred for owner', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const authOwnerAgent = authAgent(owner);
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`);
const response1 = await authOwnerAgent.get(`/credentials/${savedCredential.id}`); expect(response1.statusCode).toBe(200);
expect(response1.statusCode).toBe(200); validateMainCredentialData(response1.body.data);
expect(response1.body.data.data).toBeUndefined();
validateMainCredentialData(response1.body.data); const response2 = await authOwnerAgent
expect(response1.body.data.data).toBeUndefined(); .get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
const response2 = await authOwnerAgent expect(response2.statusCode).toBe(200);
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
expect(response2.statusCode).toBe(200); validateMainCredentialData(response2.body.data);
expect(response2.body.data.data).toBeDefined();
});
validateMainCredentialData(response2.body.data); test('should not retrieve non-owned cred for member', async () => {
expect(response2.body.data.data).toBeDefined(); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
});
test('GET /credentials/:id should not retrieve non-owned cred for member', async () => { const response = await authMemberAgent.get(`/credentials/${savedCredential.id}`);
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const response = await authAgent(member).get(`/credentials/${savedCredential.id}`); expect(response.statusCode).toBe(404);
expect(response.body.data).toBeUndefined(); // owner's cred not returned
});
expect(response.statusCode).toBe(404); test('should fail with missing encryption key', async () => {
expect(response.body.data).toBeUndefined(); // owner's cred not returned const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
});
test('GET /credentials/:id should fail with missing encryption key', async () => { const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
const ownerShell = await testDb.createUserShell(globalOwnerRole); mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const savedCredential = await saveCredential(randomCredentialPayload(), { user: ownerShell });
const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); const response = await authOwnerAgent
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); .get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
const response = await authAgent(ownerShell) expect(response.statusCode).toBe(500);
.get(`/credentials/${savedCredential.id}`)
.query({ includeData: true });
expect(response.statusCode).toBe(500); mock.mockRestore();
});
mock.mockRestore(); test('should return 404 if cred not found', async () => {
}); const response = await authOwnerAgent.get('/credentials/789');
expect(response.statusCode).toBe(404);
test('GET /credentials/:id should return 404 if cred not found', async () => { const responseAbc = await authOwnerAgent.get('/credentials/abc');
const ownerShell = await testDb.createUserShell(globalOwnerRole); expect(responseAbc.statusCode).toBe(404);
const response = await authAgent(ownerShell).get('/credentials/789'); });
expect(response.statusCode).toBe(404);
const responseAbc = await authAgent(ownerShell).get('/credentials/abc');
expect(responseAbc.statusCode).toBe(404);
}); });
function validateMainCredentialData(credential: CredentialsEntity) { function validateMainCredentialData(credential: CredentialsEntity) {

View file

@ -2,6 +2,8 @@ import express from 'express';
import config from '@/config'; import config from '@/config';
import axios from 'axios'; import axios from 'axios';
import syslog from 'syslog-client'; import syslog from 'syslog-client';
import { v4 as uuid } from 'uuid';
import type { SuperAgentTest } from 'supertest';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import { Role } from '@db/entities/Role'; import { Role } from '@db/entities/Role';
@ -15,14 +17,12 @@ import {
MessageEventBusDestinationWebhookOptions, MessageEventBusDestinationWebhookOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { eventBus } from '@/eventbus'; import { eventBus } from '@/eventbus';
import { SuperAgentTest } from 'supertest';
import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric'; import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric';
import { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; import { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee';
import { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; import { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee';
import { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee'; import { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee';
import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAudit'; import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAudit';
import { v4 as uuid } from 'uuid'; import { EventNamesTypes } from '@/eventbus/EventMessageClasses';
import { EventNamesTypes } from '../../src/eventbus/EventMessageClasses';
jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); jest.unmock('@/eventbus/MessageEventBus/MessageEventBus');
jest.mock('axios'); jest.mock('axios');
@ -54,6 +54,7 @@ const testWebhookDestination: MessageEventBusDestinationWebhookOptions = {
enabled: false, enabled: false,
subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'], subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'],
}; };
const testSentryDestination: MessageEventBusDestinationSentryOptions = { const testSentryDestination: MessageEventBusDestinationSentryOptions = {
...defaultMessageEventBusDestinationSentryOptions, ...defaultMessageEventBusDestinationSentryOptions,
id: '450ca04b-87dd-4837-a052-ab3a347a00e9', id: '450ca04b-87dd-4837-a052-ab3a347a00e9',
@ -101,13 +102,10 @@ beforeAll(async () => {
config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter'); config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter');
config.set('eventBus.logWriter.keepLogCount', '1'); config.set('eventBus.logWriter.keepLogCount', '1');
config.set('enterprise.features.logStreaming', true); config.set('enterprise.features.logStreaming', true);
await eventBus.initialize();
});
beforeEach(async () => {
config.set('userManagement.disabled', false); config.set('userManagement.disabled', false);
config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.isInstanceOwnerSetUp', true);
await eventBus.initialize();
}); });
afterAll(async () => { afterAll(async () => {

View file

@ -1,11 +1,12 @@
import express from 'express'; import express from 'express';
import type { Entry as LdapUser } from 'ldapts'; import type { Entry as LdapUser } from 'ldapts';
import { Not } from 'typeorm';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User'; 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 { LdapManager } from '@/Ldap/LdapManager.ee';
import { LdapService } from '@/Ldap/LdapService.ee'; import { LdapService } from '@/Ldap/LdapService.ee';
import { encryptPassword, saveLdapSynchronization } from '@/Ldap/helpers'; import { encryptPassword, saveLdapSynchronization } from '@/Ldap/helpers';
@ -21,7 +22,6 @@ jest.mock('@/UserManagement/email/NodeMailer');
let app: express.Application; let app: express.Application;
let globalMemberRole: Role; let globalMemberRole: Role;
let globalOwnerRole: Role;
let owner: User; let owner: User;
let authAgent: AuthAgent; let authAgent: AuthAgent;
@ -42,11 +42,12 @@ const defaultLdapConfig = {
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['auth', 'ldap'] }); app = await utils.initTestServer({ endpointGroups: ['auth', 'ldap'] });
const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles(); const [globalOwnerRole, fetchedGlobalMemberRole] = await testDb.getAllRoles();
globalOwnerRole = fetchedGlobalOwnerRole;
globalMemberRole = fetchedGlobalMemberRole; globalMemberRole = fetchedGlobalMemberRole;
owner = await testDb.createUser({ globalRole: globalOwnerRole });
authAgent = utils.createAuthAgent(app); authAgent = utils.createAuthAgent(app);
defaultLdapConfig.bindingAdminPassword = await encryptPassword( defaultLdapConfig.bindingAdminPassword = await encryptPassword(
@ -64,10 +65,9 @@ beforeEach(async () => {
'Credentials', 'Credentials',
'SharedWorkflow', 'SharedWorkflow',
'Workflow', 'Workflow',
'User',
]); ]);
owner = await testDb.createUser({ globalRole: globalOwnerRole }); await Db.collections.User.delete({ id: Not(owner.id) });
jest.mock('@/telemetry'); jest.mock('@/telemetry');

View file

@ -1,41 +1,36 @@
import express from 'express'; import type { SuperAgentTest } from 'supertest';
import config from '@/config'; import config from '@/config';
import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User';
import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils';
import { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; import { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
import { License } from '@/License'; 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_SERVER_URL = 'https://server.com/v1';
const MOCK_RENEW_OFFSET = 259200; const MOCK_RENEW_OFFSET = 259200;
const MOCK_INSTANCE_ID = 'instance-id';
let app: express.Application; let owner: User;
let globalOwnerRole: Role; let member: User;
let globalMemberRole: Role; let authOwnerAgent: SuperAgentTest;
let authAgent: AuthAgent; let authMemberAgent: SuperAgentTest;
let license: License;
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['license'] }); const app = await utils.initTestServer({ endpointGroups: ['license'] });
globalOwnerRole = await testDb.getGlobalOwnerRole(); const globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole(); 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.serverUrl', MOCK_SERVER_URL);
config.set('license.autoRenewEnabled', true); config.set('license.autoRenewEnabled', true);
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET); config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
}); });
beforeEach(async () => {
license = new License();
await license.init(MOCK_INSTANCE_ID);
});
afterEach(async () => { afterEach(async () => {
await testDb.truncate(['Settings']); await testDb.truncate(['Settings']);
}); });
@ -44,98 +39,66 @@ afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('GET /license should return license information to the instance owner', async () => { describe('GET /license', () => {
const userShell = await testDb.createUserShell(globalOwnerRole); test('should return license information to the instance owner', async () => {
// No license defined so we just expect the result to be the defaults
const response = await authAgent(userShell).get('/license'); await authOwnerAgent.get('/license').expect(200, DEFAULT_LICENSE_RESPONSE);
expect(response.statusCode).toBe(200);
// No license defined so we just expect the result to be the defaults
expect(response.body).toStrictEqual(DEFAULT_LICENSE_RESPONSE);
});
test('GET /license should return license information to a regular user', async () => {
const userShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(userShell).get('/license');
expect(response.statusCode).toBe(200);
// No license defined so we just expect the result to be the defaults
expect(response.body).toStrictEqual(DEFAULT_LICENSE_RESPONSE);
});
test('POST /license/activate should work for instance owner', async () => {
const userShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(userShell)
.post('/license/activate')
.send({ activationKey: 'abcde' });
expect(response.statusCode).toBe(200);
// No license defined so we just expect the result to be the defaults
expect(response.body).toMatchObject(DEFAULT_POST_RESPONSE);
});
test('POST /license/activate does not work for regular users', async () => {
const userShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(userShell)
.post('/license/activate')
.send({ activationKey: 'abcde' });
expect(response.statusCode).toBe(403);
expect(response.body.message).toBe(NON_OWNER_ACTIVATE_RENEW_MESSAGE);
});
test('POST /license/activate errors out properly', async () => {
License.prototype.activate = jest.fn().mockImplementation(() => {
throw new Error(INVALID_ACIVATION_KEY_MESSAGE);
}); });
const userShell = await testDb.createUserShell(globalOwnerRole); test('should return license information to a regular user', async () => {
// No license defined so we just expect the result to be the defaults
const response = await authAgent(userShell) await authMemberAgent.get('/license').expect(200, DEFAULT_LICENSE_RESPONSE);
.post('/license/activate') });
.send({ activationKey: 'abcde' });
expect(response.statusCode).toBe(400);
expect(response.body.message).toBe(INVALID_ACIVATION_KEY_MESSAGE);
}); });
test('POST /license/renew should work for instance owner', async () => { describe('POST /license/activate', () => {
const userShell = await testDb.createUserShell(globalOwnerRole); test('should work for instance owner', async () => {
await authOwnerAgent
const response = await authAgent(userShell).post('/license/renew'); .post('/license/activate')
.send({ activationKey: 'abcde' })
expect(response.statusCode).toBe(200); .expect(200, DEFAULT_POST_RESPONSE);
// No license defined so we just expect the result to be the defaults
expect(response.body).toMatchObject(DEFAULT_POST_RESPONSE);
});
test('POST /license/renew does not work for regular users', async () => {
const userShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(userShell).post('/license/renew');
expect(response.statusCode).toBe(403);
expect(response.body.message).toBe(NON_OWNER_ACTIVATE_RENEW_MESSAGE);
});
test('POST /license/renew errors out properly', async () => {
License.prototype.renew = jest.fn().mockImplementation(() => {
throw new Error(RENEW_ERROR_MESSAGE);
}); });
const userShell = await testDb.createUserShell(globalOwnerRole); test('does not work for regular users', async () => {
await authMemberAgent
.post('/license/activate')
.send({ activationKey: 'abcde' })
.expect(403, { code: 403, message: NON_OWNER_ACTIVATE_RENEW_MESSAGE });
});
const response = await authAgent(userShell).post('/license/renew'); test('errors out properly', async () => {
License.prototype.activate = jest.fn().mockImplementation(() => {
throw new Error(INVALID_ACIVATION_KEY_MESSAGE);
});
expect(response.statusCode).toBe(400); await authOwnerAgent
expect(response.body.message).toBe(RENEW_ERROR_MESSAGE); .post('/license/activate')
.send({ activationKey: 'abcde' })
.expect(400, { code: 400, message: INVALID_ACIVATION_KEY_MESSAGE });
});
});
describe('POST /license/renew', () => {
test('should work for instance owner', async () => {
// No license defined so we just expect the result to be the defaults
await authOwnerAgent.post('/license/renew').expect(200, DEFAULT_POST_RESPONSE);
});
test('does not work for regular users', async () => {
await authMemberAgent
.post('/license/renew')
.expect(403, { code: 403, message: NON_OWNER_ACTIVATE_RENEW_MESSAGE });
});
test('errors out properly', async () => {
License.prototype.renew = jest.fn().mockImplementation(() => {
throw new Error(RENEW_ERROR_MESSAGE);
});
await authOwnerAgent
.post('/license/renew')
.expect(400, { code: 400, message: RENEW_ERROR_MESSAGE });
});
}); });
const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = { 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 { IsNull } from 'typeorm';
import validator from 'validator'; import validator from 'validator';
import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import { import {
randomApiKey, randomApiKey,
@ -17,7 +17,7 @@ import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types'; import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
let app: express.Application; let app: Application;
let globalOwnerRole: Role; let globalOwnerRole: Role;
let globalMemberRole: Role; let globalMemberRole: Role;
let authAgent: AuthAgent; let authAgent: AuthAgent;
@ -40,10 +40,16 @@ afterAll(async () => {
}); });
describe('Owner shell', () => { describe('Owner shell', () => {
test('PATCH /me should succeed with valid inputs', async () => { let ownerShell: User;
const ownerShell = await testDb.createUserShell(globalOwnerRole); let authOwnerShellAgent: SuperAgentTest;
const authOwnerShellAgent = authAgent(ownerShell);
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) { for (const validPayload of VALID_PATCH_ME_PAYLOADS) {
const response = await authOwnerShellAgent.patch('/me').send(validPayload); const response = await authOwnerShellAgent.patch('/me').send(validPayload);
@ -83,9 +89,6 @@ describe('Owner shell', () => {
}); });
test('PATCH /me should fail with invalid inputs', async () => { 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) { for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) {
const response = await authOwnerShellAgent.patch('/me').send(invalidPayload); const response = await authOwnerShellAgent.patch('/me').send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
@ -98,9 +101,6 @@ describe('Owner shell', () => {
}); });
test('PATCH /me/password should fail for shell', async () => { test('PATCH /me/password should fail for shell', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = authAgent(ownerShell);
const validPasswordPayload = { const validPasswordPayload = {
currentPassword: randomValidPassword(), currentPassword: randomValidPassword(),
newPassword: randomValidPassword(), newPassword: randomValidPassword(),
@ -130,9 +130,6 @@ describe('Owner shell', () => {
}); });
test('POST /me/survey should succeed with valid inputs', async () => { test('POST /me/survey should succeed with valid inputs', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = authAgent(ownerShell);
const validPayloads = [SURVEY, {}]; const validPayloads = [SURVEY, {}];
for (const validPayload of validPayloads) { for (const validPayload of validPayloads) {
@ -150,9 +147,7 @@ describe('Owner shell', () => {
}); });
test('POST /me/api-key should create an api key', async () => { test('POST /me/api-key should create an api key', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); const response = await authOwnerShellAgent.post('/me/api-key');
const response = await authAgent(ownerShell).post('/me/api-key');
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).toBeDefined(); expect(response.body.data.apiKey).toBeDefined();
@ -166,20 +161,14 @@ describe('Owner shell', () => {
}); });
test('GET /me/api-key should fetch the api key', async () => { test('GET /me/api-key should fetch the api key', async () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole); const response = await authOwnerShellAgent.get('/me/api-key');
ownerShell = await testDb.addApiKey(ownerShell);
const response = await authAgent(ownerShell).get('/me/api-key');
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).toEqual(ownerShell.apiKey); expect(response.body.data.apiKey).toEqual(ownerShell.apiKey);
}); });
test('DELETE /me/api-key should fetch the api key', async () => { test('DELETE /me/api-key should fetch the api key', async () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole); const response = await authOwnerShellAgent.delete('/me/api-key');
ownerShell = await testDb.addApiKey(ownerShell);
const response = await authAgent(ownerShell).delete('/me/api-key');
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
@ -192,19 +181,22 @@ describe('Owner shell', () => {
}); });
describe('Member', () => { describe('Member', () => {
beforeEach(async () => { const memberPassword = randomValidPassword();
config.set('userManagement.isInstanceOwnerSetUp', true); let member: User;
let authMemberAgent: SuperAgentTest;
await Db.collections.Settings.update( beforeEach(async () => {
{ key: 'userManagement.isInstanceOwnerSetUp' }, member = await testDb.createUser({
{ value: JSON.stringify(true) }, password: memberPassword,
); globalRole: globalMemberRole,
apiKey: randomApiKey(),
});
authMemberAgent = authAgent(member);
await utils.setInstanceOwnerSetUp(true);
}); });
test('PATCH /me should succeed with valid inputs', async () => { 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) { for (const validPayload of VALID_PATCH_ME_PAYLOADS) {
const response = await authMemberAgent.patch('/me').send(validPayload); const response = await authMemberAgent.patch('/me').send(validPayload);
@ -244,9 +236,6 @@ describe('Member', () => {
}); });
test('PATCH /me should fail with invalid inputs', async () => { test('PATCH /me should fail with invalid inputs', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = authAgent(member);
for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) {
const response = await authMemberAgent.patch('/me').send(invalidPayload); const response = await authMemberAgent.patch('/me').send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
@ -259,18 +248,12 @@ describe('Member', () => {
}); });
test('PATCH /me/password should succeed with valid inputs', async () => { test('PATCH /me/password should succeed with valid inputs', async () => {
const memberPassword = randomValidPassword();
const member = await testDb.createUser({
password: memberPassword,
globalRole: globalMemberRole,
});
const validPayload = { const validPayload = {
currentPassword: memberPassword, currentPassword: memberPassword,
newPassword: randomValidPassword(), 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.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
@ -280,9 +263,6 @@ describe('Member', () => {
}); });
test('PATCH /me/password should fail with invalid inputs', async () => { test('PATCH /me/password should fail with invalid inputs', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = authAgent(member);
for (const payload of INVALID_PASSWORD_PAYLOADS) { for (const payload of INVALID_PASSWORD_PAYLOADS) {
const response = await authMemberAgent.patch('/me/password').send(payload); const response = await authMemberAgent.patch('/me/password').send(payload);
expect([400, 500].includes(response.statusCode)).toBe(true); expect([400, 500].includes(response.statusCode)).toBe(true);
@ -299,9 +279,6 @@ describe('Member', () => {
}); });
test('POST /me/survey should succeed with valid inputs', async () => { test('POST /me/survey should succeed with valid inputs', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = authAgent(member);
const validPayloads = [SURVEY, {}]; const validPayloads = [SURVEY, {}];
for (const validPayload of validPayloads) { for (const validPayload of validPayloads) {
@ -318,11 +295,6 @@ describe('Member', () => {
}); });
test('POST /me/api-key should create an api key', async () => { 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'); const response = await authAgent(member).post('/me/api-key');
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
@ -335,11 +307,6 @@ describe('Member', () => {
}); });
test('GET /me/api-key should fetch the api key', async () => { 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'); const response = await authAgent(member).get('/me/api-key');
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
@ -347,11 +314,6 @@ describe('Member', () => {
}); });
test('DELETE /me/api-key should fetch the api key', async () => { 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'); const response = await authAgent(member).delete('/me/api-key');
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
@ -364,7 +326,7 @@ describe('Member', () => {
describe('Owner', () => { describe('Owner', () => {
beforeEach(async () => { beforeEach(async () => {
config.set('userManagement.isInstanceOwnerSetUp', true); await utils.setInstanceOwnerSetUp(true);
}); });
test('PATCH /me should succeed with valid inputs', async () => { test('PATCH /me should succeed with valid inputs', async () => {

View file

@ -1,10 +1,6 @@
import path from 'path'; import path from 'path';
import express from 'express';
import { mocked } from 'jest-mock'; import { mocked } from 'jest-mock';
import type { SuperAgentTest } from 'supertest';
import * as utils from './shared/utils';
import * as testDb from './shared/testDb';
import { import {
executeCommand, executeCommand,
checkNpmPackageStatus, checkNpmPackageStatus,
@ -15,13 +11,13 @@ import {
import { findInstalledPackage, isPackageInstalled } from '@/CommunityNodes/packageModel'; import { findInstalledPackage, isPackageInstalled } from '@/CommunityNodes/packageModel';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { InstalledPackages } from '@db/entities/InstalledPackages'; import { InstalledPackages } from '@db/entities/InstalledPackages';
import type { User } from '@db/entities/User';
import type { Role } from '@db/entities/Role';
import type { AuthAgent } from './shared/types';
import type { InstalledNodes } from '@db/entities/InstalledNodes'; import type { InstalledNodes } from '@db/entities/InstalledNodes';
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { Push } from '@/push'; 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); const mockLoadNodesAndCredentials = utils.mockInstance(LoadNodesAndCredentials);
utils.mockInstance(NodeTypes); utils.mockInstance(NodeTypes);
@ -48,22 +44,21 @@ jest.mock('@/CommunityNodes/packageModel', () => {
const mockedEmptyPackage = mocked(utils.emptyPackage); const mockedEmptyPackage = mocked(utils.emptyPackage);
let app: express.Application; let ownerShell: User;
let globalOwnerRole: Role; let authOwnerShellAgent: SuperAgentTest;
let authAgent: AuthAgent;
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['nodes'] }); const app = await utils.initTestServer({ endpointGroups: ['nodes'] });
globalOwnerRole = await testDb.getGlobalOwnerRole(); const globalOwnerRole = await testDb.getGlobalOwnerRole();
ownerShell = await testDb.createUserShell(globalOwnerRole);
authAgent = utils.createAuthAgent(app); authOwnerShellAgent = utils.createAuthAgent(app)(ownerShell);
utils.initConfigFile(); utils.initConfigFile();
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['InstalledNodes', 'InstalledPackages', 'User']); await testDb.truncate(['InstalledNodes', 'InstalledPackages']);
mocked(executeCommand).mockReset(); mocked(executeCommand).mockReset();
mocked(findInstalledPackage).mockReset(); mocked(findInstalledPackage).mockReset();
@ -73,255 +68,216 @@ afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
/** describe('GET /nodes', () => {
* GET /nodes test('should respond 200 if no nodes are installed', async () => {
*/ const {
statusCode,
body: { data },
} = await authOwnerShellAgent.get('/nodes');
test('GET /nodes should respond 200 if no nodes are installed', async () => { expect(statusCode).toBe(200);
const ownerShell = await testDb.createUserShell(globalOwnerRole); expect(data).toHaveLength(0);
});
const { test('should return list of one installed package and node', async () => {
statusCode, const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
body: { data }, await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
} = await authAgent(ownerShell).get('/nodes');
expect(statusCode).toBe(200); const {
expect(data).toHaveLength(0); statusCode,
}); body: { data },
} = await authOwnerShellAgent.get('/nodes');
test('GET /nodes should return list of one installed package and node', async () => { expect(statusCode).toBe(200);
const ownerShell = await testDb.createUserShell(globalOwnerRole); expect(data).toHaveLength(1);
expect(data[0].installedNodes).toHaveLength(1);
});
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); test('should return list of multiple installed packages and nodes', async () => {
await testDb.saveInstalledNode(utils.installedNodePayload(packageName)); const first = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(first.packageName));
const { const second = await testDb.saveInstalledPackage(utils.installedPackagePayload());
statusCode, await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
body: { data }, await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
} = await authAgent(ownerShell).get('/nodes');
expect(statusCode).toBe(200); const {
expect(data).toHaveLength(1); statusCode,
expect(data[0].installedNodes).toHaveLength(1); body: { data },
}); } = await authOwnerShellAgent.get('/nodes');
test('GET /nodes should return list of multiple installed packages and nodes', async () => { expect(statusCode).toBe(200);
const ownerShell = await testDb.createUserShell(globalOwnerRole); expect(data).toHaveLength(2);
const first = await testDb.saveInstalledPackage(utils.installedPackagePayload()); const allNodes = data.reduce(
await testDb.saveInstalledNode(utils.installedNodePayload(first.packageName)); (acc: InstalledNodes[], cur: InstalledPackages) => acc.concat(cur.installedNodes),
[],
);
const second = await testDb.saveInstalledPackage(utils.installedPackagePayload()); expect(allNodes).toHaveLength(3);
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName)); });
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
const { test('should not check for updates if no packages installed', async () => {
statusCode, await authOwnerShellAgent.get('/nodes');
body: { data },
} = await authAgent(ownerShell).get('/nodes');
expect(statusCode).toBe(200); expect(mocked(executeCommand)).toHaveBeenCalledTimes(0);
expect(data).toHaveLength(2); });
const allNodes = data.reduce( test('should check for updates if packages installed', async () => {
(acc: InstalledNodes[], cur: InstalledPackages) => acc.concat(cur.installedNodes), const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
[], await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
);
expect(allNodes).toHaveLength(3); await authOwnerShellAgent.get('/nodes');
});
test('GET /nodes should not check for updates if no packages installed', async () => { expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', {
const ownerShell = await testDb.createUserShell(globalOwnerRole); doNotHandleError: true,
});
});
await authAgent(ownerShell).get('/nodes'); test('should report package updates if available', async () => {
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
expect(mocked(executeCommand)).toHaveBeenCalledTimes(0); mocked(executeCommand).mockImplementationOnce(() => {
}); throw {
code: 1,
stdout: JSON.stringify({
[packageName]: {
current: COMMUNITY_PACKAGE_VERSION.CURRENT,
wanted: COMMUNITY_PACKAGE_VERSION.CURRENT,
latest: COMMUNITY_PACKAGE_VERSION.UPDATED,
location: path.join('node_modules', packageName),
},
}),
};
});
test('GET /nodes should check for updates if packages installed', async () => { mocked(isNpmError).mockReturnValueOnce(true);
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); const {
await testDb.saveInstalledNode(utils.installedNodePayload(packageName)); body: { data },
} = await authOwnerShellAgent.get('/nodes');
await authAgent(ownerShell).get('/nodes'); expect(data[0].installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT);
expect(data[0].updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED);
expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', {
doNotHandleError: true,
}); });
}); });
test('GET /nodes should report package updates if available', async () => { describe('POST /nodes', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should reject if package name is missing', async () => {
const { statusCode } = await authOwnerShellAgent.post('/nodes');
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload()); expect(statusCode).toBe(400);
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
mocked(executeCommand).mockImplementationOnce(() => {
throw {
code: 1,
stdout: JSON.stringify({
[packageName]: {
current: COMMUNITY_PACKAGE_VERSION.CURRENT,
wanted: COMMUNITY_PACKAGE_VERSION.CURRENT,
latest: COMMUNITY_PACKAGE_VERSION.UPDATED,
location: path.join('node_modules', packageName),
},
}),
};
}); });
mocked(isNpmError).mockReturnValueOnce(true); test('should reject if package is duplicate', async () => {
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
mocked(isPackageInstalled).mockResolvedValueOnce(true);
mocked(hasPackageLoaded).mockReturnValueOnce(true);
const { const {
body: { data }, statusCode,
} = await authAgent(ownerShell).get('/nodes'); body: { message },
} = await authOwnerShellAgent.post('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
expect(data[0].installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT); expect(statusCode).toBe(400);
expect(data[0].updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED); expect(message).toContain('already installed');
});
/**
* POST /nodes
*/
test('POST /nodes should reject if package name is missing', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const { statusCode } = await authAgent(ownerShell).post('/nodes');
expect(statusCode).toBe(400);
});
test('POST /nodes should reject if package is duplicate', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
mocked(isPackageInstalled).mockResolvedValueOnce(true);
mocked(hasPackageLoaded).mockReturnValueOnce(true);
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).post('/nodes').send({
name: utils.installedPackagePayload().packageName,
}); });
expect(statusCode).toBe(400); test('should allow installing packages that could not be loaded', async () => {
expect(message).toContain('already installed'); mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
}); mocked(hasPackageLoaded).mockReturnValueOnce(false);
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' });
test('POST /nodes should allow installing packages that could not be loaded', async () => { mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage);
const ownerShell = await testDb.createUserShell(globalOwnerRole);
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages()); const { statusCode } = await authOwnerShellAgent.post('/nodes').send({
mocked(hasPackageLoaded).mockReturnValueOnce(false); name: utils.installedPackagePayload().packageName,
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' }); });
mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage); expect(statusCode).toBe(200);
expect(mocked(removePackageFromMissingList)).toHaveBeenCalled();
const { statusCode } = await authAgent(ownerShell).post('/nodes').send({
name: utils.installedPackagePayload().packageName,
}); });
expect(statusCode).toBe(200); test('should not install a banned package', async () => {
expect(mocked(removePackageFromMissingList)).toHaveBeenCalled(); mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' });
const {
statusCode,
body: { message },
} = await authOwnerShellAgent.post('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
expect(statusCode).toBe(400);
expect(message).toContain('banned');
});
}); });
test('POST /nodes should not install a banned package', async () => { describe('DELETE /nodes', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should not delete if package name is empty', async () => {
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' }); const response = await authOwnerShellAgent.delete('/nodes');
const { expect(response.statusCode).toBe(400);
statusCode,
body: { message },
} = await authAgent(ownerShell).post('/nodes').send({
name: utils.installedPackagePayload().packageName,
}); });
expect(statusCode).toBe(400); test('should reject if package is not installed', async () => {
expect(message).toContain('banned'); const {
}); statusCode,
body: { message },
} = await authOwnerShellAgent.delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
});
/** expect(statusCode).toBe(400);
* DELETE /nodes expect(message).toContain('not installed');
*/
test('DELETE /nodes should not delete if package name is empty', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).delete('/nodes');
expect(response.statusCode).toBe(400);
});
test('DELETE /nodes should reject if package is not installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
}); });
expect(statusCode).toBe(400); test('should uninstall package', async () => {
expect(message).toContain('not installed'); const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn());
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authOwnerShellAgent.delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
});
expect(statusCode).toBe(200);
expect(removeSpy).toHaveBeenCalledTimes(1);
});
}); });
test('DELETE /nodes should uninstall package', async () => { describe('PATCH /nodes', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should reject if package name is empty', async () => {
const response = await authOwnerShellAgent.patch('/nodes');
const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn()); expect(response.statusCode).toBe(400);
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authAgent(ownerShell).delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
}); });
expect(statusCode).toBe(200); test('reject if package is not installed', async () => {
expect(removeSpy).toHaveBeenCalledTimes(1); const {
}); statusCode,
body: { message },
} = await authOwnerShellAgent.patch('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
/** expect(statusCode).toBe(400);
* PATCH /nodes expect(message).toContain('not installed');
*/
test('PATCH /nodes should reject if package name is empty', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).patch('/nodes');
expect(response.statusCode).toBe(400);
});
test('PATCH /nodes reject if package is not installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).patch('/nodes').send({
name: utils.installedPackagePayload().packageName,
}); });
expect(statusCode).toBe(400); test('should update a package', async () => {
expect(message).toContain('not installed'); const updateSpy =
}); mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage);
test('PATCH /nodes should update a package', async () => { mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const updateSpy = await authOwnerShellAgent.patch('/nodes').send({
mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage); name: utils.installedPackagePayload().packageName,
});
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage); expect(updateSpy).toHaveBeenCalledTimes(1);
await authAgent(ownerShell).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 validator from 'validator';
import type { SuperAgentTest } from 'supertest';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { import {
randomEmail, randomEmail,
randomInvalidPassword, randomInvalidPassword,
@ -11,23 +13,22 @@ import {
randomValidPassword, randomValidPassword,
} from './shared/random'; } from './shared/random';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
let app: express.Application; let app: Application;
let globalOwnerRole: Role; let globalOwnerRole: Role;
let authAgent: AuthAgent; let ownerShell: User;
let authOwnerShellAgent: SuperAgentTest;
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['owner'] }); app = await utils.initTestServer({ endpointGroups: ['owner'] });
globalOwnerRole = await testDb.getGlobalOwnerRole(); globalOwnerRole = await testDb.getGlobalOwnerRole();
authAgent = utils.createAuthAgent(app);
}); });
beforeEach(async () => { beforeEach(async () => {
config.set('userManagement.isInstanceOwnerSetUp', false); config.set('userManagement.isInstanceOwnerSetUp', false);
ownerShell = await testDb.createUserShell(globalOwnerRole);
authOwnerShellAgent = utils.createAuthAgent(app)(ownerShell);
}); });
afterEach(async () => { afterEach(async () => {
@ -38,152 +39,149 @@ afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('POST /owner/setup should create owner and enable isInstanceOwnerSetUp', async () => { describe('POST /owner/setup', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should create owner and enable isInstanceOwnerSetUp', async () => {
const newOwnerData = {
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const newOwnerData = { const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData);
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const response = await authAgent(ownerShell).post('/owner/setup').send(newOwnerData); expect(response.statusCode).toBe(200);
expect(response.statusCode).toBe(200); const {
id,
email,
firstName,
lastName,
personalizationAnswers,
globalRole,
password,
resetPasswordToken,
isPending,
apiKey,
} = response.body.data;
const { expect(validator.isUUID(id)).toBe(true);
id, expect(email).toBe(newOwnerData.email);
email, expect(firstName).toBe(newOwnerData.firstName);
firstName, expect(lastName).toBe(newOwnerData.lastName);
lastName, expect(personalizationAnswers).toBeNull();
personalizationAnswers, expect(password).toBeUndefined();
globalRole, expect(isPending).toBe(false);
password, expect(resetPasswordToken).toBeUndefined();
resetPasswordToken, expect(globalRole.name).toBe('owner');
isPending, expect(globalRole.scope).toBe('global');
apiKey, expect(apiKey).toBeUndefined();
} = response.body.data;
expect(validator.isUUID(id)).toBe(true); const storedOwner = await Db.collections.User.findOneByOrFail({ id });
expect(email).toBe(newOwnerData.email); expect(storedOwner.password).not.toBe(newOwnerData.password);
expect(firstName).toBe(newOwnerData.firstName); expect(storedOwner.email).toBe(newOwnerData.email);
expect(lastName).toBe(newOwnerData.lastName); expect(storedOwner.firstName).toBe(newOwnerData.firstName);
expect(personalizationAnswers).toBeNull(); expect(storedOwner.lastName).toBe(newOwnerData.lastName);
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(resetPasswordToken).toBeUndefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const storedOwner = await Db.collections.User.findOneByOrFail({ id }); const isInstanceOwnerSetUpConfig = config.getEnv('userManagement.isInstanceOwnerSetUp');
expect(storedOwner.password).not.toBe(newOwnerData.password); expect(isInstanceOwnerSetUpConfig).toBe(true);
expect(storedOwner.email).toBe(newOwnerData.email);
expect(storedOwner.firstName).toBe(newOwnerData.firstName);
expect(storedOwner.lastName).toBe(newOwnerData.lastName);
const isInstanceOwnerSetUpConfig = config.getEnv('userManagement.isInstanceOwnerSetUp'); const isInstanceOwnerSetUpSetting = await utils.isInstanceOwnerSetUp();
expect(isInstanceOwnerSetUpConfig).toBe(true); expect(isInstanceOwnerSetUpSetting).toBe(true);
});
const isInstanceOwnerSetUpSetting = await utils.isInstanceOwnerSetUp();
expect(isInstanceOwnerSetUpSetting).toBe(true); test('should create owner with lowercased email', async () => {
}); const newOwnerData = {
email: randomEmail().toUpperCase(),
test('POST /owner/setup should create owner with lowercased email', async () => { firstName: randomName(),
const ownerShell = await testDb.createUserShell(globalOwnerRole); lastName: randomName(),
password: randomValidPassword(),
const newOwnerData = { };
email: randomEmail().toUpperCase(),
firstName: randomName(), const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData);
lastName: randomName(),
password: randomValidPassword(), expect(response.statusCode).toBe(200);
};
const { id, email } = response.body.data;
const response = await authAgent(ownerShell).post('/owner/setup').send(newOwnerData);
expect(id).toBe(ownerShell.id);
expect(response.statusCode).toBe(200); expect(email).toBe(newOwnerData.email.toLowerCase());
const { id, email } = response.body.data; const storedOwner = await Db.collections.User.findOneByOrFail({ id });
expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase());
expect(id).toBe(ownerShell.id); });
expect(email).toBe(newOwnerData.email.toLowerCase());
const INVALID_POST_OWNER_PAYLOADS = [
const storedOwner = await Db.collections.User.findOneByOrFail({ id }); {
expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase()); email: '',
}); firstName: randomName(),
lastName: randomName(),
test('POST /owner/setup should fail with invalid inputs', async () => { password: randomValidPassword(),
const ownerShell = await testDb.createUserShell(globalOwnerRole); },
const authOwnerAgent = authAgent(ownerShell); {
email: randomEmail(),
await Promise.all( firstName: '',
INVALID_POST_OWNER_PAYLOADS.map(async (invalidPayload) => { lastName: randomName(),
const response = await authOwnerAgent.post('/owner/setup').send(invalidPayload); password: randomValidPassword(),
expect(response.statusCode).toBe(400); },
}), {
); email: randomEmail(),
}); firstName: randomName(),
lastName: '',
test('POST /owner/skip-setup should persist skipping setup to the DB', async () => { password: randomValidPassword(),
const ownerShell = await testDb.createUserShell(globalOwnerRole); },
{
const response = await authAgent(ownerShell).post('/owner/skip-setup').send(); email: randomEmail(),
firstName: randomName(),
expect(response.statusCode).toBe(200); lastName: randomName(),
password: randomInvalidPassword(),
const skipConfig = config.getEnv('userManagement.skipInstanceOwnerSetup'); },
expect(skipConfig).toBe(true); {
firstName: randomName(),
const { value } = await Db.collections.Settings.findOneByOrFail({ lastName: randomName(),
key: 'userManagement.skipInstanceOwnerSetup', },
{
firstName: randomName(),
},
{
lastName: randomName(),
},
{
email: randomEmail(),
firstName: 'John <script',
lastName: randomName(),
},
{
email: randomEmail(),
firstName: 'John <a',
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);
}),
);
}); });
expect(value).toBe('true');
}); });
const INVALID_POST_OWNER_PAYLOADS = [ describe('POST /owner/skip-setup', () => {
{ test('should persist skipping setup to the DB', async () => {
email: '', const response = await authOwnerShellAgent.post('/owner/skip-setup').send();
firstName: randomName(),
lastName: randomName(), expect(response.statusCode).toBe(200);
password: randomValidPassword(),
}, const skipConfig = config.getEnv('userManagement.skipInstanceOwnerSetup');
{ expect(skipConfig).toBe(true);
email: randomEmail(),
firstName: '', const { value } = await Db.collections.Settings.findOneByOrFail({
lastName: randomName(), key: 'userManagement.skipInstanceOwnerSetup',
password: randomValidPassword(), });
}, expect(value).toBe('true');
{ });
email: randomEmail(), });
firstName: randomName(),
lastName: '',
password: randomValidPassword(),
},
{
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomInvalidPassword(),
},
{
firstName: randomName(),
lastName: randomName(),
},
{
firstName: randomName(),
},
{
lastName: randomName(),
},
{
email: randomEmail(),
firstName: 'John <script',
lastName: randomName(),
},
{
email: randomEmail(),
firstName: 'John <a',
lastName: randomName(),
},
];

View file

@ -1,10 +1,12 @@
import express from 'express'; import type { SuperAgentTest } from 'supertest';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { compare } from 'bcryptjs';
import * as utils from './shared/utils';
import * as Db from '@/Db'; import * as Db from '@/Db';
import config from '@/config'; 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 { import {
randomEmail, randomEmail,
randomInvalidPassword, randomInvalidPassword,
@ -12,276 +14,237 @@ import {
randomValidPassword, randomValidPassword,
} from './shared/random'; } from './shared/random';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { Role } from '@db/entities/Role';
jest.mock('@/UserManagement/email/NodeMailer'); jest.mock('@/UserManagement/email/NodeMailer');
let app: express.Application;
let globalOwnerRole: Role; let globalOwnerRole: Role;
let globalMemberRole: Role; let globalMemberRole: Role;
let owner: User;
let authlessAgent: SuperAgentTest;
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['passwordReset'] }); const app = await utils.initTestServer({ endpointGroups: ['passwordReset'] });
globalOwnerRole = await testDb.getGlobalOwnerRole(); globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole(); globalMemberRole = await testDb.getGlobalMemberRole();
authlessAgent = utils.createAgent(app);
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['User']); await testDb.truncate(['User']);
owner = await testDb.createUser({ globalRole: globalOwnerRole });
jest.mock('@/config');
config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.isInstanceOwnerSetUp', true);
config.set('userManagement.emails.mode', '');
}); });
afterAll(async () => { afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('POST /forgot-password should send password reset email', async () => { describe('POST /forgot-password', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); test('should send password reset email', async () => {
const member = await testDb.createUser({
email: 'test@test.com',
globalRole: globalMemberRole,
});
const authlessAgent = utils.createAgent(app); config.set('userManagement.emails.mode', 'smtp');
const member = await testDb.createUser({
email: 'test@test.com', await Promise.all(
globalRole: globalMemberRole, [{ email: owner.email }, { email: member.email.toUpperCase() }].map(async (payload) => {
const response = await authlessAgent.post('/forgot-password').send(payload);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({});
const user = await Db.collections.User.findOneByOrFail({ email: payload.email });
expect(user.resetPasswordToken).toBeDefined();
expect(user.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
}),
);
}); });
config.set('userManagement.emails.mode', 'smtp'); test('should fail if emailing is not set up', async () => {
config.set('userManagement.emails.mode', '');
await Promise.all( await authlessAgent.post('/forgot-password').send({ email: owner.email }).expect(500);
[{ email: owner.email }, { email: member.email.toUpperCase() }].map(async (payload) => {
const response = await authlessAgent.post('/forgot-password').send(payload);
expect(response.statusCode).toBe(200); const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email });
expect(response.body).toEqual({}); expect(storedOwner.resetPasswordToken).toBeNull();
});
const user = await Db.collections.User.findOneByOrFail({ email: payload.email }); test('should fail with invalid inputs', async () => {
expect(user.resetPasswordToken).toBeDefined(); config.set('userManagement.emails.mode', 'smtp');
expect(user.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
}), const invalidPayloads = [
); randomEmail(),
[randomEmail()],
{},
[{ name: randomName() }],
[{ email: randomName() }],
];
await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
const response = await authlessAgent.post('/forgot-password').send(invalidPayload);
expect(response.statusCode).toBe(400);
const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email });
expect(storedOwner.resetPasswordToken).toBeNull();
}),
);
});
test('should fail if user is not found', async () => {
config.set('userManagement.emails.mode', 'smtp');
const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() });
expect(response.statusCode).toBe(200); // expect 200 to remain vague
});
}); });
test('POST /forgot-password should fail if emailing is not set up', async () => { describe('GET /resolve-password-token', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); beforeEach(() => {
config.set('userManagement.emails.mode', 'smtp');
});
const authlessAgent = utils.createAgent(app); test('should succeed with valid inputs', async () => {
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
const response = await authlessAgent.post('/forgot-password').send({ email: owner.email }); await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
expect(response.statusCode).toBe(500); const response = await authlessAgent
.get('/resolve-password-token')
.query({ userId: owner.id, token: resetPasswordToken });
const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email }); expect(response.statusCode).toBe(200);
expect(storedOwner.resetPasswordToken).toBeNull(); });
});
test('POST /forgot-password should fail with invalid inputs', async () => { test('should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() });
const authlessAgent = utils.createAgent(app); const second = await authlessAgent.get('/resolve-password-token').query({ userId: owner.id });
config.set('userManagement.emails.mode', 'smtp'); for (const response of [first, second]) {
const invalidPayloads = [
randomEmail(),
[randomEmail()],
{},
[{ name: randomName() }],
[{ email: randomName() }],
];
await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
const response = await authlessAgent.post('/forgot-password').send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
}
const storedOwner = await Db.collections.User.findOneByOrFail({ email: owner.email });
expect(storedOwner.resetPasswordToken).toBeNull();
}),
);
});
test('POST /forgot-password should fail if user is not found', async () => {
const authlessAgent = utils.createAgent(app);
config.set('userManagement.emails.mode', 'smtp');
const response = await authlessAgent.post('/forgot-password').send({ email: randomEmail() });
expect(response.statusCode).toBe(200); // expect 200 to remain vague
});
test('GET /resolve-password-token should succeed with valid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
}); });
const response = await authlessAgent test('should fail if user is not found', async () => {
.get('/resolve-password-token') const response = await authlessAgent
.query({ userId: owner.id, token: resetPasswordToken }); .get('/resolve-password-token')
.query({ userId: owner.id, token: uuid() });
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(404);
});
test('GET /resolve-password-token should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
config.set('userManagement.emails.mode', 'smtp');
const first = await authlessAgent.get('/resolve-password-token').query({ token: uuid() });
const second = await authlessAgent.get('/resolve-password-token').query({ userId: owner.id });
for (const response of [first, second]) {
expect(response.statusCode).toBe(400);
}
});
test('GET /resolve-password-token should fail if user is not found', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
config.set('userManagement.emails.mode', 'smtp');
const response = await authlessAgent
.get('/resolve-password-token')
.query({ userId: owner.id, token: uuid() });
expect(response.statusCode).toBe(404);
});
test('GET /resolve-password-token should fail if token is expired', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
}); });
config.set('userManagement.emails.mode', 'smtp'); test('should fail if token is expired', async () => {
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
const response = await authlessAgent await Db.collections.User.update(owner.id, {
.get('/resolve-password-token') resetPasswordToken,
.query({ userId: owner.id, token: resetPasswordToken }); resetPasswordTokenExpiration,
});
expect(response.statusCode).toBe(404); const response = await authlessAgent
.get('/resolve-password-token')
.query({ userId: owner.id, token: resetPasswordToken });
expect(response.statusCode).toBe(404);
});
}); });
test('POST /change-password should succeed with valid inputs', async () => { describe('POST /change-password', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid(); const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
const passwordToStore = randomValidPassword(); const passwordToStore = randomValidPassword();
const response = await authlessAgent.post('/change-password').send({ test('should succeed with valid inputs', async () => {
token: resetPasswordToken, const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
userId: owner.id,
password: passwordToStore,
});
expect(response.statusCode).toBe(200); await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
const authToken = utils.getAuthToken(response); const response = await authlessAgent.post('/change-password').send({
expect(authToken).toBeDefined();
const { password: storedPassword } = await Db.collections.User.findOneByOrFail({ id: owner.id });
const comparisonResult = await compare(passwordToStore, storedPassword);
expect(comparisonResult).toBe(true);
expect(storedPassword).not.toBe(passwordToStore);
});
test('POST /change-password should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
const invalidPayloads = [
{ token: uuid() },
{ id: owner.id },
{ password: randomValidPassword() },
{ token: uuid(), id: owner.id },
{ token: uuid(), password: randomValidPassword() },
{ id: owner.id, password: randomValidPassword() },
{
id: owner.id,
password: randomInvalidPassword(),
token: resetPasswordToken, token: resetPasswordToken,
}, userId: owner.id,
{ password: passwordToStore,
});
expect(response.statusCode).toBe(200);
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
const { password: storedPassword } = await Db.collections.User.findOneByOrFail({
id: owner.id, id: owner.id,
password: randomValidPassword(), });
token: uuid(),
},
];
await Promise.all( const comparisonResult = await compare(passwordToStore, storedPassword);
invalidPayloads.map(async (invalidPayload) => { expect(comparisonResult).toBe(true);
const response = await authlessAgent.post('/change-password').query(invalidPayload); expect(storedPassword).not.toBe(passwordToStore);
expect(response.statusCode).toBe(400);
const { password: storedPassword } = await Db.collections.User.findOneByOrFail({});
expect(owner.password).toBe(storedPassword);
}),
);
});
test('POST /change-password should fail when token has expired', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
const resetPasswordToken = uuid();
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
}); });
const passwordToStore = randomValidPassword(); test('should fail with invalid inputs', async () => {
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) + 100;
const response = await authlessAgent.post('/change-password').send({ await Db.collections.User.update(owner.id, {
token: resetPasswordToken, resetPasswordToken,
userId: owner.id, resetPasswordTokenExpiration,
password: passwordToStore, });
const invalidPayloads = [
{ token: uuid() },
{ id: owner.id },
{ password: randomValidPassword() },
{ token: uuid(), id: owner.id },
{ token: uuid(), password: randomValidPassword() },
{ id: owner.id, password: randomValidPassword() },
{
id: owner.id,
password: randomInvalidPassword(),
token: resetPasswordToken,
},
{
id: owner.id,
password: randomValidPassword(),
token: uuid(),
},
];
await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
const response = await authlessAgent.post('/change-password').query(invalidPayload);
expect(response.statusCode).toBe(400);
const { password: storedPassword } = await Db.collections.User.findOneByOrFail({});
expect(owner.password).toBe(storedPassword);
}),
);
}); });
expect(response.statusCode).toBe(404); test('should fail when token has expired', async () => {
const resetPasswordTokenExpiration = Math.floor(Date.now() / 1000) - 1;
await Db.collections.User.update(owner.id, {
resetPasswordToken,
resetPasswordTokenExpiration,
});
const response = await authlessAgent.post('/change-password').send({
token: resetPasswordToken,
userId: owner.id,
password: passwordToStore,
});
expect(response.statusCode).toBe(404);
});
}); });

View file

@ -1,24 +1,25 @@
import express from 'express'; import type { SuperAgentTest } from 'supertest';
import { UserSettings } from 'n8n-core'; import { UserSettings } from 'n8n-core';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { randomApiKey, randomName, randomString } from '../shared/random'; import { randomApiKey, randomName, randomString } from '../shared/random';
import * as utils from '../shared/utils'; import * as utils from '../shared/utils';
import type { CredentialPayload, SaveCredentialFunction } from '../shared/types'; import type { CredentialPayload, SaveCredentialFunction } from '../shared/types';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
let app: express.Application;
let globalOwnerRole: Role;
let globalMemberRole: Role; let globalMemberRole: Role;
let credentialOwnerRole: Role; let credentialOwnerRole: Role;
let owner: User;
let member: User;
let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest;
let saveCredential: SaveCredentialFunction; let saveCredential: SaveCredentialFunction;
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ const app = await utils.initTestServer({
endpointGroups: ['publicApi'], endpointGroups: ['publicApi'],
applyAuth: false, applyAuth: false,
enablePublicAPI: true, enablePublicAPI: true,
@ -26,334 +27,265 @@ beforeAll(async () => {
utils.initConfigFile(); utils.initConfigFile();
const [fetchedGlobalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] = const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] =
await testDb.getAllRoles(); await testDb.getAllRoles();
globalOwnerRole = fetchedGlobalOwnerRole;
globalMemberRole = fetchedGlobalMemberRole; globalMemberRole = fetchedGlobalMemberRole;
credentialOwnerRole = fetchedCredentialOwnerRole; 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); saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole);
utils.initCredentialsTypes(); utils.initCredentialsTypes();
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['User', 'SharedCredentials', 'Credentials']); await testDb.truncate(['SharedCredentials', 'Credentials']);
}); });
afterAll(async () => { afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('POST /credentials should create credentials', async () => { describe('POST /credentials', () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole); test('should create credentials', async () => {
ownerShell = await testDb.addApiKey(ownerShell); const payload = {
name: 'test credential',
type: 'githubApi',
data: {
accessToken: 'abcdefghijklmnopqrstuvwxyz',
user: 'test',
server: 'testServer',
},
};
const authOwnerAgent = utils.createAgent(app, { const response = await authOwnerAgent.post('/credentials').send(payload);
apiPath: 'public',
version: 1,
auth: true,
user: ownerShell,
});
const payload = {
name: 'test credential',
type: 'githubApi',
data: {
accessToken: 'abcdefghijklmnopqrstuvwxyz',
user: 'test',
server: 'testServer',
},
};
const response = await authOwnerAgent.post('/credentials').send(payload); expect(response.statusCode).toBe(200);
expect(response.statusCode).toBe(200); const { id, name, type } = response.body;
const { id, name, type } = response.body; expect(name).toBe(payload.name);
expect(type).toBe(payload.type);
expect(name).toBe(payload.name); const credential = await Db.collections.Credentials.findOneByOrFail({ id });
expect(type).toBe(payload.type);
const credential = await Db.collections.Credentials.findOneByOrFail({ id }); expect(credential.name).toBe(payload.name);
expect(credential.type).toBe(payload.type);
expect(credential.data).not.toBe(payload.data);
expect(credential.name).toBe(payload.name); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
expect(credential.type).toBe(payload.type); relations: ['user', 'credentials', 'role'],
expect(credential.data).not.toBe(payload.data); where: { credentialsId: credential.id, userId: owner.id },
});
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ expect(sharedCredential.role).toEqual(credentialOwnerRole);
relations: ['user', 'credentials', 'role'], expect(sharedCredential.credentials.name).toBe(payload.name);
where: { credentialsId: credential.id, userId: ownerShell.id },
}); });
expect(sharedCredential.role).toEqual(credentialOwnerRole); test('should fail with invalid inputs', async () => {
expect(sharedCredential.credentials.name).toBe(payload.name); await Promise.all(
INVALID_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
expect(response.statusCode === 400 || response.statusCode === 415).toBe(true);
}),
);
});
test('should fail with missing encryption key', async () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey');
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY));
const response = await authOwnerAgent.post('/credentials').send(credentialPayload());
expect(response.statusCode).toBe(500);
mock.mockRestore();
});
}); });
test('POST /credentials should fail with invalid inputs', async () => { describe('DELETE /credentials/:id', () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole); test('should delete owned cred for owner', async () => {
ownerShell = await testDb.addApiKey(ownerShell); const savedCredential = await saveCredential(dbCredential(), { user: owner });
const authOwnerAgent = utils.createAgent(app, { const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
apiPath: 'public',
version: 1, expect(response.statusCode).toBe(200);
auth: true,
user: ownerShell, const { name, type } = response.body;
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
}); });
await Promise.all( test('should delete non-owned cred for owner', async () => {
INVALID_PAYLOADS.map(async (invalidPayload) => { const savedCredential = await saveCredential(dbCredential(), { user: member });
const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
expect(response.statusCode === 400 || response.statusCode === 415).toBe(true); const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
}),
); expect(response.statusCode).toBe(200);
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('should delete owned cred for member', async () => {
const savedCredential = await saveCredential(dbCredential(), { user: member });
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
const { name, type } = response.body;
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('should delete owned cred for member but leave others untouched', async () => {
const anotherMember = await testDb.createUser({
globalRole: globalMemberRole,
apiKey: randomApiKey(),
});
const savedCredential = await saveCredential(dbCredential(), { user: member });
const notToBeChangedCredential = await saveCredential(dbCredential(), { user: member });
const notToBeChangedCredential2 = await saveCredential(dbCredential(), {
user: anotherMember,
});
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
const { name, type } = response.body;
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOne({
where: {
credentialsId: savedCredential.id,
},
});
expect(deletedSharedCredential).toBeNull(); // deleted
await Promise.all(
[notToBeChangedCredential, notToBeChangedCredential2].map(async (credential) => {
const untouchedCredential = await Db.collections.Credentials.findOneBy({
id: credential.id,
});
expect(untouchedCredential).toEqual(credential); // not deleted
const untouchedSharedCredential = await Db.collections.SharedCredentials.findOne({
where: {
credentialsId: credential.id,
},
});
expect(untouchedSharedCredential).toBeDefined(); // not deleted
}),
);
});
test('should not delete non-owned cred for member', async () => {
const savedCredential = await saveCredential(dbCredential(), { user: owner });
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(404);
const shellCredential = await Db.collections.Credentials.findOneBy({
id: savedCredential.id,
});
expect(shellCredential).toBeDefined(); // not deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeDefined(); // not deleted
});
test('should fail if cred not found', async () => {
const response = await authOwnerAgent.delete('/credentials/123');
expect(response.statusCode).toBe(404);
});
}); });
test('POST /credentials should fail with missing encryption key', async () => { describe('GET /credentials/schema/:credentialType', () => {
const mock = jest.spyOn(UserSettings, 'getEncryptionKey'); test('should fail due to not found type', async () => {
mock.mockRejectedValue(new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY)); const response = await authOwnerAgent.get('/credentials/schema/testing');
let ownerShell = await testDb.createUserShell(globalOwnerRole); expect(response.statusCode).toBe(404);
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()); test('should retrieve credential type', async () => {
const response = await authOwnerAgent.get('/credentials/schema/githubApi');
expect(response.statusCode).toBe(500); const { additionalProperties, type, properties, required } = response.body;
mock.mockRestore(); expect(additionalProperties).toBe(false);
}); expect(type).toBe('object');
expect(properties.server).toBeDefined();
test('DELETE /credentials/:id should delete owned cred for owner', async () => { expect(properties.server.type).toBe('string');
let ownerShell = await testDb.createUserShell(globalOwnerRole); expect(properties.user.type).toBeDefined();
ownerShell = await testDb.addApiKey(ownerShell); expect(properties.user.type).toBe('string');
expect(properties.accessToken.type).toBeDefined();
const authOwnerAgent = utils.createAgent(app, { expect(properties.accessToken.type).toBe('string');
apiPath: 'public', expect(required).toEqual(expect.arrayContaining(['server', 'user', 'accessToken']));
version: 1, expect(response.statusCode).toBe(200);
auth: true,
user: ownerShell,
}); });
const savedCredential = await saveCredential(dbCredential(), { user: ownerShell });
const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
const { name, type } = response.body;
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('DELETE /credentials/:id should delete non-owned cred for owner', async () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole);
ownerShell = await testDb.addApiKey(ownerShell);
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
version: 1,
auth: true,
user: ownerShell,
});
const member = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(dbCredential(), { user: member });
const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('DELETE /credentials/:id should delete owned cred for member', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() });
const authMemberAgent = utils.createAgent(app, {
apiPath: 'public',
version: 1,
auth: true,
user: member,
});
const savedCredential = await saveCredential(dbCredential(), { user: member });
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
const { name, type } = response.body;
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeNull(); // deleted
});
test('DELETE /credentials/:id should delete owned cred for member but leave others untouched', async () => {
const member1 = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() });
const member2 = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() });
const savedCredential = await saveCredential(dbCredential(), { user: member1 });
const notToBeChangedCredential = await saveCredential(dbCredential(), { user: member1 });
const notToBeChangedCredential2 = await saveCredential(dbCredential(), { user: member2 });
const authMemberAgent = utils.createAgent(app, {
apiPath: 'public',
version: 1,
auth: true,
user: member1,
});
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(200);
const { name, type } = response.body;
expect(name).toBe(savedCredential.name);
expect(type).toBe(savedCredential.type);
const deletedCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(deletedCredential).toBeNull(); // deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOne({
where: {
credentialsId: savedCredential.id,
},
});
expect(deletedSharedCredential).toBeNull(); // deleted
await Promise.all(
[notToBeChangedCredential, notToBeChangedCredential2].map(async (credential) => {
const untouchedCredential = await Db.collections.Credentials.findOneBy({ id: credential.id });
expect(untouchedCredential).toEqual(credential); // not deleted
const untouchedSharedCredential = await Db.collections.SharedCredentials.findOne({
where: {
credentialsId: credential.id,
},
});
expect(untouchedSharedCredential).toBeDefined(); // not deleted
}),
);
});
test('DELETE /credentials/:id should not delete non-owned cred for member', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const member = await testDb.createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() });
const authMemberAgent = utils.createAgent(app, {
apiPath: 'public',
version: 1,
auth: true,
user: member,
});
const savedCredential = await saveCredential(dbCredential(), { user: ownerShell });
const response = await authMemberAgent.delete(`/credentials/${savedCredential.id}`);
expect(response.statusCode).toBe(404);
const shellCredential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(shellCredential).toBeDefined(); // not deleted
const deletedSharedCredential = await Db.collections.SharedCredentials.findOneBy({});
expect(deletedSharedCredential).toBeDefined(); // not deleted
});
test('DELETE /credentials/:id should fail if cred not found', async () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole);
ownerShell = await testDb.addApiKey(ownerShell);
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
version: 1,
auth: true,
user: ownerShell,
});
const response = await authOwnerAgent.delete('/credentials/123');
expect(response.statusCode).toBe(404);
});
test('GET /credentials/schema/:credentialType should fail due to not found type', async () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole);
ownerShell = await testDb.addApiKey(ownerShell);
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
version: 1,
auth: true,
user: ownerShell,
});
const response = await authOwnerAgent.get('/credentials/schema/testing');
expect(response.statusCode).toBe(404);
});
test('GET /credentials/schema/:credentialType should retrieve credential type', async () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole);
ownerShell = await testDb.addApiKey(ownerShell);
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
version: 1,
auth: true,
user: ownerShell,
});
const response = await authOwnerAgent.get('/credentials/schema/githubApi');
const { additionalProperties, type, properties, required } = response.body;
expect(additionalProperties).toBe(false);
expect(type).toBe('object');
expect(properties.server).toBeDefined();
expect(properties.server.type).toBe('string');
expect(properties.user.type).toBeDefined();
expect(properties.user.type).toBe('string');
expect(properties.accessToken.type).toBeDefined();
expect(properties.accessToken.type).toBe('string');
expect(required).toEqual(expect.arrayContaining(['server', 'user', 'accessToken']));
expect(response.statusCode).toBe(200);
}); });
const credentialPayload = (): CredentialPayload => ({ const credentialPayload = (): CredentialPayload => ({

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 config from '@/config';
import { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { randomApiKey } from '../shared/random'; import { randomApiKey } from '../shared/random';
import * as utils from '../shared/utils'; import * as utils from '../shared/utils';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
let app: express.Application; let app: Application;
let globalOwnerRole: Role; let owner: User;
let authOwnerAgent: SuperAgentTest;
let workflowRunner: ActiveWorkflowRunner; let workflowRunner: ActiveWorkflowRunner;
beforeAll(async () => { beforeAll(async () => {
@ -19,7 +20,8 @@ beforeAll(async () => {
enablePublicAPI: true, enablePublicAPI: true,
}); });
globalOwnerRole = await testDb.getGlobalOwnerRole(); const globalOwnerRole = await testDb.getGlobalOwnerRole();
owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
await utils.initBinaryManager(); await utils.initBinaryManager();
await utils.initNodeTypes(); await utils.initNodeTypes();
@ -31,13 +33,19 @@ beforeEach(async () => {
await testDb.truncate([ await testDb.truncate([
'SharedCredentials', 'SharedCredentials',
'SharedWorkflow', 'SharedWorkflow',
'User',
'Workflow', 'Workflow',
'Credentials', 'Credentials',
'Execution', 'Execution',
'Settings', 'Settings',
]); ]);
authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
config.set('userManagement.disabled', false); config.set('userManagement.disabled', false);
config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.isInstanceOwnerSetUp', true);
}); });
@ -50,270 +58,27 @@ afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('GET /executions/:id should fail due to missing API Key', async () => { const testWithAPIKey =
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); (method: 'get' | 'post' | 'put' | 'delete', url: string, apiKey: string | null) => async () => {
authOwnerAgent.set({ 'X-N8N-API-KEY': apiKey });
const response = await authOwnerAgent[method](url);
expect(response.statusCode).toBe(401);
};
const authOwnerAgent = utils.createAgent(app, { describe('GET /executions/:id', () => {
apiPath: 'public', test('should fail due to missing API Key', testWithAPIKey('get', '/executions/1', null));
auth: true,
user: owner,
version: 1,
});
const response = await authOwnerAgent.get('/executions/1'); test('should fail due to invalid API Key', testWithAPIKey('get', '/executions/1', 'abcXYZ'));
expect(response.statusCode).toBe(401); test('should get an execution', async () => {
}); const workflow = await testDb.createWorkflow({}, owner);
test('GET /executions/:id should fail due to invalid API Key', async () => { const execution = await testDb.createSuccessfulExecution(workflow);
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
owner.apiKey = 'abcXYZ';
const authOwnerAgent = utils.createAgent(app, { const response = await authOwnerAgent.get(`/executions/${execution.id}`);
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const response = await authOwnerAgent.get('/executions/1'); expect(response.statusCode).toBe(200);
expect(response.statusCode).toBe(401);
});
test('GET /executions/:id should get an execution', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const workflow = await testDb.createWorkflow({}, owner);
const execution = await testDb.createSuccessfulExecution(workflow);
const response = await authOwnerAgent.get(`/executions/${execution.id}`);
expect(response.statusCode).toBe(200);
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = response.body;
expect(id).toBeDefined();
expect(finished).toBe(true);
expect(mode).toEqual(execution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(execution.workflowId);
expect(waitTill).toBeNull();
});
test('DELETE /executions/:id should fail due to missing API Key', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const response = await authOwnerAgent.delete('/executions/1');
expect(response.statusCode).toBe(401);
});
test('DELETE /executions/:id should fail due to invalid API Key', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
owner.apiKey = 'abcXYZ';
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const response = await authOwnerAgent.delete('/executions/1');
expect(response.statusCode).toBe(401);
});
test('DELETE /executions/:id should delete an execution', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const workflow = await testDb.createWorkflow({}, owner);
const execution = await testDb.createSuccessfulExecution(workflow);
const response = await authOwnerAgent.delete(`/executions/${execution.id}`);
expect(response.statusCode).toBe(200);
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = response.body;
expect(id).toBeDefined();
expect(finished).toBe(true);
expect(mode).toEqual(execution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(execution.workflowId);
expect(waitTill).toBeNull();
});
test('GET /executions should fail due to missing API Key', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const response = await authOwnerAgent.get('/executions');
expect(response.statusCode).toBe(401);
});
test('GET /executions should fail due to invalid API Key', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
owner.apiKey = 'abcXYZ';
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const response = await authOwnerAgent.get('/executions');
expect(response.statusCode).toBe(401);
});
test('GET /executions should retrieve all successful executions', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const workflow = await testDb.createWorkflow({}, owner);
const successfullExecution = await testDb.createSuccessfulExecution(workflow);
await testDb.createErrorExecution(workflow);
const response = await authOwnerAgent.get(`/executions`).query({
status: 'success',
});
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(1);
expect(response.body.nextCursor).toBe(null);
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = response.body.data[0];
expect(id).toBeDefined();
expect(finished).toBe(true);
expect(mode).toEqual(successfullExecution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(successfullExecution.workflowId);
expect(waitTill).toBeNull();
});
// failing on Postgres and MySQL - ref: https://github.com/n8n-io/n8n/pull/3834
test.skip('GET /executions should paginate two executions', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const workflow = await testDb.createWorkflow({}, owner);
const firstSuccessfulExecution = await testDb.createSuccessfulExecution(workflow);
const secondSuccessfulExecution = await testDb.createSuccessfulExecution(workflow);
await testDb.createErrorExecution(workflow);
const firstExecutionResponse = await authOwnerAgent.get(`/executions`).query({
status: 'success',
limit: 1,
});
expect(firstExecutionResponse.statusCode).toBe(200);
expect(firstExecutionResponse.body.data.length).toBe(1);
expect(firstExecutionResponse.body.nextCursor).toBeDefined();
const secondExecutionResponse = await authOwnerAgent.get(`/executions`).query({
status: 'success',
limit: 1,
cursor: firstExecutionResponse.body.nextCursor,
});
expect(secondExecutionResponse.statusCode).toBe(200);
expect(secondExecutionResponse.body.data.length).toBe(1);
expect(secondExecutionResponse.body.nextCursor).toBeNull();
const successfulExecutions = [firstSuccessfulExecution, secondSuccessfulExecution];
const executions = [...firstExecutionResponse.body.data, ...secondExecutionResponse.body.data];
for (let i = 0; i < executions.length; i++) {
const { const {
id, id,
finished, finished,
@ -324,146 +89,33 @@ test.skip('GET /executions should paginate two executions', async () => {
stoppedAt, stoppedAt,
workflowId, workflowId,
waitTill, waitTill,
} = executions[i]; } = response.body;
expect(id).toBeDefined(); expect(id).toBeDefined();
expect(finished).toBe(true); expect(finished).toBe(true);
expect(mode).toEqual(successfulExecutions[i].mode); expect(mode).toEqual(execution.mode);
expect(retrySuccessId).toBeNull(); expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull(); expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull(); expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull(); expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(successfulExecutions[i].workflowId); expect(workflowId).toBe(execution.workflowId);
expect(waitTill).toBeNull(); expect(waitTill).toBeNull();
} });
}); });
test('GET /executions should retrieve all error executions', async () => { describe('DELETE /executions/:id', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); test('should fail due to missing API Key', testWithAPIKey('delete', '/executions/1', null));
const authOwnerAgent = utils.createAgent(app, { test('should fail due to invalid API Key', testWithAPIKey('delete', '/executions/1', 'abcXYZ'));
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const workflow = await testDb.createWorkflow({}, owner); test('should delete an execution', async () => {
const workflow = await testDb.createWorkflow({}, owner);
const execution = await testDb.createSuccessfulExecution(workflow);
await testDb.createSuccessfulExecution(workflow); const response = await authOwnerAgent.delete(`/executions/${execution.id}`);
const errorExecution = await testDb.createErrorExecution(workflow); expect(response.statusCode).toBe(200);
const response = await authOwnerAgent.get(`/executions`).query({
status: 'error',
});
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(1);
expect(response.body.nextCursor).toBe(null);
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = response.body.data[0];
expect(id).toBeDefined();
expect(finished).toBe(false);
expect(mode).toEqual(errorExecution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(errorExecution.workflowId);
expect(waitTill).toBeNull();
});
test('GET /executions should return all waiting executions', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const workflow = await testDb.createWorkflow({}, owner);
await testDb.createSuccessfulExecution(workflow);
await testDb.createErrorExecution(workflow);
const waitingExecution = await testDb.createWaitingExecution(workflow);
const response = await authOwnerAgent.get(`/executions`).query({
status: 'waiting',
});
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(1);
expect(response.body.nextCursor).toBe(null);
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = response.body.data[0];
expect(id).toBeDefined();
expect(finished).toBe(false);
expect(mode).toEqual(waitingExecution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(waitingExecution.workflowId);
expect(new Date(waitTill).getTime()).toBeGreaterThan(Date.now() - 1000);
});
test('GET /executions should retrieve all executions of specific workflow', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
const authOwnerAgent = utils.createAgent(app, {
apiPath: 'public',
auth: true,
user: owner,
version: 1,
});
const [workflow, workflow2] = await testDb.createManyWorkflows(2, {}, owner);
const savedExecutions = await testDb.createManyExecutions(
2,
workflow,
// @ts-ignore
testDb.createSuccessfulExecution,
);
// @ts-ignore
await testDb.createManyExecutions(2, workflow2, testDb.createSuccessfulExecution);
const response = await authOwnerAgent.get(`/executions`).query({
workflowId: workflow.id,
});
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(2);
expect(response.body.nextCursor).toBe(null);
for (const execution of response.body.data) {
const { const {
id, id,
finished, finished,
@ -474,16 +126,240 @@ test('GET /executions should retrieve all executions of specific workflow', asyn
stoppedAt, stoppedAt,
workflowId, workflowId,
waitTill, waitTill,
} = execution; } = response.body;
expect(savedExecutions.some((exec) => exec.id === id)).toBe(true); expect(id).toBeDefined();
expect(finished).toBe(true); expect(finished).toBe(true);
expect(mode).toBeDefined(); expect(mode).toEqual(execution.mode);
expect(retrySuccessId).toBeNull(); expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull(); expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull(); expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull(); expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(workflow.id); expect(workflowId).toBe(execution.workflowId);
expect(waitTill).toBeNull(); expect(waitTill).toBeNull();
} });
});
describe('GET /executions', () => {
test('should fail due to missing API Key', testWithAPIKey('get', '/executions', null));
test('should fail due to invalid API Key', testWithAPIKey('get', '/executions', 'abcXYZ'));
test('should retrieve all successful executions', async () => {
const workflow = await testDb.createWorkflow({}, owner);
const successfulExecution = await testDb.createSuccessfulExecution(workflow);
await testDb.createErrorExecution(workflow);
const response = await authOwnerAgent.get(`/executions`).query({
status: 'success',
});
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(1);
expect(response.body.nextCursor).toBe(null);
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = response.body.data[0];
expect(id).toBeDefined();
expect(finished).toBe(true);
expect(mode).toEqual(successfulExecution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(successfulExecution.workflowId);
expect(waitTill).toBeNull();
});
// failing on Postgres and MySQL - ref: https://github.com/n8n-io/n8n/pull/3834
test.skip('should paginate two executions', async () => {
const workflow = await testDb.createWorkflow({}, owner);
const firstSuccessfulExecution = await testDb.createSuccessfulExecution(workflow);
const secondSuccessfulExecution = await testDb.createSuccessfulExecution(workflow);
await testDb.createErrorExecution(workflow);
const firstExecutionResponse = await authOwnerAgent.get(`/executions`).query({
status: 'success',
limit: 1,
});
expect(firstExecutionResponse.statusCode).toBe(200);
expect(firstExecutionResponse.body.data.length).toBe(1);
expect(firstExecutionResponse.body.nextCursor).toBeDefined();
const secondExecutionResponse = await authOwnerAgent.get(`/executions`).query({
status: 'success',
limit: 1,
cursor: firstExecutionResponse.body.nextCursor,
});
expect(secondExecutionResponse.statusCode).toBe(200);
expect(secondExecutionResponse.body.data.length).toBe(1);
expect(secondExecutionResponse.body.nextCursor).toBeNull();
const successfulExecutions = [firstSuccessfulExecution, secondSuccessfulExecution];
const executions = [...firstExecutionResponse.body.data, ...secondExecutionResponse.body.data];
for (let i = 0; i < executions.length; i++) {
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = executions[i];
expect(id).toBeDefined();
expect(finished).toBe(true);
expect(mode).toEqual(successfulExecutions[i].mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(successfulExecutions[i].workflowId);
expect(waitTill).toBeNull();
}
});
test('should retrieve all error executions', async () => {
const workflow = await testDb.createWorkflow({}, owner);
await testDb.createSuccessfulExecution(workflow);
const errorExecution = await testDb.createErrorExecution(workflow);
const response = await authOwnerAgent.get(`/executions`).query({
status: 'error',
});
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(1);
expect(response.body.nextCursor).toBe(null);
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = response.body.data[0];
expect(id).toBeDefined();
expect(finished).toBe(false);
expect(mode).toEqual(errorExecution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(errorExecution.workflowId);
expect(waitTill).toBeNull();
});
test('should return all waiting executions', async () => {
const workflow = await testDb.createWorkflow({}, owner);
await testDb.createSuccessfulExecution(workflow);
await testDb.createErrorExecution(workflow);
const waitingExecution = await testDb.createWaitingExecution(workflow);
const response = await authOwnerAgent.get(`/executions`).query({
status: 'waiting',
});
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(1);
expect(response.body.nextCursor).toBe(null);
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = response.body.data[0];
expect(id).toBeDefined();
expect(finished).toBe(false);
expect(mode).toEqual(waitingExecution.mode);
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(waitingExecution.workflowId);
expect(new Date(waitTill).getTime()).toBeGreaterThan(Date.now() - 1000);
});
test('should retrieve all executions of specific workflow', async () => {
const [workflow, workflow2] = await testDb.createManyWorkflows(2, {}, owner);
const savedExecutions = await testDb.createManyExecutions(
2,
workflow,
// @ts-ignore
testDb.createSuccessfulExecution,
);
// @ts-ignore
await testDb.createManyExecutions(2, workflow2, testDb.createSuccessfulExecution);
const response = await authOwnerAgent.get(`/executions`).query({
workflowId: workflow.id,
});
expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(2);
expect(response.body.nextCursor).toBe(null);
for (const execution of response.body.data) {
const {
id,
finished,
mode,
retryOf,
retrySuccessId,
startedAt,
stoppedAt,
workflowId,
waitTill,
} = execution;
expect(savedExecutions.some((exec) => exec.id === id)).toBe(true);
expect(finished).toBe(true);
expect(mode).toBeDefined();
expect(retrySuccessId).toBeNull();
expect(retryOf).toBeNull();
expect(startedAt).not.toBeNull();
expect(stoppedAt).not.toBeNull();
expect(workflowId).toBe(workflow.id);
expect(waitTill).toBeNull();
}
});
}); });

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,14 @@
import express from 'express'; import type { SuperAgentTest } from 'supertest';
import config from '@/config'; 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 { randomEmail, randomName, randomValidPassword } from '../shared/random';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
import type { AuthAgent } from '../shared/types';
import * as utils from '../shared/utils'; import * as utils from '../shared/utils';
import { setSamlLoginEnabled } from '../../../src/sso/saml/samlHelpers';
import { setCurrentAuthenticationMethod } from '../../../src/sso/ssoHelpers';
let app: express.Application; let owner: User;
let globalOwnerRole: Role; let authOwnerAgent: SuperAgentTest;
let globalMemberRole: Role;
let authAgent: AuthAgent;
function enableSaml(enable: boolean) { function enableSaml(enable: boolean) {
setSamlLoginEnabled(enable); setSamlLoginEnabled(enable);
@ -21,58 +17,59 @@ function enableSaml(enable: boolean) {
} }
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['me'] }); const app = await utils.initTestServer({ endpointGroups: ['me'] });
owner = await testDb.createOwner();
globalOwnerRole = await testDb.getGlobalOwnerRole(); authOwnerAgent = utils.createAuthAgent(app)(owner);
globalMemberRole = await testDb.getGlobalMemberRole();
authAgent = utils.createAuthAgent(app);
}); });
beforeEach(async () => { // beforeEach(async () => {
await testDb.truncate(['User']); // await testDb.truncate(['User']);
}); // });
afterAll(async () => { afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
describe('Instance owner', () => { describe('Instance owner', () => {
test('PATCH /me should succeed with valid inputs', async () => { describe('PATCH /me', () => {
const owner = await testDb.createOwner(); test('should succeed with valid inputs', async () => {
const authOwnerAgent = authAgent(owner); enableSaml(false);
const response = await authOwnerAgent.patch('/me').send({ await authOwnerAgent
email: randomEmail(), .patch('/me')
firstName: randomName(), .send({
lastName: randomName(), email: randomEmail(),
password: randomValidPassword(), firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
})
.expect(200);
});
test('should throw BadRequestError if email is changed when SAML is enabled', async () => {
enableSaml(true);
await authOwnerAgent
.patch('/me')
.send({
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
})
.expect(400, { code: 400, message: 'SAML user may not change their email' });
}); });
expect(response.statusCode).toBe(200);
}); });
test('PATCH /me should throw BadRequestError if email is changed when SAML is enabled', async () => { describe('PATCH /password', () => {
enableSaml(true); test('should throw BadRequestError if password is changed when SAML is enabled', async () => {
const owner = await testDb.createOwner(); enableSaml(true);
const authOwnerAgent = authAgent(owner); await authOwnerAgent
const response = await authOwnerAgent.patch('/me').send({ .patch('/me/password')
email: randomEmail(), .send({
firstName: randomName(), password: randomValidPassword(),
lastName: randomName(), })
.expect(400, {
code: 400,
message: 'With SAML enabled, users need to use their SAML provider to change passwords',
});
}); });
expect(response.statusCode).toBe(400);
expect(response.body.message).toContain('SAML');
enableSaml(false);
});
test('PATCH /password should throw BadRequestError if password is changed when SAML is enabled', async () => {
enableSaml(true);
const owner = await testDb.createOwner();
const authOwnerAgent = authAgent(owner);
const response = await authOwnerAgent.patch('/me/password').send({
password: randomValidPassword(),
});
expect(response.statusCode).toBe(400);
expect(response.body.message).toContain('SAML');
enableSaml(false);
}); });
}); });

View file

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

View file

@ -9,13 +9,11 @@ import set from 'lodash.set';
import { BinaryDataManager, UserSettings } from 'n8n-core'; import { BinaryDataManager, UserSettings } from 'n8n-core';
import { import {
ICredentialType, ICredentialType,
ICredentialTypes,
IDataObject, IDataObject,
IExecuteFunctions, IExecuteFunctions,
INode, INode,
INodeExecutionData, INodeExecutionData,
INodeParameters, INodeParameters,
INodesAndCredentials,
ITriggerFunctions, ITriggerFunctions,
ITriggerResponse, ITriggerResponse,
LoggerProxy, LoggerProxy,
@ -90,13 +88,6 @@ export const mockInstance = <T>(
return instance; return instance;
}; };
const loadNodesAndCredentials: INodesAndCredentials = {
loaded: { nodes: {}, credentials: {} },
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
Container.set(LoadNodesAndCredentials, loadNodesAndCredentials);
/** /**
* Initialize a test server. * Initialize a test server.
*/ */
@ -740,6 +731,15 @@ export async function isInstanceOwnerSetUp() {
return Boolean(value); 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 // misc
// ---------------------------------- // ----------------------------------

View file

@ -1,5 +1,6 @@
import express from 'express';
import validator from 'validator'; import validator from 'validator';
import { Not } from 'typeorm';
import type { SuperAgentTest } from 'supertest';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
@ -8,6 +9,9 @@ import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { compareHash } from '@/UserManagement/UserManagementHelper'; 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 { SUCCESS_RESPONSE_BODY } from './shared/constants';
import { import {
randomCredentialPayload, randomCredentialPayload,
@ -17,41 +21,40 @@ import {
randomValidPassword, randomValidPassword,
} from './shared/random'; } from './shared/random';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import { UserManagementMailer } from '@/UserManagement/email/UserManagementMailer';
import { NodeMailer } from '@/UserManagement/email/NodeMailer';
jest.mock('@/UserManagement/email/NodeMailer'); jest.mock('@/UserManagement/email/NodeMailer');
let app: express.Application;
let globalMemberRole: Role; let globalMemberRole: Role;
let globalOwnerRole: Role;
let workflowOwnerRole: Role; let workflowOwnerRole: Role;
let credentialOwnerRole: Role; let credentialOwnerRole: Role;
let authAgent: AuthAgent; let owner: User;
let authlessAgent: SuperAgentTest;
let authOwnerAgent: SuperAgentTest;
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['users'] }); const app = await utils.initTestServer({ endpointGroups: ['users'] });
const [ const [
fetchedGlobalOwnerRole, globalOwnerRole,
fetchedGlobalMemberRole, fetchedGlobalMemberRole,
fetchedWorkflowOwnerRole, fetchedWorkflowOwnerRole,
fetchedCredentialOwnerRole, fetchedCredentialOwnerRole,
] = await testDb.getAllRoles(); ] = await testDb.getAllRoles();
globalOwnerRole = fetchedGlobalOwnerRole;
globalMemberRole = fetchedGlobalMemberRole; globalMemberRole = fetchedGlobalMemberRole;
workflowOwnerRole = fetchedWorkflowOwnerRole; workflowOwnerRole = fetchedWorkflowOwnerRole;
credentialOwnerRole = fetchedCredentialOwnerRole; credentialOwnerRole = fetchedCredentialOwnerRole;
authAgent = utils.createAuthAgent(app); owner = await testDb.createUser({ globalRole: globalOwnerRole });
authlessAgent = utils.createAgent(app);
authOwnerAgent = utils.createAuthAgent(app)(owner);
}); });
beforeEach(async () => { 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'); jest.mock('@/config');
@ -65,18 +68,16 @@ afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('GET /users should return all users', async () => { describe('GET /users', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); test('should return all users', async () => {
await testDb.createUser({ globalRole: globalMemberRole });
await testDb.createUser({ globalRole: globalMemberRole }); const response = await authOwnerAgent.get('/users');
const response = await authAgent(owner).get('/users'); expect(response.statusCode).toBe(200);
expect(response.body.data.length).toBe(2);
expect(response.statusCode).toBe(200); response.body.data.map((user: User) => {
expect(response.body.data.length).toBe(2);
await Promise.all(
response.body.data.map(async (user: User) => {
const { const {
id, id,
email, email,
@ -100,442 +101,421 @@ test('GET /users should return all users', async () => {
expect(isPending).toBe(false); expect(isPending).toBe(false);
expect(globalRole).toBeDefined(); expect(globalRole).toBeDefined();
expect(apiKey).not.toBeDefined(); expect(apiKey).not.toBeDefined();
}), });
); });
}); });
test('DELETE /users/:id should delete the user', async () => { describe('DELETE /users/:id', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); test('should delete the user', async () => {
const userToDelete = await testDb.createUser({ globalRole: globalMemberRole });
const userToDelete = await testDb.createUser({ globalRole: globalMemberRole }); const newWorkflow = new WorkflowEntity();
const newWorkflow = new WorkflowEntity(); Object.assign(newWorkflow, {
name: randomName(),
active: false,
connections: {},
nodes: [],
});
Object.assign(newWorkflow, { const savedWorkflow = await Db.collections.Workflow.save(newWorkflow);
name: randomName(),
active: false, await Db.collections.SharedWorkflow.save({
connections: {}, role: workflowOwnerRole,
nodes: [], user: userToDelete,
workflow: savedWorkflow,
});
const newCredential = new CredentialsEntity();
Object.assign(newCredential, {
name: randomName(),
data: '',
type: '',
nodesAccess: [],
});
const savedCredential = await Db.collections.Credentials.save(newCredential);
await Db.collections.SharedCredentials.save({
role: credentialOwnerRole,
user: userToDelete,
credentials: savedCredential,
});
const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
const user = await Db.collections.User.findOneBy({ id: userToDelete.id });
expect(user).toBeNull(); // deleted
const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({
relations: ['user'],
where: { userId: userToDelete.id, roleId: workflowOwnerRole.id },
});
expect(sharedWorkflow).toBeNull(); // deleted
const sharedCredential = await Db.collections.SharedCredentials.findOne({
relations: ['user'],
where: { userId: userToDelete.id, roleId: credentialOwnerRole.id },
});
expect(sharedCredential).toBeNull(); // deleted
const workflow = await Db.collections.Workflow.findOneBy({ id: savedWorkflow.id });
expect(workflow).toBeNull(); // deleted
// TODO: Include active workflow and check whether webhook has been removed
const credential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(credential).toBeNull(); // deleted
}); });
const savedWorkflow = await Db.collections.Workflow.save(newWorkflow); test('should fail to delete self', async () => {
const response = await authOwnerAgent.delete(`/users/${owner.id}`);
await Db.collections.SharedWorkflow.save({ expect(response.statusCode).toBe(400);
role: workflowOwnerRole,
user: userToDelete, const user = await Db.collections.User.findOneBy({ id: owner.id });
workflow: savedWorkflow, expect(user).toBeDefined();
}); });
const newCredential = new CredentialsEntity(); test('should fail if user to delete is transferee', async () => {
const { id: idToDelete } = await testDb.createUser({ globalRole: globalMemberRole });
Object.assign(newCredential, { const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({
name: randomName(), transferId: idToDelete,
data: '', });
type: '',
nodesAccess: [], expect(response.statusCode).toBe(400);
const user = await Db.collections.User.findOneBy({ id: idToDelete });
expect(user).toBeDefined();
}); });
const savedCredential = await Db.collections.Credentials.save(newCredential); test('with transferId should perform transfer', async () => {
const userToDelete = await testDb.createUser({ globalRole: globalMemberRole });
await Db.collections.SharedCredentials.save({ const savedWorkflow = await testDb.createWorkflow(undefined, userToDelete);
role: credentialOwnerRole,
user: userToDelete, const savedCredential = await testDb.saveCredential(randomCredentialPayload(), {
credentials: savedCredential, user: userToDelete,
role: credentialOwnerRole,
});
const response = await authOwnerAgent.delete(`/users/${userToDelete.id}`).query({
transferId: owner.id,
});
expect(response.statusCode).toBe(200);
const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({
relations: ['workflow'],
where: { userId: owner.id },
});
expect(sharedWorkflow.workflow).toBeDefined();
expect(sharedWorkflow.workflow.id).toBe(savedWorkflow.id);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { userId: owner.id },
});
expect(sharedCredential.credentials).toBeDefined();
expect(sharedCredential.credentials.id).toBe(savedCredential.id);
const deletedUser = await Db.collections.User.findOneBy({ id: userToDelete.id });
expect(deletedUser).toBeNull();
}); });
const response = await authAgent(owner).delete(`/users/${userToDelete.id}`);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
const user = await Db.collections.User.findOneBy({ id: userToDelete.id });
expect(user).toBeNull(); // deleted
const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({
relations: ['user'],
where: { userId: userToDelete.id, roleId: workflowOwnerRole.id },
});
expect(sharedWorkflow).toBeNull(); // deleted
const sharedCredential = await Db.collections.SharedCredentials.findOne({
relations: ['user'],
where: { userId: userToDelete.id, roleId: credentialOwnerRole.id },
});
expect(sharedCredential).toBeNull(); // deleted
const workflow = await Db.collections.Workflow.findOneBy({ id: savedWorkflow.id });
expect(workflow).toBeNull(); // deleted
// TODO: Include active workflow and check whether webhook has been removed
const credential = await Db.collections.Credentials.findOneBy({ id: savedCredential.id });
expect(credential).toBeNull(); // deleted
}); });
test('DELETE /users/:id should fail to delete self', async () => { describe('POST /users/:id', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); test('should fill out a user shell', async () => {
const memberShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(owner).delete(`/users/${owner.id}`); const memberData = {
expect(response.statusCode).toBe(400);
const user = await Db.collections.User.findOneBy({ id: owner.id });
expect(user).toBeDefined();
});
test('DELETE /users/:id should fail if user to delete is transferee', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const { id: idToDelete } = await testDb.createUser({ globalRole: globalMemberRole });
const response = await authAgent(owner).delete(`/users/${idToDelete}`).query({
transferId: idToDelete,
});
expect(response.statusCode).toBe(400);
const user = await Db.collections.User.findOneBy({ id: idToDelete });
expect(user).toBeDefined();
});
test('DELETE /users/:id with transferId should perform transfer', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const userToDelete = await testDb.createUser({ globalRole: globalMemberRole });
const savedWorkflow = await testDb.createWorkflow(undefined, userToDelete);
const savedCredential = await testDb.saveCredential(randomCredentialPayload(), {
user: userToDelete,
role: credentialOwnerRole,
});
const response = await authAgent(owner).delete(`/users/${userToDelete.id}`).query({
transferId: owner.id,
});
expect(response.statusCode).toBe(200);
const sharedWorkflow = await Db.collections.SharedWorkflow.findOneOrFail({
relations: ['workflow'],
where: { userId: owner.id },
});
expect(sharedWorkflow.workflow).toBeDefined();
expect(sharedWorkflow.workflow.id).toBe(savedWorkflow.id);
const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({
relations: ['credentials'],
where: { userId: owner.id },
});
expect(sharedCredential.credentials).toBeDefined();
expect(sharedCredential.credentials.id).toBe(savedCredential.id);
const deletedUser = await Db.collections.User.findOneBy({ id: userToDelete.id });
expect(deletedUser).toBeNull();
});
test('POST /users/:id should fill out a user shell', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const memberShell = await testDb.createUserShell(globalMemberRole);
const memberData = {
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.post(`/users/${memberShell.id}`).send(memberData);
const {
id,
email,
firstName,
lastName,
personalizationAnswers,
password,
resetPasswordToken,
globalRole,
isPending,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBeDefined();
expect(firstName).toBe(memberData.firstName);
expect(lastName).toBe(memberData.lastName);
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(resetPasswordToken).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole).toBeDefined();
expect(apiKey).not.toBeDefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
const member = await Db.collections.User.findOneByOrFail({ id: memberShell.id });
expect(member.firstName).toBe(memberData.firstName);
expect(member.lastName).toBe(memberData.lastName);
expect(member.password).not.toBe(memberData.password);
});
test('POST /users/:id should fail with invalid inputs', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
const memberShellEmail = randomEmail();
const memberShell = await Db.collections.User.save({
email: memberShellEmail,
globalRole: globalMemberRole,
});
const invalidPayloads = [
{
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
},
{
inviterId: owner.id,
firstName: randomName(),
password: randomValidPassword(),
},
{
inviterId: owner.id,
firstName: randomName(),
password: randomValidPassword(),
},
{
inviterId: owner.id, inviterId: owner.id,
firstName: randomName(), firstName: randomName(),
lastName: randomName(), lastName: randomName(),
}, password: randomValidPassword(),
{ };
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
password: randomInvalidPassword(),
},
];
await Promise.all( const response = await authlessAgent.post(`/users/${memberShell.id}`).send(memberData);
invalidPayloads.map(async (invalidPayload) => {
const response = await authlessAgent.post(`/users/${memberShell.id}`).send(invalidPayload);
expect(response.statusCode).toBe(400);
const storedUser = await Db.collections.User.findOneOrFail({ const {
where: { email: memberShellEmail }, id,
}); email,
firstName,
lastName,
personalizationAnswers,
password,
resetPasswordToken,
globalRole,
isPending,
apiKey,
} = response.body.data;
expect(storedUser.firstName).toBeNull();
expect(storedUser.lastName).toBeNull();
expect(storedUser.password).toBeNull();
}),
);
});
test('POST /users/:id should fail with already accepted invite', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const member = await testDb.createUser({ globalRole: globalMemberRole });
const newMemberData = {
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const authlessAgent = utils.createAgent(app);
const response = await authlessAgent.post(`/users/${member.id}`).send(newMemberData);
expect(response.statusCode).toBe(400);
const storedMember = await Db.collections.User.findOneOrFail({
where: { email: member.email },
});
expect(storedMember.firstName).not.toBe(newMemberData.firstName);
expect(storedMember.lastName).not.toBe(newMemberData.lastName);
const comparisonResult = await compareHash(member.password, storedMember.password);
expect(comparisonResult).toBe(false);
expect(storedMember.password).not.toBe(newMemberData.password);
});
test('POST /users should succeed if emailing is not set up', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const response = await authAgent(owner)
.post('/users')
.send([{ email: randomEmail() }]);
expect(response.statusCode).toBe(200);
expect(response.body.data[0].user.inviteAcceptUrl).toBeDefined();
});
test('POST /users should fail if user management is disabled', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
config.set('userManagement.disabled', true);
config.set('userManagement.isInstanceOwnerSetUp', false);
const response = await authAgent(owner)
.post('/users')
.send([{ email: randomEmail() }]);
expect(response.statusCode).toBe(400);
});
test('POST /users should email invites and create user shells but ignore existing', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const member = await testDb.createUser({ globalRole: globalMemberRole });
const memberShell = await testDb.createUserShell(globalMemberRole);
config.set('userManagement.emails.mode', 'smtp');
const testEmails = [randomEmail(), randomEmail().toUpperCase(), memberShell.email, member.email];
const payload = testEmails.map((e) => ({ email: e }));
const response = await authAgent(owner).post('/users').send(payload);
expect(response.statusCode).toBe(200);
for (const {
user: { id, email: receivedEmail },
error,
} of response.body.data) {
expect(validator.isUUID(id)).toBe(true); expect(validator.isUUID(id)).toBe(true);
expect(id).not.toBe(member.id); expect(email).toBeDefined();
expect(firstName).toBe(memberData.firstName);
const lowerCasedEmail = receivedEmail.toLowerCase(); expect(lastName).toBe(memberData.lastName);
expect(receivedEmail).toBe(lowerCasedEmail);
expect(payload.some(({ email }) => email.toLowerCase() === lowerCasedEmail)).toBe(true);
if (error) {
expect(error).toBe('Email could not be sent');
}
const storedUser = await Db.collections.User.findOneByOrFail({ id });
const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } =
storedUser;
expect(firstName).toBeNull();
expect(lastName).toBeNull();
expect(personalizationAnswers).toBeNull(); expect(personalizationAnswers).toBeNull();
expect(password).toBeNull(); expect(password).toBeUndefined();
expect(resetPasswordToken).toBeNull(); expect(resetPasswordToken).toBeUndefined();
} expect(isPending).toBe(false);
expect(globalRole).toBeDefined();
expect(apiKey).not.toBeDefined();
const authToken = utils.getAuthToken(response);
expect(authToken).toBeDefined();
const member = await Db.collections.User.findOneByOrFail({ id: memberShell.id });
expect(member.firstName).toBe(memberData.firstName);
expect(member.lastName).toBe(memberData.lastName);
expect(member.password).not.toBe(memberData.password);
});
test('should fail with invalid inputs', async () => {
const memberShellEmail = randomEmail();
const memberShell = await Db.collections.User.save({
email: memberShellEmail,
globalRole: globalMemberRole,
});
const invalidPayloads = [
{
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
},
{
inviterId: owner.id,
firstName: randomName(),
password: randomValidPassword(),
},
{
inviterId: owner.id,
firstName: randomName(),
password: randomValidPassword(),
},
{
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
},
{
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
password: randomInvalidPassword(),
},
];
await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
const response = await authlessAgent.post(`/users/${memberShell.id}`).send(invalidPayload);
expect(response.statusCode).toBe(400);
const storedUser = await Db.collections.User.findOneOrFail({
where: { email: memberShellEmail },
});
expect(storedUser.firstName).toBeNull();
expect(storedUser.lastName).toBeNull();
expect(storedUser.password).toBeNull();
}),
);
});
test('should fail with already accepted invite', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const newMemberData = {
inviterId: owner.id,
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const response = await authlessAgent.post(`/users/${member.id}`).send(newMemberData);
expect(response.statusCode).toBe(400);
const storedMember = await Db.collections.User.findOneOrFail({
where: { email: member.email },
});
expect(storedMember.firstName).not.toBe(newMemberData.firstName);
expect(storedMember.lastName).not.toBe(newMemberData.lastName);
const comparisonResult = await compareHash(member.password, storedMember.password);
expect(comparisonResult).toBe(false);
expect(storedMember.password).not.toBe(newMemberData.password);
});
}); });
test('POST /users should fail with invalid inputs', async () => { describe('POST /users', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); beforeEach(() => {
const authOwnerAgent = authAgent(owner); config.set('userManagement.emails.mode', 'smtp');
});
config.set('userManagement.emails.mode', 'smtp'); test('should succeed if emailing is not set up', async () => {
const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]);
const invalidPayloads = [ expect(response.statusCode).toBe(200);
randomEmail(), expect(response.body.data[0].user.inviteAcceptUrl).toBeDefined();
[randomEmail()], });
{},
[{ name: randomName() }],
[{ email: randomName() }],
];
await Promise.all( test('should fail if user management is disabled', async () => {
invalidPayloads.map(async (invalidPayload) => { config.set('userManagement.disabled', true);
const response = await authOwnerAgent.post('/users').send(invalidPayload); config.set('userManagement.isInstanceOwnerSetUp', false);
expect(response.statusCode).toBe(400);
const users = await Db.collections.User.find(); const response = await authOwnerAgent.post('/users').send([{ email: randomEmail() }]);
expect(users.length).toBe(1); // DB unaffected
}), expect(response.statusCode).toBe(400);
); });
test('should email invites and create user shells but ignore existing', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const memberShell = await testDb.createUserShell(globalMemberRole);
const testEmails = [
randomEmail(),
randomEmail().toUpperCase(),
memberShell.email,
member.email,
];
const payload = testEmails.map((e) => ({ email: e }));
const response = await authOwnerAgent.post('/users').send(payload);
expect(response.statusCode).toBe(200);
for (const {
user: { id, email: receivedEmail },
error,
} of response.body.data) {
expect(validator.isUUID(id)).toBe(true);
expect(id).not.toBe(member.id);
const lowerCasedEmail = receivedEmail.toLowerCase();
expect(receivedEmail).toBe(lowerCasedEmail);
expect(payload.some(({ email }) => email.toLowerCase() === lowerCasedEmail)).toBe(true);
if (error) {
expect(error).toBe('Email could not be sent');
}
const storedUser = await Db.collections.User.findOneByOrFail({ id });
const { firstName, lastName, personalizationAnswers, password, resetPasswordToken } =
storedUser;
expect(firstName).toBeNull();
expect(lastName).toBeNull();
expect(personalizationAnswers).toBeNull();
expect(password).toBeNull();
expect(resetPasswordToken).toBeNull();
}
});
test('should fail with invalid inputs', async () => {
const invalidPayloads = [
randomEmail(),
[randomEmail()],
{},
[{ name: randomName() }],
[{ email: randomName() }],
];
await Promise.all(
invalidPayloads.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/users').send(invalidPayload);
expect(response.statusCode).toBe(400);
const users = await Db.collections.User.find();
expect(users.length).toBe(1); // DB unaffected
}),
);
});
test('should ignore an empty payload', async () => {
const response = await authOwnerAgent.post('/users').send([]);
const { data } = response.body;
expect(response.statusCode).toBe(200);
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(0);
const users = await Db.collections.User.find();
expect(users.length).toBe(1);
});
}); });
test('POST /users should ignore an empty payload', async () => { describe('POST /users/:id/reinvite', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); beforeEach(() => {
config.set('userManagement.emails.mode', 'smtp');
// those configs are needed to make sure the reinvite email is sent,because of this check isEmailSetUp()
config.set('userManagement.emails.smtp.host', 'host');
config.set('userManagement.emails.smtp.auth.user', 'user');
config.set('userManagement.emails.smtp.auth.pass', 'pass');
});
config.set('userManagement.emails.mode', 'smtp'); test('should send reinvite, but fail if user already accepted invite', async () => {
const email = randomEmail();
const payload = [{ email }];
const response = await authOwnerAgent.post('/users').send(payload);
const response = await authAgent(owner).post('/users').send([]); expect(response.statusCode).toBe(200);
const { data } = response.body; const { data } = response.body;
const invitedUserId = data[0].user.id;
const reinviteResponse = await authOwnerAgent.post(`/users/${invitedUserId}/reinvite`);
expect(response.statusCode).toBe(200); expect(reinviteResponse.statusCode).toBe(200);
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(0);
const users = await Db.collections.User.find(); const member = await testDb.createUser({ globalRole: globalMemberRole });
expect(users.length).toBe(1); const reinviteMemberResponse = await authOwnerAgent.post(`/users/${member.id}/reinvite`);
expect(reinviteMemberResponse.statusCode).toBe(400);
});
}); });
test('POST /users/:id/reinvite should send reinvite, but fail if user already accepted invite', async () => { describe('UserManagementMailer expect NodeMailer.verifyConnection', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); let mockInit: jest.SpyInstance<Promise<void>, []>;
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); let mockVerifyConnection: jest.SpyInstance<Promise<void>, []>;
config.set('userManagement.emails.mode', 'smtp'); beforeAll(() => {
mockVerifyConnection = jest
.spyOn(NodeMailer.prototype, 'verifyConnection')
.mockImplementation(async () => {});
mockInit = jest.spyOn(NodeMailer.prototype, 'init').mockImplementation(async () => {});
});
// those configs are needed to make sure the reinvite email is sent,because of this check isEmailSetUp() afterAll(() => {
config.set('userManagement.emails.smtp.host', 'host'); mockVerifyConnection.mockRestore();
config.set('userManagement.emails.smtp.auth.user', 'user'); mockInit.mockRestore();
config.set('userManagement.emails.smtp.auth.pass', 'pass'); });
const email = randomEmail(); test('not be called when SMTP not set up', async () => {
const payload = [{ email }]; const userManagementMailer = new UserManagementMailer();
const response = await authOwnerAgent.post('/users').send(payload); // NodeMailer.verifyConnection gets called only explicitly
expect(async () => await userManagementMailer.verifyConnection()).rejects.toThrow();
expect(response.statusCode).toBe(200); expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(0);
});
const { data } = response.body; test('to be called when SMTP set up', async () => {
const invitedUserId = data[0].user.id; // host needs to be set, otherwise smtp is skipped
const reinviteResponse = await authOwnerAgent.post(`/users/${invitedUserId}/reinvite`); config.set('userManagement.emails.smtp.host', 'host');
config.set('userManagement.emails.mode', 'smtp');
expect(reinviteResponse.statusCode).toBe(200); const userManagementMailer = new UserManagementMailer();
// NodeMailer.verifyConnection gets called only explicitly
const member = await testDb.createUser({ globalRole: globalMemberRole }); expect(async () => await userManagementMailer.verifyConnection()).not.toThrow();
const reinviteMemberResponse = await authOwnerAgent.post(`/users/${member.id}/reinvite`); });
expect(reinviteMemberResponse.statusCode).toBe(400);
});
test('UserManagementMailer expect NodeMailer.verifyConnection not be called when SMTP not set up', async () => {
const mockVerifyConnection = jest.spyOn(NodeMailer.prototype, 'verifyConnection');
mockVerifyConnection.mockImplementation(async () => {});
const userManagementMailer = new UserManagementMailer();
// NodeMailer.verifyConnection gets called only explicitly
expect(async () => await userManagementMailer.verifyConnection()).rejects.toThrow();
expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(0);
mockVerifyConnection.mockRestore();
});
test('UserManagementMailer expect NodeMailer.verifyConnection to be called when SMTP set up', async () => {
const mockVerifyConnection = jest.spyOn(NodeMailer.prototype, 'verifyConnection');
mockVerifyConnection.mockImplementation(async () => {});
const mockInit = jest.spyOn(NodeMailer.prototype, 'init');
mockInit.mockImplementation(async () => {});
// host needs to be set, otherwise smtp is skipped
config.set('userManagement.emails.smtp.host', 'host');
config.set('userManagement.emails.mode', 'smtp');
const userManagementMailer = new UserManagementMailer();
// NodeMailer.verifyConnection gets called only explicitly
expect(async () => await userManagementMailer.verifyConnection()).not.toThrow();
// expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(1);
mockVerifyConnection.mockRestore();
mockInit.mockRestore();
}); });

View file

@ -1,89 +1,89 @@
import express from 'express'; import type { SuperAgentTest } from 'supertest';
import { v4 as uuid } from 'uuid'; 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 utils from './shared/utils';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import { createWorkflow } from './shared/testDb'; import { createWorkflow } from './shared/testDb';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper'; import type { SaveCredentialFunction } from './shared/types';
import type { Role } from '@db/entities/Role';
import config from '@/config';
import type { AuthAgent, SaveCredentialFunction } from './shared/types';
import { makeWorkflow } from './shared/utils'; import { makeWorkflow } from './shared/utils';
import { randomCredentialPayload } from './shared/random'; import { randomCredentialPayload } from './shared/random';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
let app: express.Application; let owner: User;
let globalOwnerRole: Role; let member: User;
let globalMemberRole: Role; let anotherMember: User;
let credentialOwnerRole: Role; let authOwnerAgent: SuperAgentTest;
let authAgent: AuthAgent; let authMemberAgent: SuperAgentTest;
let authAnotherMemberAgent: SuperAgentTest;
let saveCredential: SaveCredentialFunction; let saveCredential: SaveCredentialFunction;
let isSharingEnabled: jest.SpyInstance<boolean>;
let workflowRunner: ActiveWorkflowRunner;
let sharingSpy: jest.SpyInstance<boolean>; let sharingSpy: jest.SpyInstance<boolean>;
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['workflows'] }); const app = await utils.initTestServer({ endpointGroups: ['workflows'] });
globalOwnerRole = await testDb.getGlobalOwnerRole(); const globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole(); const 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 });
anotherMember = await testDb.createUser({ globalRole: globalMemberRole });
const authAgent = utils.createAuthAgent(app);
authOwnerAgent = authAgent(owner);
authMemberAgent = authAgent(member);
authAnotherMemberAgent = authAgent(anotherMember);
saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole); saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole);
sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
authAgent = utils.createAuthAgent(app);
isSharingEnabled = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
await utils.initNodeTypes(); await utils.initNodeTypes();
workflowRunner = await utils.initActiveWorkflowRunner();
config.set('enterprise.features.sharing', true); config.set('enterprise.features.sharing', true);
sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true); // @TODO: Remove on release
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['User', 'Workflow', 'SharedWorkflow']); await testDb.truncate(['Workflow', 'SharedWorkflow']);
}); });
afterAll(async () => { afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('Router should switch dynamically', async () => { describe('router should switch based on flag', () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); let savedWorkflowId: string;
const member = await testDb.createUser({ globalRole: globalMemberRole });
const createWorkflowResponse = await authAgent(owner).post('/workflows').send(makeWorkflow()); beforeEach(async () => {
const { id } = createWorkflowResponse.body.data; 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) test('when sharing is enabled', async () => {
.put(`/workflows/${id}/share`) await authOwnerAgent
.send({ shareWithIds: [member.id] }); .put(`/workflows/${savedWorkflowId}/share`)
.send({ shareWithIds: [member.id] })
expect(freeShareResponse.status).toBe(404); .expect(200);
});
// EE router
const paidShareResponse = await authAgent(owner)
.put(`/workflows/${id}/share`)
.send({ shareWithIds: [member.id] });
expect(paidShareResponse.status).toBe(200);
}); });
describe('PUT /workflows/:id', () => { describe('PUT /workflows/:id', () => {
test('PUT /workflows/:id/share should save sharing with new users', async () => { 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 workflow = await createWorkflow({}, owner);
const response = await authAgent(owner) const response = await authOwnerAgent
.put(`/workflows/${workflow.id}/share`) .put(`/workflows/${workflow.id}/share`)
.send({ shareWithIds: [member.id] }); .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 () => { 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 workflow = await createWorkflow({}, owner);
const response = await authAgent(owner) const response = await authOwnerAgent
.put(`/workflows/${workflow.id}/share`) .put(`/workflows/${workflow.id}/share`)
.send({ shareWithIds: [uuid()] }); .send({ shareWithIds: [uuid()] });
@ -108,12 +107,9 @@ describe('PUT /workflows/:id', () => {
}); });
test('PUT /workflows/:id/share should allow sharing with multiple users', async () => { 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 workflow = await createWorkflow({}, owner);
const response = await authAgent(owner) const response = await authOwnerAgent
.put(`/workflows/${workflow.id}/share`) .put(`/workflows/${workflow.id}/share`)
.send({ shareWithIds: [member.id, anotherMember.id] }); .send({ shareWithIds: [member.id, anotherMember.id] });
@ -124,13 +120,8 @@ describe('PUT /workflows/:id', () => {
}); });
test('PUT /workflows/:id/share should override sharing', async () => { 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 workflow = await createWorkflow({}, owner);
const authOwnerAgent = authAgent(owner);
const response = await authOwnerAgent const response = await authOwnerAgent
.put(`/workflows/${workflow.id}/share`) .put(`/workflows/${workflow.id}/share`)
.send({ shareWithIds: [member.id, anotherMember.id] }); .send({ shareWithIds: [member.id, anotherMember.id] });
@ -152,8 +143,6 @@ describe('PUT /workflows/:id', () => {
describe('GET /workflows', () => { describe('GET /workflows', () => {
test('should return workflows without nodes, sharing and credential usage details', async () => { 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 tag = await testDb.createTag({ name: 'test' });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
@ -183,7 +172,7 @@ describe('GET /workflows', () => {
await testDb.shareWorkflowWithUsers(workflow, [member]); await testDb.shareWorkflowWithUsers(workflow, [member]);
const response = await authAgent(owner).get('/workflows'); const response = await authOwnerAgent.get('/workflows');
const [fetchedWorkflow] = response.body.data; const [fetchedWorkflow] = response.body.data;
@ -192,42 +181,37 @@ describe('GET /workflows', () => {
id: owner.id, id: owner.id,
}); });
expect(fetchedWorkflow.sharedWith).not.toBeDefined() expect(fetchedWorkflow.sharedWith).not.toBeDefined();
expect(fetchedWorkflow.usedCredentials).not.toBeDefined() expect(fetchedWorkflow.usedCredentials).not.toBeDefined();
expect(fetchedWorkflow.nodes).not.toBeDefined() expect(fetchedWorkflow.nodes).not.toBeDefined();
expect(fetchedWorkflow.tags).toEqual( expect(fetchedWorkflow.tags).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(String), id: expect.any(String),
name: expect.any(String) name: expect.any(String),
}) }),
]) ]),
) );
}); });
}); });
describe('GET /workflows/:id', () => { describe('GET /workflows/:id', () => {
test('GET should fail with invalid id due to route rule', async () => { test('GET should fail with invalid id due to route rule', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const response = await authOwnerAgent.get('/workflows/potatoes');
const response = await authAgent(owner).get('/workflows/potatoes');
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
}); });
test('GET should return 404 for non existing workflow', async () => { test('GET should return 404 for non existing workflow', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const response = await authOwnerAgent.get('/workflows/9001');
const response = await authAgent(owner).get('/workflows/9001');
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
}); });
test('GET should return a workflow with owner', async () => { test('GET should return a workflow with owner', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const workflow = await createWorkflow({}, owner); 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.statusCode).toBe(200);
expect(response.body.data.ownedBy).toMatchObject({ expect(response.body.data.ownedBy).toMatchObject({
@ -241,12 +225,10 @@ describe('GET /workflows/:id', () => {
}); });
test('GET should return shared workflow with user data', async () => { 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); const workflow = await createWorkflow({}, owner);
await testDb.shareWorkflowWithUsers(workflow, [member]); 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.statusCode).toBe(200);
expect(response.body.data.ownedBy).toMatchObject({ expect(response.body.data.ownedBy).toMatchObject({
@ -266,13 +248,10 @@ describe('GET /workflows/:id', () => {
}); });
test('GET should return all sharees', async () => { 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); 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.statusCode).toBe(200);
expect(response.body.data.ownedBy).toMatchObject({ 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 () => { 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 savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const workflowPayload = makeWorkflow({ const workflowPayload = makeWorkflow({
@ -295,7 +273,7 @@ describe('GET /workflows/:id', () => {
}); });
const workflow = await createWorkflow(workflowPayload, owner); 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.statusCode).toBe(200);
expect(response.body.data.usedCredentials).toMatchObject([ 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 () => { 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 savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const workflowPayload = makeWorkflow({ const workflowPayload = makeWorkflow({
@ -320,7 +296,7 @@ describe('GET /workflows/:id', () => {
}); });
const workflow = await createWorkflow(workflowPayload, owner); 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.statusCode).toBe(200);
expect(response.body.data.usedCredentials).toMatchObject([ 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 () => { test('GET should return workflow with credentials for all users with or without access', async () => {
const member1 = await testDb.createUser({ globalRole: globalMemberRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const member2 = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
const workflowPayload = makeWorkflow({ const workflowPayload = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id, name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const workflow = await createWorkflow(workflowPayload, member1); const workflow = await createWorkflow(workflowPayload, member);
await testDb.shareWorkflowWithUsers(workflow, [member2]); 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.statusCode).toBe(200);
expect(responseMember1.body.data.usedCredentials).toMatchObject([ expect(responseMember1.body.data.usedCredentials).toMatchObject([
{ {
@ -357,7 +331,7 @@ describe('GET /workflows/:id', () => {
]); ]);
expect(responseMember1.body.data.sharedWith).toHaveLength(1); 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.statusCode).toBe(200);
expect(responseMember2.body.data.usedCredentials).toMatchObject([ 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 () => { test('GET should return workflow with credentials for all users with access', async () => {
const member1 = await testDb.createUser({ globalRole: globalMemberRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const member2 = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
// Both users have access to the credential (none is owner) // Both users have access to the credential (none is owner)
await testDb.shareCredentialWithUsers(savedCredential, [member2]); await testDb.shareCredentialWithUsers(savedCredential, [anotherMember]);
const workflowPayload = makeWorkflow({ const workflowPayload = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id, name: savedCredential.name }, withCredential: { id: savedCredential.id, name: savedCredential.name },
}); });
const workflow = await createWorkflow(workflowPayload, member1); const workflow = await createWorkflow(workflowPayload, member);
await testDb.shareWorkflowWithUsers(workflow, [member2]); 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.statusCode).toBe(200);
expect(responseMember1.body.data.usedCredentials).toMatchObject([ expect(responseMember1.body.data.usedCredentials).toMatchObject([
{ {
@ -394,7 +366,7 @@ describe('GET /workflows/:id', () => {
]); ]);
expect(responseMember1.body.data.sharedWith).toHaveLength(1); 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.statusCode).toBe(200);
expect(responseMember2.body.data.usedCredentials).toMatchObject([ expect(responseMember2.body.data.usedCredentials).toMatchObject([
{ {
@ -409,33 +381,26 @@ describe('GET /workflows/:id', () => {
describe('POST /workflows', () => { describe('POST /workflows', () => {
it('Should create a workflow that uses no credential', async () => { it('Should create a workflow that uses no credential', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const workflow = makeWorkflow({ withPinData: false }); 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); expect(response.statusCode).toBe(200);
}); });
it('Should save a new workflow with credentials', async () => { it('Should save a new workflow with credentials', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const workflow = makeWorkflow({ const workflow = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id, name: savedCredential.name }, 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); expect(response.statusCode).toBe(200);
}); });
it('Should not allow saving a workflow using credential you have no access', async () => { 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. // Credential belongs to owner, member cannot use it.
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const workflow = makeWorkflow({ const workflow = makeWorkflow({
@ -443,7 +408,7 @@ describe('POST /workflows', () => {
withCredential: { id: savedCredential.id, name: savedCredential.name }, 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.statusCode).toBe(400);
expect(response.body.message).toBe( 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 () => { 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. // Credential belongs to owner, member cannot use it.
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const workflow = makeWorkflow({ const workflow = makeWorkflow({
@ -462,32 +424,27 @@ describe('POST /workflows', () => {
withCredential: { id: savedCredential.id, name: savedCredential.name }, 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); expect(response.statusCode).toBe(200);
}); });
it('Should allow saving a workflow using a credential owned by others and shared with you', async () => { 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 savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const member2 = await testDb.createUser({ globalRole: globalMemberRole }); await testDb.shareCredentialWithUsers(savedCredential, [anotherMember]);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
await testDb.shareCredentialWithUsers(savedCredential, [member2]);
const workflow = makeWorkflow({ const workflow = makeWorkflow({
withPinData: false, withPinData: false,
withCredential: { id: savedCredential.id, name: savedCredential.name }, 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); expect(response.statusCode).toBe(200);
}); });
}); });
describe('PATCH /workflows/:id - validate credential permissions to user', () => { describe('PATCH /workflows/:id - validate credential permissions to user', () => {
it('Should succeed when saving unchanged workflow nodes', async () => { it('Should succeed when saving unchanged workflow nodes', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const workflow = { const workflow = {
name: 'test', 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 { 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', name: 'new name',
versionId, 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 () => { 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 savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const workflow = { const workflow = {
name: 'test', name: 'test',
@ -549,38 +503,33 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
], ],
}; };
const createResponse = await authAgent(owner).post('/workflows').send(workflow); const createResponse = await authOwnerAgent.post('/workflows').send(workflow);
const { id, versionId } = createResponse.body.data; const { id, versionId } = createResponse.body.data;
const response = await authAgent(owner) const response = await authOwnerAgent.patch(`/workflows/${id}`).send({
.patch(`/workflows/${id}`) versionId,
.send({ nodes: [
versionId, {
nodes: [ id: 'uuid-1234',
{ name: 'Start',
id: 'uuid-1234', parameters: {},
name: 'Start', position: [-20, 260],
parameters: {}, type: 'n8n-nodes-base.start',
position: [-20, 260], typeVersion: 1,
type: 'n8n-nodes-base.start', credentials: {
typeVersion: 1, default: {
credentials: { id: savedCredential.id,
default: { name: savedCredential.name,
id: savedCredential.id,
name: savedCredential.name,
},
}, },
}, },
], },
}); ],
});
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
}); });
it('Should prevent member from adding node containing credential inaccessible to member', async () => { 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 savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const workflow = { const workflow = {
@ -605,48 +554,43 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
], ],
}; };
const createResponse = await authAgent(owner).post('/workflows').send(workflow); const createResponse = await authOwnerAgent.post('/workflows').send(workflow);
const { id, versionId } = createResponse.body.data; const { id, versionId } = createResponse.body.data;
const response = await authAgent(member) const response = await authMemberAgent.patch(`/workflows/${id}`).send({
.patch(`/workflows/${id}`) versionId,
.send({ nodes: [
versionId, {
nodes: [ id: 'uuid-1234',
{ name: 'Start',
id: 'uuid-1234', parameters: {},
name: 'Start', position: [-20, 260],
parameters: {}, type: 'n8n-nodes-base.start',
position: [-20, 260], typeVersion: 1,
type: 'n8n-nodes-base.start', credentials: {},
typeVersion: 1, },
credentials: {}, {
}, id: 'uuid-12345',
{ name: 'Start',
id: 'uuid-12345', parameters: {},
name: 'Start', position: [-20, 260],
parameters: {}, type: 'n8n-nodes-base.start',
position: [-20, 260], typeVersion: 1,
type: 'n8n-nodes-base.start', credentials: {
typeVersion: 1, default: {
credentials: { id: savedCredential.id,
default: { name: savedCredential.name,
id: savedCredential.id,
name: savedCredential.name,
},
}, },
}, },
], },
}); ],
});
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
}); });
it('Should succeed but prevent modifying node attributes other than position, name and disabled', async () => { it('Should succeed but prevent modifying node attributes other than position, name and disabled', async () => {
const member1 = await testDb.createUser({ globalRole: globalMemberRole }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const member2 = await testDb.createUser({ globalRole: globalMemberRole });
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
const originalNodes: INode[] = [ const originalNodes: INode[] = [
{ {
@ -714,14 +658,12 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
nodes: originalNodes, 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; const { id, versionId } = createResponse.body.data;
await authAgent(member1) await authMemberAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [anotherMember.id] });
.put(`/workflows/${id}/share`)
.send({ shareWithIds: [member2.id] });
const response = await authAgent(member2).patch(`/workflows/${id}`).send({ const response = await authAnotherMemberAgent.patch(`/workflows/${id}`).send({
versionId, versionId,
nodes: changedNodes, nodes: changedNodes,
}); });
@ -733,29 +675,24 @@ describe('PATCH /workflows/:id - validate credential permissions to user', () =>
describe('PATCH /workflows/:id - validate interim updates', () => { describe('PATCH /workflows/:id - validate interim updates', () => {
it('should block owner updating workflow nodes on interim update by member', async () => { 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 // 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; const { id, versionId: ownerVersionId } = createResponse.body.data;
await authAgent(owner) await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] });
.put(`/workflows/${id}/share`)
.send({ shareWithIds: [member.id] });
// member accesses and updates workflow name // 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; const { versionId: memberVersionId } = memberGetResponse.body.data;
await authAgent(member) await authMemberAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ name: 'Update by member', versionId: memberVersionId }); .send({ name: 'Update by member', versionId: memberVersionId });
// owner blocked from updating workflow nodes // owner blocked from updating workflow nodes
const updateAttemptResponse = await authAgent(owner) const updateAttemptResponse = await authOwnerAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ nodes: [], versionId: ownerVersionId }); .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 () => { 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 // 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 { id, versionId: ownerFirstVersionId } = createResponse.body.data;
const updateResponse = await authAgent(owner) const updateResponse = await authOwnerAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ name: 'Update by owner', versionId: ownerFirstVersionId }); .send({ name: 'Update by owner', versionId: ownerFirstVersionId });
const { versionId: ownerSecondVersionId } = updateResponse.body.data; const { versionId: ownerSecondVersionId } = updateResponse.body.data;
await authAgent(owner) await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] });
.put(`/workflows/${id}/share`)
.send({ shareWithIds: [member.id] });
// member accesses workflow // member accesses workflow
const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`);
const { versionId: memberVersionId } = memberGetResponse.body.data; const { versionId: memberVersionId } = memberGetResponse.body.data;
// owner re-updates workflow // owner re-updates workflow
await authAgent(owner) await authOwnerAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ name: 'Owner update again', versionId: ownerSecondVersionId }); .send({ name: 'Owner update again', versionId: ownerSecondVersionId });
// member blocked from updating workflow // member blocked from updating workflow
const updateAttemptResponse = await authAgent(member) const updateAttemptResponse = await authMemberAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ nodes: [], versionId: memberVersionId }); .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 () => { 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 // 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; const { id, versionId: ownerVersionId } = createResponse.body.data;
await authAgent(owner) await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] });
.put(`/workflows/${id}/share`)
.send({ shareWithIds: [member.id] });
// member accesses and activates workflow // 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; const { versionId: memberVersionId } = memberGetResponse.body.data;
await authAgent(member) await authMemberAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ active: true, versionId: memberVersionId }); .send({ active: true, versionId: memberVersionId });
// owner blocked from activating workflow // owner blocked from activating workflow
const activationAttemptResponse = await authAgent(owner) const activationAttemptResponse = await authOwnerAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ active: true, versionId: ownerVersionId }); .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 () => { 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 // 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 { id, versionId: ownerFirstVersionId } = createResponse.body.data;
const updateResponse = await authAgent(owner) const updateResponse = await authOwnerAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ name: 'Update by owner', versionId: ownerFirstVersionId }); .send({ name: 'Update by owner', versionId: ownerFirstVersionId });
const { versionId: ownerSecondVersionId } = updateResponse.body.data; const { versionId: ownerSecondVersionId } = updateResponse.body.data;
await authAgent(owner) await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] });
.put(`/workflows/${id}/share`)
.send({ shareWithIds: [member.id] });
// member accesses workflow // member accesses workflow
const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`);
const { versionId: memberVersionId } = memberGetResponse.body.data; const { versionId: memberVersionId } = memberGetResponse.body.data;
// owner activates workflow // owner activates workflow
await authAgent(owner) await authOwnerAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ active: true, versionId: ownerSecondVersionId }); .send({ active: true, versionId: ownerSecondVersionId });
// member blocked from activating workflow // member blocked from activating workflow
const updateAttemptResponse = await authAgent(member) const updateAttemptResponse = await authMemberAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ active: true, versionId: memberVersionId }); .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 () => { 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 // 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; const { id, versionId: ownerVersionId } = createResponse.body.data;
await authAgent(owner) await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] });
.put(`/workflows/${id}/share`)
.send({ shareWithIds: [member.id] });
// member accesses workflow // member accesses workflow
const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`);
const { versionId: memberVersionId } = memberGetResponse.body.data; const { versionId: memberVersionId } = memberGetResponse.body.data;
// owner updates workflow name // owner updates workflow name
await authAgent(owner) await authOwnerAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ name: 'Another name', versionId: ownerVersionId }); .send({ name: 'Another name', versionId: ownerVersionId });
// member blocked from updating workflow settings // member blocked from updating workflow settings
const updateAttemptResponse = await authAgent(member) const updateAttemptResponse = await authMemberAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ settings: { saveManualExecutions: true }, versionId: memberVersionId }); .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 () => { 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 // 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; const { id, versionId: ownerVersionId } = createResponse.body.data;
await authAgent(owner) await authOwnerAgent.put(`/workflows/${id}/share`).send({ shareWithIds: [member.id] });
.put(`/workflows/${id}/share`)
.send({ shareWithIds: [member.id] });
// member accesses workflow // member accesses workflow
const memberGetResponse = await authAgent(member).get(`/workflows/${id}`); const memberGetResponse = await authMemberAgent.get(`/workflows/${id}`);
const { versionId: memberVersionId } = memberGetResponse.body.data; const { versionId: memberVersionId } = memberGetResponse.body.data;
// owner updates workflow settings // owner updates workflow settings
await authAgent(owner) await authOwnerAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ settings: { saveManualExecutions: true }, versionId: ownerVersionId }); .send({ settings: { saveManualExecutions: true }, versionId: ownerVersionId });
// member blocked from updating workflow name // member blocked from updating workflow name
const updateAttemptResponse = await authAgent(member) const updateAttemptResponse = await authMemberAgent
.patch(`/workflows/${id}`) .patch(`/workflows/${id}`)
.send({ settings: { saveManualExecutions: true }, versionId: memberVersionId }); .send({ settings: { saveManualExecutions: true }, versionId: memberVersionId });

View file

@ -1,78 +1,74 @@
import express from 'express'; import { SuperAgentTest } from 'supertest';
import type { IPinData } from 'n8n-workflow';
import type { User } from '@db/entities/User';
import * as UserManagementHelpers from '@/UserManagement/UserManagementHelper';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import * as testDb from './shared/testDb'; 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'; import { makeWorkflow, MOCK_PINDATA } from './shared/utils';
let app: express.Application; let ownerShell: User;
let globalOwnerRole: Role; let authOwnerAgent: SuperAgentTest;
// mock whether sharing is enabled or not
jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(false);
beforeAll(async () => { beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['workflows'] }); const app = await utils.initTestServer({ endpointGroups: ['workflows'] });
const globalOwnerRole = await testDb.getGlobalOwnerRole();
ownerShell = await testDb.createUserShell(globalOwnerRole);
authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
globalOwnerRole = await testDb.getGlobalOwnerRole(); // mock whether sharing is enabled or not
jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(false);
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['User', 'Workflow', 'SharedWorkflow']); await testDb.truncate(['Workflow', 'SharedWorkflow']);
}); });
afterAll(async () => { afterAll(async () => {
await testDb.terminate(); await testDb.terminate();
}); });
test('POST /workflows should store pin data for node in workflow', async () => { describe('POST /workflows', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should store pin data for node in workflow', async () => {
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const workflow = makeWorkflow({ withPinData: true });
const workflow = makeWorkflow({ withPinData: true }); const response = await authOwnerAgent.post('/workflows').send(workflow);
const response = await authOwnerAgent.post('/workflows').send(workflow); expect(response.statusCode).toBe(200);
expect(response.statusCode).toBe(200); const { pinData } = response.body.data as { pinData: IPinData };
const { pinData } = response.body.data as { pinData: IPinData }; expect(pinData).toMatchObject(MOCK_PINDATA);
});
expect(pinData).toMatchObject(MOCK_PINDATA); test('should set pin data to null if no pin data', async () => {
const workflow = makeWorkflow({ withPinData: false });
const response = await authOwnerAgent.post('/workflows').send(workflow);
expect(response.statusCode).toBe(200);
const { pinData } = response.body.data as { pinData: IPinData };
expect(pinData).toBeNull();
});
}); });
test('POST /workflows should set pin data to null if no pin data', async () => { describe('GET /workflows/:id', () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); test('should return pin data', async () => {
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell }); const workflow = makeWorkflow({ withPinData: true });
const workflow = makeWorkflow({ withPinData: false }); const workflowCreationResponse = await authOwnerAgent.post('/workflows').send(workflow);
const response = await authOwnerAgent.post('/workflows').send(workflow); const { id } = workflowCreationResponse.body.data as { id: string };
expect(response.statusCode).toBe(200); const workflowRetrievalResponse = await authOwnerAgent.get(`/workflows/${id}`);
const { pinData } = response.body.data as { pinData: IPinData }; expect(workflowRetrievalResponse.statusCode).toBe(200);
expect(pinData).toBeNull(); const { pinData } = workflowRetrievalResponse.body.data as { pinData: IPinData };
});
expect(pinData).toMatchObject(MOCK_PINDATA);
test('GET /workflows/:id should return pin data', async () => { });
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
const workflow = makeWorkflow({ withPinData: true });
const workflowCreationResponse = await authOwnerAgent.post('/workflows').send(workflow);
const { id } = workflowCreationResponse.body.data as { id: string };
const workflowRetrievalResponse = await authOwnerAgent.get(`/workflows/${id}`);
expect(workflowRetrievalResponse.statusCode).toBe(200);
const { pinData } = workflowRetrievalResponse.body.data as { pinData: IPinData };
expect(pinData).toMatchObject(MOCK_PINDATA);
}); });

View file

@ -18,7 +18,7 @@ import { User } from '@/databases/entities/User';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import { randomEmail, randomName } from '../integration/shared/random'; import { randomEmail, randomName } from '../integration/shared/random';
import * as Helpers from './Helpers'; import * as Helpers from './Helpers';
import { WorkflowExecuteAdditionalData } from '@/index'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';

View file

@ -11,20 +11,59 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { CredentialsHelper } from '@/CredentialsHelper'; import { CredentialsHelper } from '@/CredentialsHelper';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import * as Helpers from './Helpers';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
const TEST_ENCRYPTION_KEY = 'test';
const mockNodesAndCredentials: INodesAndCredentials = {
loaded: { nodes: {}, credentials: {} },
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
Container.set(LoadNodesAndCredentials, mockNodesAndCredentials);
describe('CredentialsHelper', () => { describe('CredentialsHelper', () => {
const TEST_ENCRYPTION_KEY = 'test';
const mockNodesAndCredentials: INodesAndCredentials = {
loaded: {
nodes: {
'test.set': {
sourcePath: '',
type: {
description: {
displayName: 'Set',
name: 'set',
group: ['input'],
version: 1,
description: 'Sets a value',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Value1',
name: 'value1',
type: 'string',
default: 'default-value1',
},
{
displayName: 'Value2',
name: 'value2',
type: 'string',
default: 'default-value2',
},
],
},
},
},
},
credentials: {},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
Container.set(LoadNodesAndCredentials, mockNodesAndCredentials);
const nodeTypes = Container.get(NodeTypes);
describe('authenticate', () => { describe('authenticate', () => {
const tests: Array<{ const tests: Array<{
description: string; description: string;
@ -219,8 +258,6 @@ describe('CredentialsHelper', () => {
qs: {}, qs: {},
}; };
const nodeTypes = Helpers.NodeTypes() as unknown as NodeTypes;
const workflow = new Workflow({ const workflow = new Workflow({
nodes: [node], nodes: [node],
connections: {}, connections: {},

View file

@ -1,32 +1,37 @@
import { LoggerProxy, WorkflowExecuteMode } from 'n8n-workflow'; import { IRun, LoggerProxy, WorkflowExecuteMode } from 'n8n-workflow';
import { QueryFailedError } from 'typeorm'; import { QueryFailedError, Repository } from 'typeorm';
import { mock } from 'jest-mock-extended';
import config from '@/config'; 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 { nodeFetchedData, workflowExecutionCompleted } from '@/events/WorkflowStatistics';
import { getLogger } from '@/Logger';
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
import { getLogger } from '@/Logger';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { mockInstance } from '../integration/shared/utils'; import { mockInstance } from '../integration/shared/utils';
const FAKE_USER_ID = 'abcde-fghij'; type WorkflowStatisticsRepository = Repository<WorkflowStatistics>;
jest.mock('@/Db', () => { jest.mock('@/Db', () => {
return { return {
collections: { collections: {
WorkflowStatistics: { WorkflowStatistics: mock<WorkflowStatisticsRepository>(),
insert: jest.fn((...args) => {}),
update: jest.fn((...args) => {}),
},
}, },
}; };
}); });
jest.spyOn(UserManagementHelper, 'getWorkflowOwner').mockImplementation(async (_workflowId) => {
return { id: FAKE_USER_ID };
});
describe('Events', () => { describe('Events', () => {
const fakeUser = Object.assign(new User(), { id: 'abcde-fghij' });
const internalHooks = mockInstance(InternalHooks); const internalHooks = mockInstance(InternalHooks);
jest.spyOn(UserManagementHelper, 'getWorkflowOwner').mockResolvedValue(fakeUser);
const workflowStatisticsRepository = Db.collections.WorkflowStatistics as ReturnType<
typeof mock<WorkflowStatisticsRepository>
>;
beforeAll(() => { beforeAll(() => {
config.set('diagnostics.enabled', true); config.set('diagnostics.enabled', true);
config.set('deployment.type', 'n8n-testing'); config.set('deployment.type', 'n8n-testing');
@ -57,8 +62,9 @@ describe('Events', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
}; };
const runData = { const runData: IRun = {
finished: true, finished: true,
status: 'success',
data: { resultData: { runData: {} } }, data: { resultData: { runData: {} } },
mode: 'internal' as WorkflowExecuteMode, mode: 'internal' as WorkflowExecuteMode,
startedAt: new Date(), startedAt: new Date(),
@ -66,7 +72,7 @@ describe('Events', () => {
await workflowExecutionCompleted(workflow, runData); await workflowExecutionCompleted(workflow, runData);
expect(internalHooks.onFirstProductionWorkflowSuccess).toBeCalledTimes(1); expect(internalHooks.onFirstProductionWorkflowSuccess).toBeCalledTimes(1);
expect(internalHooks.onFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, { expect(internalHooks.onFirstProductionWorkflowSuccess).toHaveBeenNthCalledWith(1, {
user_id: FAKE_USER_ID, user_id: fakeUser.id,
workflow_id: workflow.id, workflow_id: workflow.id,
}); });
}); });
@ -82,8 +88,9 @@ describe('Events', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
}; };
const runData = { const runData: IRun = {
finished: false, finished: false,
status: 'failed',
data: { resultData: { runData: {} } }, data: { resultData: { runData: {} } },
mode: 'internal' as WorkflowExecuteMode, mode: 'internal' as WorkflowExecuteMode,
startedAt: new Date(), startedAt: new Date(),
@ -94,7 +101,7 @@ describe('Events', () => {
test('should not send metrics for updated entries', async () => { 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 // 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', [], ''); throw new QueryFailedError('invalid insert', [], '');
}); });
const workflow = { const workflow = {
@ -106,8 +113,9 @@ describe('Events', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
}; };
const runData = { const runData: IRun = {
finished: true, finished: true,
status: 'success',
data: { resultData: { runData: {} } }, data: { resultData: { runData: {} } },
mode: 'internal' as WorkflowExecuteMode, mode: 'internal' as WorkflowExecuteMode,
startedAt: new Date(), startedAt: new Date(),
@ -132,7 +140,7 @@ describe('Events', () => {
await nodeFetchedData(workflowId, node); await nodeFetchedData(workflowId, node);
expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(1); expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(1);
expect(internalHooks.onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, { expect(internalHooks.onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, {
user_id: FAKE_USER_ID, user_id: fakeUser.id,
workflow_id: workflowId, workflow_id: workflowId,
node_type: node.type, node_type: node.type,
node_id: node.id, node_id: node.id,
@ -159,7 +167,7 @@ describe('Events', () => {
await nodeFetchedData(workflowId, node); await nodeFetchedData(workflowId, node);
expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(1); expect(internalHooks.onFirstWorkflowDataLoad).toBeCalledTimes(1);
expect(internalHooks.onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, { expect(internalHooks.onFirstWorkflowDataLoad).toHaveBeenNthCalledWith(1, {
user_id: FAKE_USER_ID, user_id: fakeUser.id,
workflow_id: workflowId, workflow_id: workflowId,
node_type: node.type, node_type: node.type,
node_id: node.id, node_id: node.id,
@ -170,7 +178,7 @@ describe('Events', () => {
test('should not send metrics for entries that already have the flag set', async () => { 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 // 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', [], ''); throw new QueryFailedError('invalid insert', [], '');
}); });
const workflowId = '1'; const workflowId = '1';

View file

@ -1,108 +1,4 @@
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { INodeTypeData } from 'n8n-workflow';
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;
}
/** /**
* Ensure all pending promises settle. The promise's `resolve` is placed in * Ensure all pending promises settle. The promise's `resolve` is placed in

View file

@ -1,46 +1,47 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { import { Container } from 'typedi';
ICredentialTypes, import { ICredentialTypes, INodeTypes, SubworkflowOperationError, Workflow } from 'n8n-workflow';
INodeTypeData,
INodeTypes,
SubworkflowOperationError,
Workflow,
} from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as testDb from '../integration/shared/testDb'; import { Role } from '@db/entities/Role';
import { mockNodeTypesData, NodeTypes as MockNodeTypes } from './Helpers'; 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 { UserService } from '@/user/user.service';
import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
import { WorkflowsService } from '@/workflows/workflows.services'; import { WorkflowsService } from '@/workflows/workflows.services';
import { import {
randomCredentialPayload as randomCred, randomCredentialPayload as randomCred,
randomPositiveDigit, randomPositiveDigit,
} from '../integration/shared/random'; } from '../integration/shared/random';
import * as testDb from '../integration/shared/testDb';
import { Role } from '@db/entities/Role'; import { mockNodeTypesData } from './Helpers';
import type { SaveCredentialFunction } from '../integration/shared/types'; import type { SaveCredentialFunction } from '../integration/shared/types';
import { User } from '@db/entities/User'; import { mockInstance } from '../integration/shared/utils';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
let mockNodeTypes: INodeTypes; let mockNodeTypes: INodeTypes;
let credentialOwnerRole: Role; let credentialOwnerRole: Role;
let workflowOwnerRole: Role; let workflowOwnerRole: Role;
let saveCredential: SaveCredentialFunction; let saveCredential: SaveCredentialFunction;
const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']);
mockInstance(LoadNodesAndCredentials, {
loaded: {
nodes: MOCK_NODE_TYPES_DATA,
credentials: {},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
});
beforeAll(async () => { beforeAll(async () => {
await testDb.init(); await testDb.init();
mockNodeTypes = MockNodeTypes({ mockNodeTypes = Container.get(NodeTypes);
loaded: {
nodes: MOCK_NODE_TYPES_DATA,
credentials: {},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
});
credentialOwnerRole = await testDb.getCredentialOwnerRole(); credentialOwnerRole = await testDb.getCredentialOwnerRole();
workflowOwnerRole = await testDb.getWorkflowOwnerRole(); workflowOwnerRole = await testDb.getWorkflowOwnerRole();
@ -241,7 +242,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
active: false, active: false,
nodeTypes: MockNodeTypes(), nodeTypes: mockNodeTypes,
id: '2', id: '2',
}); });
await expect( await expect(
@ -263,7 +264,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
active: false, active: false,
nodeTypes: MockNodeTypes(), nodeTypes: mockNodeTypes,
id: '2', id: '2',
}); });
await expect( await expect(
@ -301,7 +302,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
active: false, active: false,
nodeTypes: MockNodeTypes(), nodeTypes: mockNodeTypes,
id: '2', id: '2',
settings: { settings: {
callerPolicy: 'workflowsFromAList', callerPolicy: 'workflowsFromAList',
@ -327,7 +328,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
active: false, active: false,
nodeTypes: MockNodeTypes(), nodeTypes: mockNodeTypes,
id: '2', id: '2',
}); });
await expect( await expect(
@ -350,7 +351,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
active: false, active: false,
nodeTypes: MockNodeTypes(), nodeTypes: mockNodeTypes,
id: '2', id: '2',
settings: { settings: {
callerPolicy: 'workflowsFromAList', callerPolicy: 'workflowsFromAList',
@ -376,7 +377,7 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
nodes: [], nodes: [],
connections: {}, connections: {},
active: false, active: false,
nodeTypes: MockNodeTypes(), nodeTypes: mockNodeTypes,
id: '2', id: '2',
settings: { settings: {
callerPolicy: 'any', callerPolicy: 'any',
@ -387,5 +388,3 @@ describe('PermissionChecker.checkSubworkflowExecutePolicy', () => {
).resolves.not.toThrow(); ).resolves.not.toThrow();
}); });
}); });
const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']);

File diff suppressed because it is too large Load diff