mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 00:54:06 -08:00
fdac2c8572
PR adds a new field to the SourceControlPreferences as well as to the POST parameters for the `source-control/preferences` and `source-control/generate-key-pair` endpoints. Both now accept an optional string parameter `keyGeneratorType` of `'ed25519' | 'rsa'` Calling the `source-control/generate-key-pair` endpoint with the parameter set, it will also update the stored preferences accordingly (so that in the future new keys will use the same method) By default ed25519 is being used. The default may be changed using a new environment parameter: `N8N_SOURCECONTROL_DEFAULT_SSH_KEY_TYPE` which can be `rsa` or `ed25519` RSA keys are generated with a length of 4096 bytes.
266 lines
7.5 KiB
TypeScript
266 lines
7.5 KiB
TypeScript
import Container from 'typedi';
|
|
import {
|
|
generateSshKeyPair,
|
|
getRepoType,
|
|
getTrackingInformationFromPostPushResult,
|
|
getTrackingInformationFromPrePushResult,
|
|
getTrackingInformationFromPullResult,
|
|
sourceControlFoldersExistCheck,
|
|
} from '@/environments/sourceControl/sourceControlHelper.ee';
|
|
import { License } from '@/License';
|
|
import { SourceControlPreferencesService } from '@/environments/sourceControl/sourceControlPreferences.service.ee';
|
|
import { UserSettings } from 'n8n-core';
|
|
import path from 'path';
|
|
import {
|
|
SOURCE_CONTROL_SSH_FOLDER,
|
|
SOURCE_CONTROL_GIT_FOLDER,
|
|
SOURCE_CONTROL_SSH_KEY_NAME,
|
|
} from '@/environments/sourceControl/constants';
|
|
import { LoggerProxy } from 'n8n-workflow';
|
|
import { getLogger } from '@/Logger';
|
|
import { constants as fsConstants, accessSync } from 'fs';
|
|
import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile';
|
|
import type { SourceControlPreferences } from '@/environments/sourceControl/types/sourceControlPreferences';
|
|
|
|
const pushResult: SourceControlledFile[] = [
|
|
{
|
|
file: 'credential_stubs/kkookWGIeey9K4Kt.json',
|
|
id: 'kkookWGIeey9K4Kt',
|
|
name: '(deleted)',
|
|
type: 'credential',
|
|
status: 'deleted',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '',
|
|
pushed: true,
|
|
},
|
|
{
|
|
file: 'variable_stubs.json',
|
|
id: 'variables',
|
|
name: 'variables',
|
|
type: 'variables',
|
|
status: 'modified',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '',
|
|
pushed: true,
|
|
},
|
|
{
|
|
file: 'workflows/BpFS26gViuGqrIVP.json',
|
|
id: 'BpFS26gViuGqrIVP',
|
|
name: 'My workflow 5',
|
|
type: 'workflow',
|
|
status: 'modified',
|
|
location: 'remote',
|
|
conflict: true,
|
|
pushed: true,
|
|
updatedAt: '2023-07-10T10:10:59.000Z',
|
|
},
|
|
{
|
|
file: 'workflows/BpFS26gViuGqrIVP.json',
|
|
id: 'BpFS26gViuGqrIVP',
|
|
name: 'My workflow 5',
|
|
type: 'workflow',
|
|
status: 'modified',
|
|
location: 'local',
|
|
conflict: true,
|
|
updatedAt: '2023-07-10T10:10:59.000Z',
|
|
},
|
|
{
|
|
file: 'workflows/dAU6dNthm4TR3gXx.json',
|
|
id: 'dAU6dNthm4TR3gXx',
|
|
name: 'My workflow 7',
|
|
type: 'workflow',
|
|
status: 'created',
|
|
location: 'local',
|
|
conflict: false,
|
|
pushed: true,
|
|
updatedAt: '2023-07-10T10:02:45.186Z',
|
|
},
|
|
{
|
|
file: 'workflows/haQetoXq9GxHSkft.json',
|
|
id: 'haQetoXq9GxHSkft',
|
|
name: 'My workflow 6',
|
|
type: 'workflow',
|
|
status: 'created',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '2023-07-10T10:02:39.276Z',
|
|
},
|
|
];
|
|
|
|
const pullResult: SourceControlledFile[] = [
|
|
{
|
|
file: 'credential_stubs/kkookWGIeey9K4Kt.json',
|
|
id: 'kkookWGIeey9K4Kt',
|
|
name: '(deleted)',
|
|
type: 'credential',
|
|
status: 'deleted',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '',
|
|
},
|
|
{
|
|
file: 'credential_stubs/abcdeWGIeey9K4aa.json',
|
|
id: 'abcdeWGIeey9K4aa',
|
|
name: 'modfied credential',
|
|
type: 'credential',
|
|
status: 'modified',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '',
|
|
},
|
|
{
|
|
file: 'workflows/BpFS26gViuGqrIVP.json',
|
|
id: 'BpFS26gViuGqrIVP',
|
|
name: '(deleted)',
|
|
type: 'workflow',
|
|
status: 'deleted',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '',
|
|
},
|
|
{
|
|
file: 'variable_stubs.json',
|
|
id: 'variables',
|
|
name: 'variables',
|
|
type: 'variables',
|
|
status: 'modified',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '',
|
|
},
|
|
{
|
|
file: 'workflows/dAU6dNthm4TR3gXx.json',
|
|
id: 'dAU6dNthm4TR3gXx',
|
|
name: 'My workflow 7',
|
|
type: 'workflow',
|
|
status: 'created',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '2023-07-10T10:02:45.186Z',
|
|
},
|
|
{
|
|
file: 'workflows/haQetoXq9GxHSkft.json',
|
|
id: 'haQetoXq9GxHSkft',
|
|
name: 'My workflow 6',
|
|
type: 'workflow',
|
|
status: 'modified',
|
|
location: 'local',
|
|
conflict: false,
|
|
updatedAt: '2023-07-10T10:02:39.276Z',
|
|
},
|
|
];
|
|
|
|
beforeAll(async () => {
|
|
LoggerProxy.init(getLogger());
|
|
Container.get(License).isSourceControlLicensed = () => true;
|
|
Container.get(SourceControlPreferencesService).getPreferences = () => ({
|
|
branchName: 'main',
|
|
connected: true,
|
|
repositoryUrl: 'git@example.com:n8ntest/n8n_testrepo.git',
|
|
branchReadOnly: false,
|
|
branchColor: '#5296D6',
|
|
publicKey:
|
|
'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBSz2nMZAiUBWe6n89aWd5x9QMcIOaznVW3fpuCYC4L n8n deploy key',
|
|
});
|
|
});
|
|
|
|
describe('Source Control', () => {
|
|
it('should generate an SSH key pair', async () => {
|
|
const keyPair = await generateSshKeyPair('ed25519');
|
|
expect(keyPair.privateKey).toBeTruthy();
|
|
expect(keyPair.privateKey).toContain('BEGIN OPENSSH PRIVATE KEY');
|
|
expect(keyPair.publicKey).toBeTruthy();
|
|
expect(keyPair.publicKey).toContain('ssh-ed25519');
|
|
});
|
|
|
|
it('should generate an RSA key pair', async () => {
|
|
const keyPair = await generateSshKeyPair('rsa');
|
|
expect(keyPair.privateKey).toBeTruthy();
|
|
expect(keyPair.privateKey).toContain('BEGIN OPENSSH PRIVATE KEY');
|
|
expect(keyPair.publicKey).toBeTruthy();
|
|
expect(keyPair.publicKey).toContain('ssh-rsa');
|
|
});
|
|
|
|
it('should check for git and ssh folders and create them if required', async () => {
|
|
const userFolder = UserSettings.getUserN8nFolderPath();
|
|
const sshFolder = path.join(userFolder, SOURCE_CONTROL_SSH_FOLDER);
|
|
const gitFolder = path.join(userFolder, SOURCE_CONTROL_GIT_FOLDER);
|
|
const sshKeyName = path.join(sshFolder, SOURCE_CONTROL_SSH_KEY_NAME);
|
|
let hasThrown = false;
|
|
try {
|
|
accessSync(sshFolder, fsConstants.F_OK);
|
|
} catch (error) {
|
|
hasThrown = true;
|
|
}
|
|
expect(hasThrown).toBeTruthy();
|
|
hasThrown = false;
|
|
try {
|
|
accessSync(gitFolder, fsConstants.F_OK);
|
|
} catch (error) {
|
|
hasThrown = true;
|
|
}
|
|
expect(hasThrown).toBeTruthy();
|
|
// create missing folders
|
|
expect(sourceControlFoldersExistCheck([gitFolder, sshFolder], true)).toBe(false);
|
|
// find folders this time
|
|
expect(sourceControlFoldersExistCheck([gitFolder, sshFolder], true)).toBe(true);
|
|
expect(accessSync(sshFolder, fsConstants.F_OK)).toBeUndefined();
|
|
expect(accessSync(gitFolder, fsConstants.F_OK)).toBeUndefined();
|
|
});
|
|
|
|
it('should check if source control is licensed', async () => {
|
|
expect(Container.get(License).isSourceControlLicensed()).toBe(true);
|
|
});
|
|
|
|
it('should get repo type from url', async () => {
|
|
expect(getRepoType('git@github.com:n8ntest/n8n_testrepo.git')).toBe('github');
|
|
expect(getRepoType('git@gitlab.com:n8ntest/n8n_testrepo.git')).toBe('gitlab');
|
|
expect(getRepoType('git@mygitea.io:n8ntest/n8n_testrepo.git')).toBe('other');
|
|
});
|
|
|
|
it('should get tracking information from pre-push results', () => {
|
|
const trackingResult = getTrackingInformationFromPrePushResult(pushResult);
|
|
expect(trackingResult).toEqual({
|
|
workflows_eligible: 3,
|
|
workflows_eligible_with_conflicts: 1,
|
|
creds_eligible: 1,
|
|
creds_eligible_with_conflicts: 0,
|
|
variables_eligible: 1,
|
|
});
|
|
});
|
|
|
|
it('should get tracking information from post-push results', () => {
|
|
const trackingResult = getTrackingInformationFromPostPushResult(pushResult);
|
|
expect(trackingResult).toEqual({
|
|
workflows_pushed: 2,
|
|
workflows_eligible: 3,
|
|
creds_pushed: 1,
|
|
variables_pushed: 1,
|
|
});
|
|
});
|
|
|
|
it('should get tracking information from pull results', () => {
|
|
const trackingResult = getTrackingInformationFromPullResult(pullResult);
|
|
expect(trackingResult).toEqual({
|
|
cred_conflicts: 1,
|
|
workflow_conflicts: 1,
|
|
workflow_updates: 3,
|
|
});
|
|
});
|
|
|
|
it('should class validate correct preferences', async () => {
|
|
const validPreferences: Partial<SourceControlPreferences> = {
|
|
branchName: 'main',
|
|
repositoryUrl: 'git@example.com:n8ntest/n8n_testrepo.git',
|
|
branchReadOnly: false,
|
|
branchColor: '#5296D6',
|
|
};
|
|
const validationResult = await Container.get(
|
|
SourceControlPreferencesService,
|
|
).validateSourceControlPreferences(validPreferences);
|
|
expect(validationResult).toBeTruthy();
|
|
});
|
|
});
|