mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 08:34:07 -08:00
5156313074
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
357 lines
10 KiB
TypeScript
357 lines
10 KiB
TypeScript
import { mock } from 'jest-mock-extended';
|
|
import { Cipher } from 'n8n-core';
|
|
import type { InstanceSettings } from 'n8n-core';
|
|
import * as utils from 'n8n-workflow';
|
|
import { nanoid } from 'nanoid';
|
|
import fsp from 'node:fs/promises';
|
|
import Container from 'typedi';
|
|
|
|
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
|
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
|
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
|
import { SourceControlImportService } from '@/environments/source-control/source-control-import.service.ee';
|
|
import type { ExportableCredential } from '@/environments/source-control/types/exportable-credential';
|
|
import type { SourceControlledFile } from '@/environments/source-control/types/source-controlled-file';
|
|
|
|
import { mockInstance } from '../../shared/mocking';
|
|
import { saveCredential } from '../shared/db/credentials';
|
|
import { createTeamProject, getPersonalProject } from '../shared/db/projects';
|
|
import { createMember, getGlobalOwner } from '../shared/db/users';
|
|
import { randomCredentialPayload } from '../shared/random';
|
|
import * as testDb from '../shared/test-db';
|
|
|
|
describe('SourceControlImportService', () => {
|
|
let service: SourceControlImportService;
|
|
const cipher = mockInstance(Cipher);
|
|
|
|
beforeAll(async () => {
|
|
service = new SourceControlImportService(
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock(),
|
|
mock<InstanceSettings>({ n8nFolder: '/some-path' }),
|
|
);
|
|
|
|
await testDb.init();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await testDb.truncate(['Credentials', 'SharedCredentials']);
|
|
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await testDb.terminate();
|
|
});
|
|
|
|
describe('importCredentialsFromWorkFolder()', () => {
|
|
describe('if user email specified by `ownedBy` exists at target instance', () => {
|
|
it('should assign credential ownership to original user', async () => {
|
|
const [importingUser, member] = await Promise.all([getGlobalOwner(), createMember()]);
|
|
|
|
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
|
|
|
const CREDENTIAL_ID = nanoid();
|
|
|
|
const stub: ExportableCredential = {
|
|
id: CREDENTIAL_ID,
|
|
name: 'My Credential',
|
|
type: 'someCredentialType',
|
|
data: {},
|
|
ownedBy: member.email, // user at source instance owns credential
|
|
};
|
|
|
|
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
|
|
|
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
|
|
|
await service.importCredentialsFromWorkFolder(
|
|
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
|
importingUser.id,
|
|
);
|
|
|
|
const personalProject = await getPersonalProject(member);
|
|
|
|
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
|
credentialsId: CREDENTIAL_ID,
|
|
projectId: personalProject.id,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
expect(sharing).toBeTruthy(); // same user at target instance owns credential
|
|
});
|
|
});
|
|
|
|
describe('if user email specified by `ownedBy` is `null`', () => {
|
|
it('should assign credential ownership to importing user', async () => {
|
|
const importingUser = await getGlobalOwner();
|
|
|
|
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
|
|
|
const CREDENTIAL_ID = nanoid();
|
|
|
|
const stub: ExportableCredential = {
|
|
id: CREDENTIAL_ID,
|
|
name: 'My Credential',
|
|
type: 'someCredentialType',
|
|
data: {},
|
|
ownedBy: null,
|
|
};
|
|
|
|
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
|
|
|
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
|
|
|
await service.importCredentialsFromWorkFolder(
|
|
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
|
importingUser.id,
|
|
);
|
|
|
|
const personalProject = await getPersonalProject(importingUser);
|
|
|
|
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
|
credentialsId: CREDENTIAL_ID,
|
|
projectId: personalProject.id,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
expect(sharing).toBeTruthy(); // original user has no email, so importing user owns credential
|
|
});
|
|
});
|
|
|
|
describe('if user email specified by `ownedBy` does not exist at target instance', () => {
|
|
it('should assign credential ownership to importing user', async () => {
|
|
const importingUser = await getGlobalOwner();
|
|
|
|
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
|
|
|
const CREDENTIAL_ID = nanoid();
|
|
|
|
const stub: ExportableCredential = {
|
|
id: CREDENTIAL_ID,
|
|
name: 'My Credential',
|
|
type: 'someCredentialType',
|
|
data: {},
|
|
ownedBy: 'user@test.com', // user at source instance owns credential
|
|
};
|
|
|
|
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
|
|
|
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
|
|
|
await service.importCredentialsFromWorkFolder(
|
|
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
|
importingUser.id,
|
|
);
|
|
|
|
const personalProject = await getPersonalProject(importingUser);
|
|
|
|
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
|
credentialsId: CREDENTIAL_ID,
|
|
projectId: personalProject.id,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
expect(sharing).toBeTruthy(); // original user missing, so importing user owns credential
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('if owner specified by `ownedBy` does not exist at target instance', () => {
|
|
it('should assign the credential ownership to the importing user if it was owned by a personal project in the source instance', async () => {
|
|
const importingUser = await getGlobalOwner();
|
|
|
|
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
|
|
|
const CREDENTIAL_ID = nanoid();
|
|
|
|
const stub: ExportableCredential = {
|
|
id: CREDENTIAL_ID,
|
|
name: 'My Credential',
|
|
type: 'someCredentialType',
|
|
data: {},
|
|
ownedBy: {
|
|
type: 'personal',
|
|
personalEmail: 'test@example.com',
|
|
}, // user at source instance owns credential
|
|
};
|
|
|
|
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
|
|
|
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
|
|
|
await service.importCredentialsFromWorkFolder(
|
|
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
|
importingUser.id,
|
|
);
|
|
|
|
const personalProject = await getPersonalProject(importingUser);
|
|
|
|
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
|
credentialsId: CREDENTIAL_ID,
|
|
projectId: personalProject.id,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
expect(sharing).toBeTruthy(); // original user missing, so importing user owns credential
|
|
});
|
|
|
|
it('should create a new team project if the credential was owned by a team project in the source instance', async () => {
|
|
const importingUser = await getGlobalOwner();
|
|
|
|
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
|
|
|
const CREDENTIAL_ID = nanoid();
|
|
|
|
const stub: ExportableCredential = {
|
|
id: CREDENTIAL_ID,
|
|
name: 'My Credential',
|
|
type: 'someCredentialType',
|
|
data: {},
|
|
ownedBy: {
|
|
type: 'team',
|
|
teamId: '1234-asdf',
|
|
teamName: 'Marketing',
|
|
}, // user at source instance owns credential
|
|
};
|
|
|
|
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
|
|
|
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
|
|
|
{
|
|
const project = await Container.get(ProjectRepository).findOne({
|
|
where: [
|
|
{
|
|
id: '1234-asdf',
|
|
},
|
|
{ name: 'Marketing' },
|
|
],
|
|
});
|
|
|
|
expect(project?.id).not.toBe('1234-asdf');
|
|
expect(project?.name).not.toBe('Marketing');
|
|
}
|
|
|
|
await service.importCredentialsFromWorkFolder(
|
|
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
|
importingUser.id,
|
|
);
|
|
|
|
const sharing = await Container.get(SharedCredentialsRepository).findOne({
|
|
where: {
|
|
credentialsId: CREDENTIAL_ID,
|
|
role: 'credential:owner',
|
|
},
|
|
relations: { project: true },
|
|
});
|
|
|
|
expect(sharing?.project.id).toBe('1234-asdf');
|
|
expect(sharing?.project.name).toBe('Marketing');
|
|
expect(sharing?.project.type).toBe('team');
|
|
|
|
expect(sharing).toBeTruthy(); // original user missing, so importing user owns credential
|
|
});
|
|
});
|
|
|
|
describe('if owner specified by `ownedBy` does exist at target instance', () => {
|
|
it('should use the existing team project if credential owning project is found', async () => {
|
|
const importingUser = await getGlobalOwner();
|
|
|
|
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
|
|
|
const CREDENTIAL_ID = nanoid();
|
|
|
|
const project = await createTeamProject('Sales');
|
|
|
|
const stub: ExportableCredential = {
|
|
id: CREDENTIAL_ID,
|
|
name: 'My Credential',
|
|
type: 'someCredentialType',
|
|
data: {},
|
|
ownedBy: {
|
|
type: 'team',
|
|
teamId: project.id,
|
|
teamName: 'Sales',
|
|
},
|
|
};
|
|
|
|
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
|
|
|
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
|
|
|
await service.importCredentialsFromWorkFolder(
|
|
[mock<SourceControlledFile>({ id: CREDENTIAL_ID })],
|
|
importingUser.id,
|
|
);
|
|
|
|
const sharing = await Container.get(SharedCredentialsRepository).findOneBy({
|
|
credentialsId: CREDENTIAL_ID,
|
|
projectId: project.id,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
expect(sharing).toBeTruthy();
|
|
});
|
|
|
|
it('should not change the owner if the credential is owned by somebody else on the target instance', async () => {
|
|
cipher.encrypt.mockReturnValue('some-encrypted-data');
|
|
|
|
const importingUser = await getGlobalOwner();
|
|
|
|
fsp.readFile = jest.fn().mockResolvedValue(Buffer.from('some-content'));
|
|
|
|
const targetProject = await createTeamProject('Marketing');
|
|
const credential = await saveCredential(randomCredentialPayload(), {
|
|
project: targetProject,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
const sourceProjectId = nanoid();
|
|
|
|
const stub: ExportableCredential = {
|
|
id: credential.id,
|
|
name: 'My Credential',
|
|
type: 'someCredentialType',
|
|
data: {},
|
|
ownedBy: {
|
|
type: 'team',
|
|
teamId: sourceProjectId,
|
|
teamName: 'Sales',
|
|
},
|
|
};
|
|
|
|
jest.spyOn(utils, 'jsonParse').mockReturnValue(stub);
|
|
|
|
await service.importCredentialsFromWorkFolder(
|
|
[mock<SourceControlledFile>({ id: credential.id })],
|
|
importingUser.id,
|
|
);
|
|
|
|
await expect(
|
|
Container.get(SharedCredentialsRepository).findBy({
|
|
credentialsId: credential.id,
|
|
}),
|
|
).resolves.toMatchObject([
|
|
{
|
|
projectId: targetProject.id,
|
|
role: 'credential:owner',
|
|
},
|
|
]);
|
|
await expect(
|
|
Container.get(CredentialsRepository).findBy({
|
|
id: credential.id,
|
|
}),
|
|
).resolves.toMatchObject([
|
|
{
|
|
name: stub.name,
|
|
type: stub.type,
|
|
data: 'some-encrypted-data',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|