ci: Refactor cli tests to speed up CI (no-changelog) (#5718)

* ci: Refactor cli tests to speed up CI (no-changelog)

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

View file

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

View file

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

View file

@ -1,30 +1 @@
/* eslint-disable import/first */
export * from './CredentialsHelper';
export * from './CredentialTypes';
export * from './CredentialsOverwrites';
export * from './Interfaces';
export * from './WaitingWebhooks';
export * from './WorkflowCredentials';
export * from './WorkflowRunner';
import * as Db from './Db';
import * as GenericHelpers from './GenericHelpers';
import * as ResponseHelper from './ResponseHelper';
import * as Server from './Server';
import * as TestWebhooks from './TestWebhooks';
import * as WebhookHelpers from './WebhookHelpers';
import * as WebhookServer from './WebhookServer';
import * as WorkflowExecuteAdditionalData from './WorkflowExecuteAdditionalData';
import * as WorkflowHelpers from './WorkflowHelpers';
export {
Db,
GenericHelpers,
ResponseHelper,
Server,
TestWebhooks,
WebhookHelpers,
WebhookServer,
WorkflowExecuteAdditionalData,
WorkflowHelpers,
};
export {};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,41 +1,36 @@
import express from 'express';
import type { SuperAgentTest } from 'supertest';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils';
import type { User } from '@db/entities/User';
import { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
import { License } from '@/License';
import * as testDb from './shared/testDb';
import * as utils from './shared/utils';
const MOCK_SERVER_URL = 'https://server.com/v1';
const MOCK_RENEW_OFFSET = 259200;
const MOCK_INSTANCE_ID = 'instance-id';
let app: express.Application;
let globalOwnerRole: Role;
let globalMemberRole: Role;
let authAgent: AuthAgent;
let license: License;
let owner: User;
let member: User;
let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest;
beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['license'] });
const app = await utils.initTestServer({ endpointGroups: ['license'] });
globalOwnerRole = await testDb.getGlobalOwnerRole();
globalMemberRole = await testDb.getGlobalMemberRole();
const globalOwnerRole = await testDb.getGlobalOwnerRole();
const globalMemberRole = await testDb.getGlobalMemberRole();
owner = await testDb.createUserShell(globalOwnerRole);
member = await testDb.createUserShell(globalMemberRole);
authAgent = utils.createAuthAgent(app);
const authAgent = utils.createAuthAgent(app);
authOwnerAgent = authAgent(owner);
authMemberAgent = authAgent(member);
config.set('license.serverUrl', MOCK_SERVER_URL);
config.set('license.autoRenewEnabled', true);
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
});
beforeEach(async () => {
license = new License();
await license.init(MOCK_INSTANCE_ID);
});
afterEach(async () => {
await testDb.truncate(['Settings']);
});
@ -44,98 +39,66 @@ afterAll(async () => {
await testDb.terminate();
});
test('GET /license should return license information to the instance owner', async () => {
const userShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(userShell).get('/license');
expect(response.statusCode).toBe(200);
// No license defined so we just expect the result to be the defaults
expect(response.body).toStrictEqual(DEFAULT_LICENSE_RESPONSE);
});
test('GET /license should return license information to a regular user', async () => {
const userShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(userShell).get('/license');
expect(response.statusCode).toBe(200);
// No license defined so we just expect the result to be the defaults
expect(response.body).toStrictEqual(DEFAULT_LICENSE_RESPONSE);
});
test('POST /license/activate should work for instance owner', async () => {
const userShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(userShell)
.post('/license/activate')
.send({ activationKey: 'abcde' });
expect(response.statusCode).toBe(200);
// No license defined so we just expect the result to be the defaults
expect(response.body).toMatchObject(DEFAULT_POST_RESPONSE);
});
test('POST /license/activate does not work for regular users', async () => {
const userShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(userShell)
.post('/license/activate')
.send({ activationKey: 'abcde' });
expect(response.statusCode).toBe(403);
expect(response.body.message).toBe(NON_OWNER_ACTIVATE_RENEW_MESSAGE);
});
test('POST /license/activate errors out properly', async () => {
License.prototype.activate = jest.fn().mockImplementation(() => {
throw new Error(INVALID_ACIVATION_KEY_MESSAGE);
describe('GET /license', () => {
test('should return license information to the instance owner', async () => {
// No license defined so we just expect the result to be the defaults
await authOwnerAgent.get('/license').expect(200, DEFAULT_LICENSE_RESPONSE);
});
const userShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(userShell)
.post('/license/activate')
.send({ activationKey: 'abcde' });
expect(response.statusCode).toBe(400);
expect(response.body.message).toBe(INVALID_ACIVATION_KEY_MESSAGE);
test('should return license information to a regular user', async () => {
// No license defined so we just expect the result to be the defaults
await authMemberAgent.get('/license').expect(200, DEFAULT_LICENSE_RESPONSE);
});
});
test('POST /license/renew should work for instance owner', async () => {
const userShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(userShell).post('/license/renew');
expect(response.statusCode).toBe(200);
// No license defined so we just expect the result to be the defaults
expect(response.body).toMatchObject(DEFAULT_POST_RESPONSE);
});
test('POST /license/renew does not work for regular users', async () => {
const userShell = await testDb.createUserShell(globalMemberRole);
const response = await authAgent(userShell).post('/license/renew');
expect(response.statusCode).toBe(403);
expect(response.body.message).toBe(NON_OWNER_ACTIVATE_RENEW_MESSAGE);
});
test('POST /license/renew errors out properly', async () => {
License.prototype.renew = jest.fn().mockImplementation(() => {
throw new Error(RENEW_ERROR_MESSAGE);
describe('POST /license/activate', () => {
test('should work for instance owner', async () => {
await authOwnerAgent
.post('/license/activate')
.send({ activationKey: 'abcde' })
.expect(200, DEFAULT_POST_RESPONSE);
});
const userShell = await testDb.createUserShell(globalOwnerRole);
test('does not work for regular users', async () => {
await authMemberAgent
.post('/license/activate')
.send({ activationKey: 'abcde' })
.expect(403, { code: 403, message: NON_OWNER_ACTIVATE_RENEW_MESSAGE });
});
const response = await authAgent(userShell).post('/license/renew');
test('errors out properly', async () => {
License.prototype.activate = jest.fn().mockImplementation(() => {
throw new Error(INVALID_ACIVATION_KEY_MESSAGE);
});
expect(response.statusCode).toBe(400);
expect(response.body.message).toBe(RENEW_ERROR_MESSAGE);
await authOwnerAgent
.post('/license/activate')
.send({ activationKey: 'abcde' })
.expect(400, { code: 400, message: INVALID_ACIVATION_KEY_MESSAGE });
});
});
describe('POST /license/renew', () => {
test('should work for instance owner', async () => {
// No license defined so we just expect the result to be the defaults
await authOwnerAgent.post('/license/renew').expect(200, DEFAULT_POST_RESPONSE);
});
test('does not work for regular users', async () => {
await authMemberAgent
.post('/license/renew')
.expect(403, { code: 403, message: NON_OWNER_ACTIVATE_RENEW_MESSAGE });
});
test('errors out properly', async () => {
License.prototype.renew = jest.fn().mockImplementation(() => {
throw new Error(RENEW_ERROR_MESSAGE);
});
await authOwnerAgent
.post('/license/renew')
.expect(400, { code: 400, message: RENEW_ERROR_MESSAGE });
});
});
const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = {

View file

@ -1,10 +1,10 @@
import express from 'express';
import type { Application } from 'express';
import type { SuperAgentTest } from 'supertest';
import { IsNull } from 'typeorm';
import validator from 'validator';
import config from '@/config';
import * as Db from '@/Db';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import {
randomApiKey,
@ -17,7 +17,7 @@ import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils';
let app: express.Application;
let app: Application;
let globalOwnerRole: Role;
let globalMemberRole: Role;
let authAgent: AuthAgent;
@ -40,10 +40,16 @@ afterAll(async () => {
});
describe('Owner shell', () => {
test('PATCH /me should succeed with valid inputs', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = authAgent(ownerShell);
let ownerShell: User;
let authOwnerShellAgent: SuperAgentTest;
beforeEach(async () => {
ownerShell = await testDb.createUserShell(globalOwnerRole);
await testDb.addApiKey(ownerShell);
authOwnerShellAgent = authAgent(ownerShell);
});
test('PATCH /me should succeed with valid inputs', async () => {
for (const validPayload of VALID_PATCH_ME_PAYLOADS) {
const response = await authOwnerShellAgent.patch('/me').send(validPayload);
@ -83,9 +89,6 @@ describe('Owner shell', () => {
});
test('PATCH /me should fail with invalid inputs', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = authAgent(ownerShell);
for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) {
const response = await authOwnerShellAgent.patch('/me').send(invalidPayload);
expect(response.statusCode).toBe(400);
@ -98,9 +101,6 @@ describe('Owner shell', () => {
});
test('PATCH /me/password should fail for shell', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = authAgent(ownerShell);
const validPasswordPayload = {
currentPassword: randomValidPassword(),
newPassword: randomValidPassword(),
@ -130,9 +130,6 @@ describe('Owner shell', () => {
});
test('POST /me/survey should succeed with valid inputs', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerShellAgent = authAgent(ownerShell);
const validPayloads = [SURVEY, {}];
for (const validPayload of validPayloads) {
@ -150,9 +147,7 @@ describe('Owner shell', () => {
});
test('POST /me/api-key should create an api key', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).post('/me/api-key');
const response = await authOwnerShellAgent.post('/me/api-key');
expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).toBeDefined();
@ -166,20 +161,14 @@ describe('Owner shell', () => {
});
test('GET /me/api-key should fetch the api key', async () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole);
ownerShell = await testDb.addApiKey(ownerShell);
const response = await authAgent(ownerShell).get('/me/api-key');
const response = await authOwnerShellAgent.get('/me/api-key');
expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).toEqual(ownerShell.apiKey);
});
test('DELETE /me/api-key should fetch the api key', async () => {
let ownerShell = await testDb.createUserShell(globalOwnerRole);
ownerShell = await testDb.addApiKey(ownerShell);
const response = await authAgent(ownerShell).delete('/me/api-key');
const response = await authOwnerShellAgent.delete('/me/api-key');
expect(response.statusCode).toBe(200);
@ -192,19 +181,22 @@ describe('Owner shell', () => {
});
describe('Member', () => {
beforeEach(async () => {
config.set('userManagement.isInstanceOwnerSetUp', true);
const memberPassword = randomValidPassword();
let member: User;
let authMemberAgent: SuperAgentTest;
await Db.collections.Settings.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: JSON.stringify(true) },
);
beforeEach(async () => {
member = await testDb.createUser({
password: memberPassword,
globalRole: globalMemberRole,
apiKey: randomApiKey(),
});
authMemberAgent = authAgent(member);
await utils.setInstanceOwnerSetUp(true);
});
test('PATCH /me should succeed with valid inputs', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = authAgent(member);
for (const validPayload of VALID_PATCH_ME_PAYLOADS) {
const response = await authMemberAgent.patch('/me').send(validPayload);
@ -244,9 +236,6 @@ describe('Member', () => {
});
test('PATCH /me should fail with invalid inputs', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = authAgent(member);
for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) {
const response = await authMemberAgent.patch('/me').send(invalidPayload);
expect(response.statusCode).toBe(400);
@ -259,18 +248,12 @@ describe('Member', () => {
});
test('PATCH /me/password should succeed with valid inputs', async () => {
const memberPassword = randomValidPassword();
const member = await testDb.createUser({
password: memberPassword,
globalRole: globalMemberRole,
});
const validPayload = {
currentPassword: memberPassword,
newPassword: randomValidPassword(),
};
const response = await authAgent(member).patch('/me/password').send(validPayload);
const response = await authMemberAgent.patch('/me/password').send(validPayload);
expect(response.statusCode).toBe(200);
expect(response.body).toEqual(SUCCESS_RESPONSE_BODY);
@ -280,9 +263,6 @@ describe('Member', () => {
});
test('PATCH /me/password should fail with invalid inputs', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = authAgent(member);
for (const payload of INVALID_PASSWORD_PAYLOADS) {
const response = await authMemberAgent.patch('/me/password').send(payload);
expect([400, 500].includes(response.statusCode)).toBe(true);
@ -299,9 +279,6 @@ describe('Member', () => {
});
test('POST /me/survey should succeed with valid inputs', async () => {
const member = await testDb.createUser({ globalRole: globalMemberRole });
const authMemberAgent = authAgent(member);
const validPayloads = [SURVEY, {}];
for (const validPayload of validPayloads) {
@ -318,11 +295,6 @@ describe('Member', () => {
});
test('POST /me/api-key should create an api key', async () => {
const member = await testDb.createUser({
globalRole: globalMemberRole,
apiKey: randomApiKey(),
});
const response = await authAgent(member).post('/me/api-key');
expect(response.statusCode).toBe(200);
@ -335,11 +307,6 @@ describe('Member', () => {
});
test('GET /me/api-key should fetch the api key', async () => {
const member = await testDb.createUser({
globalRole: globalMemberRole,
apiKey: randomApiKey(),
});
const response = await authAgent(member).get('/me/api-key');
expect(response.statusCode).toBe(200);
@ -347,11 +314,6 @@ describe('Member', () => {
});
test('DELETE /me/api-key should fetch the api key', async () => {
const member = await testDb.createUser({
globalRole: globalMemberRole,
apiKey: randomApiKey(),
});
const response = await authAgent(member).delete('/me/api-key');
expect(response.statusCode).toBe(200);
@ -364,7 +326,7 @@ describe('Member', () => {
describe('Owner', () => {
beforeEach(async () => {
config.set('userManagement.isInstanceOwnerSetUp', true);
await utils.setInstanceOwnerSetUp(true);
});
test('PATCH /me should succeed with valid inputs', async () => {

View file

@ -1,10 +1,6 @@
import path from 'path';
import express from 'express';
import { mocked } from 'jest-mock';
import * as utils from './shared/utils';
import * as testDb from './shared/testDb';
import type { SuperAgentTest } from 'supertest';
import {
executeCommand,
checkNpmPackageStatus,
@ -15,13 +11,13 @@ import {
import { findInstalledPackage, isPackageInstalled } from '@/CommunityNodes/packageModel';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { InstalledPackages } from '@db/entities/InstalledPackages';
import type { Role } from '@db/entities/Role';
import type { AuthAgent } from './shared/types';
import type { User } from '@db/entities/User';
import type { InstalledNodes } from '@db/entities/InstalledNodes';
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
import { NodeTypes } from '@/NodeTypes';
import { Push } from '@/push';
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
import * as utils from './shared/utils';
import * as testDb from './shared/testDb';
const mockLoadNodesAndCredentials = utils.mockInstance(LoadNodesAndCredentials);
utils.mockInstance(NodeTypes);
@ -48,22 +44,21 @@ jest.mock('@/CommunityNodes/packageModel', () => {
const mockedEmptyPackage = mocked(utils.emptyPackage);
let app: express.Application;
let globalOwnerRole: Role;
let authAgent: AuthAgent;
let ownerShell: User;
let authOwnerShellAgent: SuperAgentTest;
beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['nodes'] });
const app = await utils.initTestServer({ endpointGroups: ['nodes'] });
globalOwnerRole = await testDb.getGlobalOwnerRole();
authAgent = utils.createAuthAgent(app);
const globalOwnerRole = await testDb.getGlobalOwnerRole();
ownerShell = await testDb.createUserShell(globalOwnerRole);
authOwnerShellAgent = utils.createAuthAgent(app)(ownerShell);
utils.initConfigFile();
});
beforeEach(async () => {
await testDb.truncate(['InstalledNodes', 'InstalledPackages', 'User']);
await testDb.truncate(['InstalledNodes', 'InstalledPackages']);
mocked(executeCommand).mockReset();
mocked(findInstalledPackage).mockReset();
@ -73,255 +68,216 @@ afterAll(async () => {
await testDb.terminate();
});
/**
* GET /nodes
*/
describe('GET /nodes', () => {
test('should respond 200 if no nodes are installed', async () => {
const {
statusCode,
body: { data },
} = await authOwnerShellAgent.get('/nodes');
test('GET /nodes should respond 200 if no nodes are installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
expect(statusCode).toBe(200);
expect(data).toHaveLength(0);
});
const {
statusCode,
body: { data },
} = await authAgent(ownerShell).get('/nodes');
test('should return list of one installed package and node', async () => {
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
expect(statusCode).toBe(200);
expect(data).toHaveLength(0);
});
const {
statusCode,
body: { data },
} = await authOwnerShellAgent.get('/nodes');
test('GET /nodes should return list of one installed package and node', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
expect(statusCode).toBe(200);
expect(data).toHaveLength(1);
expect(data[0].installedNodes).toHaveLength(1);
});
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
test('should return list of multiple installed packages and nodes', async () => {
const first = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(first.packageName));
const {
statusCode,
body: { data },
} = await authAgent(ownerShell).get('/nodes');
const second = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
expect(statusCode).toBe(200);
expect(data).toHaveLength(1);
expect(data[0].installedNodes).toHaveLength(1);
});
const {
statusCode,
body: { data },
} = await authOwnerShellAgent.get('/nodes');
test('GET /nodes should return list of multiple installed packages and nodes', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
expect(statusCode).toBe(200);
expect(data).toHaveLength(2);
const first = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(first.packageName));
const allNodes = data.reduce(
(acc: InstalledNodes[], cur: InstalledPackages) => acc.concat(cur.installedNodes),
[],
);
const second = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
expect(allNodes).toHaveLength(3);
});
const {
statusCode,
body: { data },
} = await authAgent(ownerShell).get('/nodes');
test('should not check for updates if no packages installed', async () => {
await authOwnerShellAgent.get('/nodes');
expect(statusCode).toBe(200);
expect(data).toHaveLength(2);
expect(mocked(executeCommand)).toHaveBeenCalledTimes(0);
});
const allNodes = data.reduce(
(acc: InstalledNodes[], cur: InstalledPackages) => acc.concat(cur.installedNodes),
[],
);
test('should check for updates if packages installed', async () => {
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
expect(allNodes).toHaveLength(3);
});
await authOwnerShellAgent.get('/nodes');
test('GET /nodes should not check for updates if no packages installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', {
doNotHandleError: true,
});
});
await authAgent(ownerShell).get('/nodes');
test('should report package updates if available', async () => {
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
expect(mocked(executeCommand)).toHaveBeenCalledTimes(0);
});
mocked(executeCommand).mockImplementationOnce(() => {
throw {
code: 1,
stdout: JSON.stringify({
[packageName]: {
current: COMMUNITY_PACKAGE_VERSION.CURRENT,
wanted: COMMUNITY_PACKAGE_VERSION.CURRENT,
latest: COMMUNITY_PACKAGE_VERSION.UPDATED,
location: path.join('node_modules', packageName),
},
}),
};
});
test('GET /nodes should check for updates if packages installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
mocked(isNpmError).mockReturnValueOnce(true);
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
const {
body: { data },
} = await authOwnerShellAgent.get('/nodes');
await authAgent(ownerShell).get('/nodes');
expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', {
doNotHandleError: true,
expect(data[0].installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT);
expect(data[0].updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED);
});
});
test('GET /nodes should report package updates if available', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
describe('POST /nodes', () => {
test('should reject if package name is missing', async () => {
const { statusCode } = await authOwnerShellAgent.post('/nodes');
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
mocked(executeCommand).mockImplementationOnce(() => {
throw {
code: 1,
stdout: JSON.stringify({
[packageName]: {
current: COMMUNITY_PACKAGE_VERSION.CURRENT,
wanted: COMMUNITY_PACKAGE_VERSION.CURRENT,
latest: COMMUNITY_PACKAGE_VERSION.UPDATED,
location: path.join('node_modules', packageName),
},
}),
};
expect(statusCode).toBe(400);
});
mocked(isNpmError).mockReturnValueOnce(true);
test('should reject if package is duplicate', async () => {
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
mocked(isPackageInstalled).mockResolvedValueOnce(true);
mocked(hasPackageLoaded).mockReturnValueOnce(true);
const {
body: { data },
} = await authAgent(ownerShell).get('/nodes');
const {
statusCode,
body: { message },
} = await authOwnerShellAgent.post('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
expect(data[0].installedVersion).toBe(COMMUNITY_PACKAGE_VERSION.CURRENT);
expect(data[0].updateAvailable).toBe(COMMUNITY_PACKAGE_VERSION.UPDATED);
});
/**
* POST /nodes
*/
test('POST /nodes should reject if package name is missing', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const { statusCode } = await authAgent(ownerShell).post('/nodes');
expect(statusCode).toBe(400);
});
test('POST /nodes should reject if package is duplicate', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
mocked(isPackageInstalled).mockResolvedValueOnce(true);
mocked(hasPackageLoaded).mockReturnValueOnce(true);
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).post('/nodes').send({
name: utils.installedPackagePayload().packageName,
expect(statusCode).toBe(400);
expect(message).toContain('already installed');
});
expect(statusCode).toBe(400);
expect(message).toContain('already installed');
});
test('should allow installing packages that could not be loaded', async () => {
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
mocked(hasPackageLoaded).mockReturnValueOnce(false);
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' });
test('POST /nodes should allow installing packages that could not be loaded', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage);
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
mocked(hasPackageLoaded).mockReturnValueOnce(false);
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' });
const { statusCode } = await authOwnerShellAgent.post('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authAgent(ownerShell).post('/nodes').send({
name: utils.installedPackagePayload().packageName,
expect(statusCode).toBe(200);
expect(mocked(removePackageFromMissingList)).toHaveBeenCalled();
});
expect(statusCode).toBe(200);
expect(mocked(removePackageFromMissingList)).toHaveBeenCalled();
test('should not install a banned package', async () => {
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' });
const {
statusCode,
body: { message },
} = await authOwnerShellAgent.post('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
expect(statusCode).toBe(400);
expect(message).toContain('banned');
});
});
test('POST /nodes should not install a banned package', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' });
describe('DELETE /nodes', () => {
test('should not delete if package name is empty', async () => {
const response = await authOwnerShellAgent.delete('/nodes');
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).post('/nodes').send({
name: utils.installedPackagePayload().packageName,
expect(response.statusCode).toBe(400);
});
expect(statusCode).toBe(400);
expect(message).toContain('banned');
});
test('should reject if package is not installed', async () => {
const {
statusCode,
body: { message },
} = await authOwnerShellAgent.delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
});
/**
* DELETE /nodes
*/
test('DELETE /nodes should not delete if package name is empty', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).delete('/nodes');
expect(response.statusCode).toBe(400);
});
test('DELETE /nodes should reject if package is not installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
expect(statusCode).toBe(400);
expect(message).toContain('not installed');
});
expect(statusCode).toBe(400);
expect(message).toContain('not installed');
test('should uninstall package', async () => {
const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn());
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authOwnerShellAgent.delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
});
expect(statusCode).toBe(200);
expect(removeSpy).toHaveBeenCalledTimes(1);
});
});
test('DELETE /nodes should uninstall package', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
describe('PATCH /nodes', () => {
test('should reject if package name is empty', async () => {
const response = await authOwnerShellAgent.patch('/nodes');
const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn());
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authAgent(ownerShell).delete('/nodes').query({
name: utils.installedPackagePayload().packageName,
expect(response.statusCode).toBe(400);
});
expect(statusCode).toBe(200);
expect(removeSpy).toHaveBeenCalledTimes(1);
});
test('reject if package is not installed', async () => {
const {
statusCode,
body: { message },
} = await authOwnerShellAgent.patch('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
/**
* PATCH /nodes
*/
test('PATCH /nodes should reject if package name is empty', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).patch('/nodes');
expect(response.statusCode).toBe(400);
});
test('PATCH /nodes reject if package is not installed', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const {
statusCode,
body: { message },
} = await authAgent(ownerShell).patch('/nodes').send({
name: utils.installedPackagePayload().packageName,
expect(statusCode).toBe(400);
expect(message).toContain('not installed');
});
expect(statusCode).toBe(400);
expect(message).toContain('not installed');
});
test('should update a package', async () => {
const updateSpy =
mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage);
test('PATCH /nodes should update a package', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
const updateSpy =
mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage);
await authOwnerShellAgent.patch('/nodes').send({
name: utils.installedPackagePayload().packageName,
});
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
await authAgent(ownerShell).patch('/nodes').send({
name: utils.installedPackagePayload().packageName,
expect(updateSpy).toHaveBeenCalledTimes(1);
});
expect(updateSpy).toHaveBeenCalledTimes(1);
});

View file

@ -1,9 +1,11 @@
import express from 'express';
import type { Application } from 'express';
import validator from 'validator';
import type { SuperAgentTest } from 'supertest';
import config from '@/config';
import * as Db from '@/Db';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import {
randomEmail,
randomInvalidPassword,
@ -11,23 +13,22 @@ import {
randomValidPassword,
} from './shared/random';
import * as testDb from './shared/testDb';
import type { AuthAgent } from './shared/types';
import * as utils from './shared/utils';
let app: express.Application;
let app: Application;
let globalOwnerRole: Role;
let authAgent: AuthAgent;
let ownerShell: User;
let authOwnerShellAgent: SuperAgentTest;
beforeAll(async () => {
app = await utils.initTestServer({ endpointGroups: ['owner'] });
globalOwnerRole = await testDb.getGlobalOwnerRole();
authAgent = utils.createAuthAgent(app);
});
beforeEach(async () => {
config.set('userManagement.isInstanceOwnerSetUp', false);
ownerShell = await testDb.createUserShell(globalOwnerRole);
authOwnerShellAgent = utils.createAuthAgent(app)(ownerShell);
});
afterEach(async () => {
@ -38,152 +39,149 @@ afterAll(async () => {
await testDb.terminate();
});
test('POST /owner/setup should create owner and enable isInstanceOwnerSetUp', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
describe('POST /owner/setup', () => {
test('should create owner and enable isInstanceOwnerSetUp', async () => {
const newOwnerData = {
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const newOwnerData = {
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData);
const response = await authAgent(ownerShell).post('/owner/setup').send(newOwnerData);
expect(response.statusCode).toBe(200);
expect(response.statusCode).toBe(200);
const {
id,
email,
firstName,
lastName,
personalizationAnswers,
globalRole,
password,
resetPasswordToken,
isPending,
apiKey,
} = response.body.data;
const {
id,
email,
firstName,
lastName,
personalizationAnswers,
globalRole,
password,
resetPasswordToken,
isPending,
apiKey,
} = response.body.data;
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(newOwnerData.email);
expect(firstName).toBe(newOwnerData.firstName);
expect(lastName).toBe(newOwnerData.lastName);
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(resetPasswordToken).toBeUndefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
expect(validator.isUUID(id)).toBe(true);
expect(email).toBe(newOwnerData.email);
expect(firstName).toBe(newOwnerData.firstName);
expect(lastName).toBe(newOwnerData.lastName);
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(resetPasswordToken).toBeUndefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(apiKey).toBeUndefined();
const storedOwner = await Db.collections.User.findOneByOrFail({ id });
expect(storedOwner.password).not.toBe(newOwnerData.password);
expect(storedOwner.email).toBe(newOwnerData.email);
expect(storedOwner.firstName).toBe(newOwnerData.firstName);
expect(storedOwner.lastName).toBe(newOwnerData.lastName);
const storedOwner = await Db.collections.User.findOneByOrFail({ id });
expect(storedOwner.password).not.toBe(newOwnerData.password);
expect(storedOwner.email).toBe(newOwnerData.email);
expect(storedOwner.firstName).toBe(newOwnerData.firstName);
expect(storedOwner.lastName).toBe(newOwnerData.lastName);
const isInstanceOwnerSetUpConfig = config.getEnv('userManagement.isInstanceOwnerSetUp');
expect(isInstanceOwnerSetUpConfig).toBe(true);
const isInstanceOwnerSetUpConfig = config.getEnv('userManagement.isInstanceOwnerSetUp');
expect(isInstanceOwnerSetUpConfig).toBe(true);
const isInstanceOwnerSetUpSetting = await utils.isInstanceOwnerSetUp();
expect(isInstanceOwnerSetUpSetting).toBe(true);
});
test('POST /owner/setup should create owner with lowercased email', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const newOwnerData = {
email: randomEmail().toUpperCase(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const response = await authAgent(ownerShell).post('/owner/setup').send(newOwnerData);
expect(response.statusCode).toBe(200);
const { id, email } = response.body.data;
expect(id).toBe(ownerShell.id);
expect(email).toBe(newOwnerData.email.toLowerCase());
const storedOwner = await Db.collections.User.findOneByOrFail({ id });
expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase());
});
test('POST /owner/setup should fail with invalid inputs', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const authOwnerAgent = authAgent(ownerShell);
await Promise.all(
INVALID_POST_OWNER_PAYLOADS.map(async (invalidPayload) => {
const response = await authOwnerAgent.post('/owner/setup').send(invalidPayload);
expect(response.statusCode).toBe(400);
}),
);
});
test('POST /owner/skip-setup should persist skipping setup to the DB', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const response = await authAgent(ownerShell).post('/owner/skip-setup').send();
expect(response.statusCode).toBe(200);
const skipConfig = config.getEnv('userManagement.skipInstanceOwnerSetup');
expect(skipConfig).toBe(true);
const { value } = await Db.collections.Settings.findOneByOrFail({
key: 'userManagement.skipInstanceOwnerSetup',
const isInstanceOwnerSetUpSetting = await utils.isInstanceOwnerSetUp();
expect(isInstanceOwnerSetUpSetting).toBe(true);
});
test('should create owner with lowercased email', async () => {
const newOwnerData = {
email: randomEmail().toUpperCase(),
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
};
const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData);
expect(response.statusCode).toBe(200);
const { id, email } = response.body.data;
expect(id).toBe(ownerShell.id);
expect(email).toBe(newOwnerData.email.toLowerCase());
const storedOwner = await Db.collections.User.findOneByOrFail({ id });
expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase());
});
const INVALID_POST_OWNER_PAYLOADS = [
{
email: '',
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
},
{
email: randomEmail(),
firstName: '',
lastName: randomName(),
password: randomValidPassword(),
},
{
email: randomEmail(),
firstName: randomName(),
lastName: '',
password: randomValidPassword(),
},
{
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomInvalidPassword(),
},
{
firstName: randomName(),
lastName: randomName(),
},
{
firstName: randomName(),
},
{
lastName: randomName(),
},
{
email: randomEmail(),
firstName: 'John <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 = [
{
email: '',
firstName: randomName(),
lastName: randomName(),
password: randomValidPassword(),
},
{
email: randomEmail(),
firstName: '',
lastName: randomName(),
password: randomValidPassword(),
},
{
email: randomEmail(),
firstName: randomName(),
lastName: '',
password: randomValidPassword(),
},
{
email: randomEmail(),
firstName: randomName(),
lastName: randomName(),
password: randomInvalidPassword(),
},
{
firstName: randomName(),
lastName: randomName(),
},
{
firstName: randomName(),
},
{
lastName: randomName(),
},
{
email: randomEmail(),
firstName: 'John <script',
lastName: randomName(),
},
{
email: randomEmail(),
firstName: 'John <a',
lastName: randomName(),
},
];
describe('POST /owner/skip-setup', () => {
test('should persist skipping setup to the DB', async () => {
const response = await authOwnerShellAgent.post('/owner/skip-setup').send();
expect(response.statusCode).toBe(200);
const skipConfig = config.getEnv('userManagement.skipInstanceOwnerSetup');
expect(skipConfig).toBe(true);
const { value } = await Db.collections.Settings.findOneByOrFail({
key: 'userManagement.skipInstanceOwnerSetup',
});
expect(value).toBe('true');
});
});

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -1,108 +1,4 @@
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import {
INodeType,
INodeTypeData,
INodeTypes,
IVersionedNodeType,
NodeHelpers,
} from 'n8n-workflow';
// TODO: delete this
class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypeData = {
'test.set': {
sourcePath: '',
type: {
description: {
displayName: 'Set',
name: 'set',
group: ['input'],
version: 1,
description: 'Sets a value',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Value1',
name: 'value1',
type: 'string',
default: 'default-value1',
},
{
displayName: 'Value2',
name: 'value2',
type: 'string',
default: 'default-value2',
},
],
},
},
},
'fake-scheduler': {
sourcePath: '',
type: {
description: {
displayName: 'Schedule',
name: 'set',
group: ['input'],
version: 1,
description: 'Schedules execuitons',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Value1',
name: 'value1',
type: 'string',
default: 'default-value1',
},
{
displayName: 'Value2',
name: 'value2',
type: 'string',
default: 'default-value2',
},
],
},
trigger: () => {
return Promise.resolve(undefined);
},
},
},
};
constructor(nodesAndCredentials?: LoadNodesAndCredentials) {
if (nodesAndCredentials?.loaded?.nodes) {
this.nodeTypes = nodesAndCredentials?.loaded?.nodes;
}
}
getByName(nodeType: string): INodeType | IVersionedNodeType {
return this.nodeTypes[nodeType].type;
}
getByNameAndVersion(nodeType: string, version?: number): INodeType {
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
}
}
let nodeTypesInstance: NodeTypesClass | undefined;
export function NodeTypes(nodesAndCredentials?: LoadNodesAndCredentials): NodeTypesClass {
if (nodeTypesInstance === undefined) {
nodeTypesInstance = new NodeTypesClass(nodesAndCredentials);
}
return nodeTypesInstance;
}
import { INodeTypeData } from 'n8n-workflow';
/**
* Ensure all pending promises settle. The promise's `resolve` is placed in

View file

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

File diff suppressed because it is too large Load diff