first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
import { getPersonalProject, mockInstance, testDb } from '@n8n/backend-test-utils';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import '@/zod-alias-support';
|
||||
import { ImportCredentialsCommand } from '@/commands/import/credentials';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
import { getAllCredentials, getAllSharedCredentials } from '../shared/db/credentials';
|
||||
import { createMember, createOwner } from '../shared/db/users';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const command = setupTestCommand(ImportCredentialsCommand);
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['CredentialsEntity', 'SharedCredentials', 'User']);
|
||||
});
|
||||
|
||||
test('import:credentials should import a credential', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--input=./test/integration/commands/import-credentials/credentials.json']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
expect(after).toMatchObject({
|
||||
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
projectId: ownerProject.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('import:credentials should import a credential from separated files', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// import credential the first time, assigning it to the owner
|
||||
await command.run([
|
||||
'--separate',
|
||||
'--input=./test/integration/commands/import-credentials/separate',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
|
||||
expect(after).toMatchObject({
|
||||
credentials: [
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
name: 'cred-aws-test',
|
||||
}),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
projectId: ownerProject.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('`import:credentials --userId ...` should fail if the credential exists already and is owned by somebody else', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
const member = await createMember();
|
||||
|
||||
// import credential the first time, assigning it to the owner
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-credentials/credentials.json',
|
||||
`--userId=${owner.id}`,
|
||||
]);
|
||||
|
||||
// making sure the import worked
|
||||
const before = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
expect(before).toMatchObject({
|
||||
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
projectId: ownerProject.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
|
||||
// Import again while updating the name we try to assign the
|
||||
// credential to another user.
|
||||
await expect(
|
||||
command.run([
|
||||
'--input=./test/integration/commands/import-credentials/credentials-updated.json',
|
||||
`--userId=${member.id}`,
|
||||
]),
|
||||
).rejects.toThrowError(
|
||||
`The credential with ID "123" is already owned by the user with the ID "${owner.id}". It can't be re-owned by the user with the ID "${member.id}"`,
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
|
||||
expect(after).toMatchObject({
|
||||
credentials: [
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
// only the name was updated
|
||||
name: 'cred-aws-test',
|
||||
}),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
projectId: ownerProject.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("only update credential, don't create or update owner if neither `--userId` nor `--projectId` is passed", async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
await createOwner();
|
||||
const member = await createMember();
|
||||
const memberProject = await getPersonalProject(member);
|
||||
|
||||
// import credential the first time, assigning it to a member
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-credentials/credentials.json',
|
||||
`--userId=${member.id}`,
|
||||
]);
|
||||
|
||||
// making sure the import worked
|
||||
const before = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
expect(before).toMatchObject({
|
||||
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
projectId: memberProject.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// Import again only updating the name and omitting `--userId`
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-credentials/credentials-updated.json',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
|
||||
expect(after).toMatchObject({
|
||||
credentials: [
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
// only the name was updated
|
||||
name: 'cred-aws-prod',
|
||||
}),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
projectId: memberProject.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('`import:credential --projectId ...` should fail if the credential already exists and is owned by another project', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
const member = await createMember();
|
||||
const memberProject = await getPersonalProject(member);
|
||||
|
||||
// import credential the first time, assigning it to the owner
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-credentials/credentials.json',
|
||||
`--userId=${owner.id}`,
|
||||
]);
|
||||
|
||||
// making sure the import worked
|
||||
const before = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
expect(before).toMatchObject({
|
||||
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
projectId: ownerProject.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
|
||||
// Import again while updating the name we try to assign the
|
||||
// credential to another user.
|
||||
await expect(
|
||||
command.run([
|
||||
'--input=./test/integration/commands/import-credentials/credentials-updated.json',
|
||||
`--projectId=${memberProject.id}`,
|
||||
]),
|
||||
).rejects.toThrowError(
|
||||
`The credential with ID "123" is already owned by the user with the ID "${owner.id}". It can't be re-owned by the project with the ID "${memberProject.id}".`,
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
credentials: await getAllCredentials(),
|
||||
sharings: await getAllSharedCredentials(),
|
||||
};
|
||||
|
||||
expect(after).toMatchObject({
|
||||
credentials: [
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
// only the name was updated
|
||||
name: 'cred-aws-test',
|
||||
}),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
credentialsId: '123',
|
||||
projectId: ownerProject.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('`import:credential --projectId ... --userId ...` fails explaining that only one of the options can be used at a time', async () => {
|
||||
await expect(
|
||||
command.run([
|
||||
'--input=./test/integration/commands/import-credentials/credentials-updated.json',
|
||||
`--projectId=${nanoid()}`,
|
||||
`--userId=${nanoid()}`,
|
||||
]),
|
||||
).rejects.toThrowError(
|
||||
'You cannot use `--userId` and `--projectId` together. Use one or the other.',
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,403 @@
|
||||
import {
|
||||
mockInstance,
|
||||
testDb,
|
||||
createWorkflowWithTriggerAndHistory,
|
||||
setActiveVersion,
|
||||
createWorkflowHistory,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import fs from 'fs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { ExportWorkflowsCommand } from '@/commands/export/workflow';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
|
||||
const command = setupTestCommand(ExportWorkflowsCommand);
|
||||
|
||||
let testOutputDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['WorkflowEntity', 'WorkflowHistory']);
|
||||
testOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'n8n-export-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(testOutputDir)) {
|
||||
fs.rmSync(testOutputDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should reject both --version and --published flags', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
|
||||
await command.run([
|
||||
`--id=${workflow.id}`,
|
||||
`--version=${workflow.versionId}`,
|
||||
'--published',
|
||||
`--output=${outputFile}`,
|
||||
]);
|
||||
|
||||
expect(fs.existsSync(outputFile)).toBe(false);
|
||||
});
|
||||
|
||||
test('should reject --version with --all flag', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
|
||||
await command.run(['--all', `--version=${workflow.versionId}`, `--output=${outputFile}`]);
|
||||
|
||||
expect(fs.existsSync(outputFile)).toBe(false);
|
||||
});
|
||||
|
||||
test('should export current draft version when no flags set', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-draft',
|
||||
parameters: {},
|
||||
name: 'Draft Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([`--id=${workflow.id}`, `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'))[0];
|
||||
|
||||
expect(exportedData).toMatchObject({
|
||||
id: workflow.id,
|
||||
name: 'Test Workflow',
|
||||
versionId: workflow.versionId,
|
||||
});
|
||||
expect(exportedData.nodes[0].name).toBe('Draft Node');
|
||||
});
|
||||
|
||||
test('should export specified version with --version flag', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-v1',
|
||||
parameters: {},
|
||||
name: 'Version 1 Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const version1Id = workflow.versionId;
|
||||
|
||||
const newVersionId = nanoid();
|
||||
workflow.versionId = newVersionId;
|
||||
workflow.nodes = [
|
||||
{
|
||||
id: 'uuid-v2',
|
||||
parameters: {},
|
||||
name: 'Version 2 Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
},
|
||||
];
|
||||
await Container.get(WorkflowRepository).save(workflow);
|
||||
await createWorkflowHistory(workflow);
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([`--id=${workflow.id}`, `--version=${version1Id}`, `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'))[0];
|
||||
|
||||
expect(exportedData).toMatchObject({
|
||||
id: workflow.id,
|
||||
versionId: version1Id,
|
||||
});
|
||||
expect(exportedData.nodes[0].name).toBe('Version 1 Node');
|
||||
});
|
||||
|
||||
test('should export published version with --published flag', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
name: 'Test Workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-published',
|
||||
parameters: {},
|
||||
name: 'Published Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const publishedVersionId = workflow.versionId;
|
||||
|
||||
await setActiveVersion(workflow.id, publishedVersionId);
|
||||
|
||||
const draftVersionId = nanoid();
|
||||
workflow.versionId = draftVersionId;
|
||||
workflow.activeVersionId = publishedVersionId;
|
||||
workflow.nodes = [
|
||||
{
|
||||
id: 'uuid-draft',
|
||||
parameters: {},
|
||||
name: 'Draft Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
},
|
||||
];
|
||||
await Container.get(WorkflowRepository).save(workflow);
|
||||
await createWorkflowHistory(workflow);
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([`--id=${workflow.id}`, '--published', `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'))[0];
|
||||
|
||||
expect(exportedData).toMatchObject({
|
||||
id: workflow.id,
|
||||
versionId: publishedVersionId,
|
||||
});
|
||||
expect(exportedData.nodes[0].name).toBe('Published Node');
|
||||
});
|
||||
|
||||
test('should optimize when target version is current draft', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
name: 'Test Workflow',
|
||||
});
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([
|
||||
`--id=${workflow.id}`,
|
||||
`--version=${workflow.versionId}`,
|
||||
`--output=${outputFile}`,
|
||||
]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'))[0];
|
||||
|
||||
expect(exportedData).toMatchObject({
|
||||
id: workflow.id,
|
||||
versionId: workflow.versionId,
|
||||
});
|
||||
});
|
||||
|
||||
test('should merge historical nodes with current metadata', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
name: 'Original Name',
|
||||
nodes: [
|
||||
{
|
||||
id: 'uuid-v1',
|
||||
parameters: {},
|
||||
name: 'Version 1 Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const version1Id = workflow.versionId;
|
||||
|
||||
const newVersionId = nanoid();
|
||||
workflow.versionId = newVersionId;
|
||||
workflow.name = 'Updated Name';
|
||||
workflow.nodes = [
|
||||
{
|
||||
id: 'uuid-v2',
|
||||
parameters: {},
|
||||
name: 'Version 2 Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [240, 300],
|
||||
},
|
||||
];
|
||||
await Container.get(WorkflowRepository).save(workflow);
|
||||
await createWorkflowHistory(workflow);
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([`--id=${workflow.id}`, `--version=${version1Id}`, `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'))[0];
|
||||
|
||||
expect(exportedData.nodes[0].name).toBe('Version 1 Node');
|
||||
expect(exportedData.versionId).toBe(version1Id);
|
||||
expect(exportedData.name).toBe('Updated Name');
|
||||
});
|
||||
|
||||
test('should error when version not found', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
const nonExistentVersionId = 'non-existent-version';
|
||||
|
||||
await expect(
|
||||
command.run([`--id=${workflow.id}`, `--version=${nonExistentVersionId}`]),
|
||||
).rejects.toThrow(
|
||||
`Version "${nonExistentVersionId}" not found for workflow "${workflow.name}" (${workflow.id})`,
|
||||
);
|
||||
});
|
||||
|
||||
test('should error when --published used on unpublished workflow', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
name: 'Unpublished Workflow',
|
||||
});
|
||||
|
||||
workflow.activeVersionId = null;
|
||||
await Container.get(WorkflowRepository).save(workflow);
|
||||
|
||||
await expect(command.run([`--id=${workflow.id}`, '--published'])).rejects.toThrow(
|
||||
`No published version found for workflow "${workflow.name}" (${workflow.id})`,
|
||||
);
|
||||
});
|
||||
|
||||
test('should error when workflow not found', async () => {
|
||||
const nonExistentId = 'non-existent-id';
|
||||
|
||||
await expect(command.run([`--id=${nonExistentId}`])).rejects.toThrow(
|
||||
'No workflows found with specified filters',
|
||||
);
|
||||
});
|
||||
|
||||
test('should work without any version flags (existing behavior)', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
name: 'Test Workflow',
|
||||
});
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([`--id=${workflow.id}`, `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'))[0];
|
||||
|
||||
expect(exportedData).toMatchObject({
|
||||
id: workflow.id,
|
||||
name: 'Test Workflow',
|
||||
versionId: workflow.versionId,
|
||||
});
|
||||
});
|
||||
|
||||
test('should work with --all flag (existing behavior)', async () => {
|
||||
await createWorkflowWithTriggerAndHistory({ name: 'Workflow 1' });
|
||||
await createWorkflowWithTriggerAndHistory({ name: 'Workflow 2' });
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run(['--all', `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'));
|
||||
|
||||
expect(exportedData).toHaveLength(2);
|
||||
|
||||
const workflowNames = exportedData.map((w: any) => w.name);
|
||||
expect(workflowNames).toContain('Workflow 1');
|
||||
expect(workflowNames).toContain('Workflow 2');
|
||||
});
|
||||
|
||||
test('should work with --pretty flag (existing behavior)', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([`--id=${workflow.id}`, '--pretty', `--output=${outputFile}`]);
|
||||
|
||||
const fileContents = fs.readFileSync(outputFile, 'utf-8');
|
||||
expect(fileContents).toContain('\n');
|
||||
expect(fileContents).toMatch(/\s{2}/);
|
||||
});
|
||||
|
||||
test('should export all published versions with --all --published', async () => {
|
||||
const workflow1 = await createWorkflowWithTriggerAndHistory({ name: 'Published Workflow' });
|
||||
const publishedVersionId = workflow1.versionId;
|
||||
await setActiveVersion(workflow1.id, publishedVersionId);
|
||||
|
||||
const draftVersionId = nanoid();
|
||||
workflow1.versionId = draftVersionId;
|
||||
workflow1.activeVersionId = publishedVersionId;
|
||||
await Container.get(WorkflowRepository).save(workflow1);
|
||||
await createWorkflowHistory(workflow1);
|
||||
|
||||
await createWorkflowWithTriggerAndHistory({ name: 'Unpublished Workflow' });
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run(['--all', '--published', `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'));
|
||||
|
||||
expect(exportedData).toHaveLength(1);
|
||||
expect(exportedData[0].name).toBe('Published Workflow');
|
||||
expect(exportedData[0].versionId).toBe(publishedVersionId);
|
||||
});
|
||||
|
||||
test('should include versionMetadata with historical name when set', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
name: 'Original Name',
|
||||
});
|
||||
|
||||
const version2Id = nanoid();
|
||||
workflow.versionId = version2Id;
|
||||
workflow.name = 'Updated Name';
|
||||
await Container.get(WorkflowRepository).save(workflow);
|
||||
|
||||
await createWorkflowHistory(workflow, undefined, undefined, {
|
||||
name: 'Version 2 Historical Name',
|
||||
});
|
||||
|
||||
const version3Id = nanoid();
|
||||
workflow.versionId = version3Id;
|
||||
workflow.name = 'Current Name';
|
||||
await Container.get(WorkflowRepository).save(workflow);
|
||||
await createWorkflowHistory(workflow);
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([`--id=${workflow.id}`, `--version=${version2Id}`, `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'))[0];
|
||||
|
||||
expect(exportedData.name).toBe('Current Name');
|
||||
expect(exportedData.versionId).toBe(version2Id);
|
||||
expect(exportedData.versionMetadata).toEqual({
|
||||
name: 'Version 2 Historical Name',
|
||||
description: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('should include versionMetadata with historical description when set', async () => {
|
||||
const workflow = await createWorkflowWithTriggerAndHistory({
|
||||
description: 'Original Description',
|
||||
});
|
||||
|
||||
const version2Id = nanoid();
|
||||
workflow.versionId = version2Id;
|
||||
workflow.description = 'Updated Description';
|
||||
await Container.get(WorkflowRepository).save(workflow);
|
||||
|
||||
await createWorkflowHistory(workflow, undefined, undefined, {
|
||||
description: 'Version 2 Historical Description',
|
||||
});
|
||||
|
||||
const version3Id = nanoid();
|
||||
workflow.versionId = version3Id;
|
||||
workflow.description = 'Current Description';
|
||||
await Container.get(WorkflowRepository).save(workflow);
|
||||
await createWorkflowHistory(workflow);
|
||||
|
||||
const outputFile = path.join(testOutputDir, 'output.json');
|
||||
await command.run([`--id=${workflow.id}`, `--version=${version2Id}`, `--output=${outputFile}`]);
|
||||
|
||||
const exportedData = JSON.parse(fs.readFileSync(outputFile, 'utf-8'))[0];
|
||||
|
||||
expect(exportedData.description).toBe('Current Description');
|
||||
expect(exportedData.versionId).toBe(version2Id);
|
||||
expect(exportedData.versionMetadata).toEqual({
|
||||
name: null,
|
||||
description: 'Version 2 Historical Description',
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"createdAt": "2023-07-10T14:50:49.193Z",
|
||||
"updatedAt": "2023-10-27T13:34:42.917Z",
|
||||
"id": "123",
|
||||
"name": "cred-aws-prod",
|
||||
"data": {
|
||||
"region": "eu-west-1",
|
||||
"accessKeyId": "999999999999",
|
||||
"secretAccessKey": "aaaaaaaaaaaaa"
|
||||
},
|
||||
"type": "aws"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"createdAt": "2023-07-10T14:50:49.193Z",
|
||||
"updatedAt": "2023-10-27T13:34:42.917Z",
|
||||
"id": "123",
|
||||
"name": "cred-aws-test",
|
||||
"data": {
|
||||
"region": "eu-west-1",
|
||||
"accessKeyId": "999999999999",
|
||||
"secretAccessKey": "aaaaaaaaaaaaa"
|
||||
},
|
||||
"type": "aws"
|
||||
}
|
||||
]
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"createdAt": "2023-07-10T14:50:49.193Z",
|
||||
"updatedAt": "2023-10-27T13:34:42.917Z",
|
||||
"id": "123",
|
||||
"name": "cred-aws-test",
|
||||
"data": {
|
||||
"region": "eu-west-1",
|
||||
"accessKeyId": "999999999999",
|
||||
"secretAccessKey": "aaaaaaaaaaaaa"
|
||||
},
|
||||
"type": "aws"
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
[
|
||||
{
|
||||
"name": "active-workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "998",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
[
|
||||
{
|
||||
"name": "active-workflow updated",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "998",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,160 @@
|
||||
[
|
||||
{
|
||||
"name": "active-workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "998",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"name": "inactive-workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b137",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b137"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097c",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "999",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "active-workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "998",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "active-workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "998",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "inactive-workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b137",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [800, 580],
|
||||
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b137"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"boolean": [
|
||||
{
|
||||
"name": "hooked",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9701b1ef-9ab0-432a-b086-cf76981b097c",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [1240, 580]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
|
||||
"id": "999",
|
||||
"meta": {
|
||||
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
[
|
||||
{
|
||||
"id": "test-workflow-123",
|
||||
"name": "Workflow with History Metadata",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "node-uuid-1",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [240, 300]
|
||||
}
|
||||
],
|
||||
"connections": {},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "version-uuid-1",
|
||||
"versionMetadata": {
|
||||
"name": "Historical Version Name",
|
||||
"description": "Historical version description"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,373 @@
|
||||
import {
|
||||
mockInstance,
|
||||
testDb,
|
||||
getPersonalProject,
|
||||
getAllSharedWorkflows,
|
||||
getAllWorkflows,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import { WorkflowPublishHistoryRepository, WorkflowHistoryRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import '@/zod-alias-support';
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import { ImportWorkflowsCommand } from '@/commands/import/workflow';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
import { createMember, createOwner } from '../shared/db/users';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
mockInstance(ActiveWorkflowManager);
|
||||
mockInstance(WorkflowPublishHistoryRepository);
|
||||
|
||||
const command = setupTestCommand(ImportWorkflowsCommand);
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['WorkflowEntity', 'SharedWorkflow', 'User']);
|
||||
});
|
||||
|
||||
test('import:workflow should import active workflow and deactivate it', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([
|
||||
'--separate',
|
||||
'--input=./test/integration/commands/import-workflows/separate',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
expect(after).toMatchObject({
|
||||
workflows: [
|
||||
expect.objectContaining({ name: 'active-workflow', active: false, activeVersionId: null }),
|
||||
expect.objectContaining({ name: 'inactive-workflow', active: false, activeVersionId: null }),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
workflowId: '999',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('import:workflow should import active workflow from combined file and deactivate it', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-workflows/combined/combined.json',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
expect(after).toMatchObject({
|
||||
workflows: [
|
||||
expect.objectContaining({ name: 'active-workflow', active: false, activeVersionId: null }),
|
||||
expect.objectContaining({ name: 'inactive-workflow', active: false, activeVersionId: null }),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
workflowId: '999',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('import:workflow can import a single workflow object', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--input=./test/integration/commands/import-workflows/combined/single.json']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
expect(after).toMatchObject({
|
||||
workflows: [
|
||||
expect.objectContaining({ name: 'active-workflow', active: false, activeVersionId: null }),
|
||||
],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('`import:workflow --userId ...` should fail if the workflow exists already and is owned by somebody else', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
const member = await createMember();
|
||||
|
||||
// Import workflow the first time, assigning it to a member.
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-workflows/combined-with-update/original.json',
|
||||
`--userId=${owner.id}`,
|
||||
]);
|
||||
|
||||
const before = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure the workflow and sharing have been created.
|
||||
expect(before).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// Import the same workflow again, with another name but the same ID, and try
|
||||
// to assign it to the member.
|
||||
await expect(
|
||||
command.run([
|
||||
'--input=./test/integration/commands/import-workflows/combined-with-update/updated.json',
|
||||
`--userId=${member.id}`,
|
||||
]),
|
||||
).rejects.toThrowError(
|
||||
`The credential with ID "998" is already owned by the user with the ID "${owner.id}". It can't be re-owned by the user with the ID "${member.id}"`,
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure there is no new sharing and that the name DID NOT change.
|
||||
expect(after).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("only update the workflow, don't create or update the owner if `--userId` is not passed", async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
await createOwner();
|
||||
const member = await createMember();
|
||||
const memberProject = await getPersonalProject(member);
|
||||
|
||||
// Import workflow the first time, assigning it to a member.
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-workflows/combined-with-update/original.json',
|
||||
`--userId=${member.id}`,
|
||||
]);
|
||||
|
||||
const before = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure the workflow and sharing have been created.
|
||||
expect(before).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: memberProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// Import the same workflow again, with another name but the same ID.
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-workflows/combined-with-update/updated.json',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure there is no new sharing and that the name changed.
|
||||
expect(after).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow updated' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: memberProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('`import:workflow --projectId ...` should fail if the credential already exists and is owned by another project', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createOwner();
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
const member = await createMember();
|
||||
const memberProject = await getPersonalProject(member);
|
||||
|
||||
// Import workflow the first time, assigning it to a member.
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-workflows/combined-with-update/original.json',
|
||||
`--userId=${owner.id}`,
|
||||
]);
|
||||
|
||||
const before = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure the workflow and sharing have been created.
|
||||
expect(before).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
// Import the same workflow again, with another name but the same ID, and try
|
||||
// to assign it to the member.
|
||||
await expect(
|
||||
command.run([
|
||||
'--input=./test/integration/commands/import-workflows/combined-with-update/updated.json',
|
||||
`--projectId=${memberProject.id}`,
|
||||
]),
|
||||
).rejects.toThrowError(
|
||||
`The credential with ID "998" is already owned by the user with the ID "${owner.id}". It can't be re-owned by the project with the ID "${memberProject.id}"`,
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = {
|
||||
workflows: await getAllWorkflows(),
|
||||
sharings: await getAllSharedWorkflows(),
|
||||
};
|
||||
// Make sure there is no new sharing and that the name DID NOT change.
|
||||
expect(after).toMatchObject({
|
||||
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
|
||||
sharings: [
|
||||
expect.objectContaining({
|
||||
workflowId: '998',
|
||||
projectId: ownerProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('`import:workflow --projectId ... --userId ...` fails explaining that only one of the options can be used at a time', async () => {
|
||||
await expect(
|
||||
command.run([
|
||||
'--input=./test/integration/commands/import-workflows/combined-with-update/updated.json',
|
||||
`--userId=${nanoid()}`,
|
||||
`--projectId=${nanoid()}`,
|
||||
]),
|
||||
).rejects.toThrowError(
|
||||
'You cannot use `--userId` and `--projectId` together. Use one or the other.',
|
||||
);
|
||||
});
|
||||
|
||||
test('should preserve versionMetadata from JSON file when importing', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
await createOwner();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([
|
||||
'--input=./test/integration/commands/import-workflows/with-history/workflow-with-metadata.json',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const workflows = await getAllWorkflows();
|
||||
expect(workflows).toHaveLength(1);
|
||||
expect(workflows[0].id).toBe('test-workflow-123');
|
||||
expect(workflows[0].name).toBe('Workflow with History Metadata');
|
||||
|
||||
const workflowHistoryRecords = await Container.get(WorkflowHistoryRepository).find({
|
||||
where: { workflowId: 'test-workflow-123' },
|
||||
});
|
||||
|
||||
expect(workflowHistoryRecords).toHaveLength(1);
|
||||
expect(workflowHistoryRecords[0].name).toBe('Historical Version Name');
|
||||
expect(workflowHistoryRecords[0].description).toBe('Historical version description');
|
||||
});
|
||||
@@ -0,0 +1,365 @@
|
||||
import {
|
||||
createTeamProject,
|
||||
findProject,
|
||||
getPersonalProject,
|
||||
mockInstance,
|
||||
createWorkflow,
|
||||
randomCredentialPayload,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import {
|
||||
CredentialsRepository,
|
||||
SharedCredentialsRepository,
|
||||
SharedWorkflowRepository,
|
||||
WorkflowRepository,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { EntityNotFoundError } from '@n8n/typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { Reset } from '@/commands/ldap/reset';
|
||||
import { getLdapSynchronizations, saveLdapSynchronization } from '@/modules/ldap.ee/helpers.ee';
|
||||
import { LdapService } from '@/modules/ldap.ee/ldap.service.ee';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { Push } from '@/push';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
import { saveCredential } from '../../shared/db/credentials';
|
||||
import { createLdapUser, createMember, getUserById } from '../../shared/db/users';
|
||||
import { createLdapConfig } from '../../shared/ldap';
|
||||
|
||||
mockInstance(Telemetry);
|
||||
|
||||
mockInstance(Push);
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const command = setupTestCommand(Reset);
|
||||
|
||||
test('fails if neither `--userId` nor `--projectId` nor `--deleteWorkflowsAndCredentials` is passed', async () => {
|
||||
await expect(command.run()).rejects.toThrowError(
|
||||
'You must use exactly one of `--userId`, `--projectId` or `--deleteWorkflowsAndCredentials`.',
|
||||
);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[`--userId=${uuid()}`, `--projectId=${uuid()}`, '--deleteWorkflowsAndCredentials'],
|
||||
|
||||
[`--userId=${uuid()}`, `--projectId=${uuid()}`],
|
||||
[`--userId=${uuid()}`, '--deleteWorkflowsAndCredentials'],
|
||||
|
||||
['--deleteWorkflowsAndCredentials', `--projectId=${uuid()}`],
|
||||
])(
|
||||
'fails if more than one of `--userId`, `--projectId`, `--deleteWorkflowsAndCredentials` are passed',
|
||||
async (...argv) => {
|
||||
await expect(command.run(argv)).rejects.toThrowError(
|
||||
'You must use exactly one of `--userId`, `--projectId` or `--deleteWorkflowsAndCredentials`.',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
describe('--deleteWorkflowsAndCredentials', () => {
|
||||
test('deletes personal projects, workflows and credentials owned by LDAP managed users', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
|
||||
const memberProject = await getPersonalProject(member);
|
||||
const workflow = await createWorkflow({}, member);
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
user: member,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
const normalMember = await createMember();
|
||||
const workflow2 = await createWorkflow({}, normalMember);
|
||||
const credential2 = await saveCredential(randomCredentialPayload(), {
|
||||
user: normalMember,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--deleteWorkflowsAndCredentials']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// LDAP user is deleted
|
||||
await expect(getUserById(member.id)).rejects.toThrowError(EntityNotFoundError);
|
||||
await expect(findProject(memberProject.id)).rejects.toThrowError(EntityNotFoundError);
|
||||
await expect(
|
||||
Container.get(WorkflowRepository).findOneBy({ id: workflow.id }),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
Container.get(CredentialsRepository).findOneBy({ id: credential.id }),
|
||||
).resolves.toBeNull();
|
||||
|
||||
// Non LDAP user is not deleted
|
||||
await expect(getUserById(normalMember.id)).resolves.not.toThrowError();
|
||||
await expect(
|
||||
Container.get(WorkflowRepository).findOneBy({ id: workflow2.id }),
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
Container.get(CredentialsRepository).findOneBy({ id: credential2.id }),
|
||||
).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
test('deletes the LDAP sync history', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
await saveLdapSynchronization({
|
||||
created: 1,
|
||||
disabled: 1,
|
||||
scanned: 1,
|
||||
updated: 1,
|
||||
endedAt: new Date(),
|
||||
startedAt: new Date(),
|
||||
error: '',
|
||||
runMode: 'dry',
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--deleteWorkflowsAndCredentials']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
await expect(getLdapSynchronizations(0, 10)).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
test('resets LDAP settings', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
await createLdapConfig();
|
||||
await expect(Container.get(LdapService).loadConfig()).resolves.toMatchObject({
|
||||
loginEnabled: true,
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--deleteWorkflowsAndCredentials']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
await expect(Container.get(LdapService).loadConfig()).resolves.toMatchObject({
|
||||
loginEnabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('--userId', () => {
|
||||
test('fails if the user does not exist', async () => {
|
||||
const userId = uuid();
|
||||
await expect(command.run([`--userId=${userId}`])).rejects.toThrowError(
|
||||
`Could not find the user with the ID ${userId} or their personalProject.`,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if the user to migrate to is also an LDAP user', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
|
||||
|
||||
await expect(command.run([`--userId=${member.id}`])).rejects.toThrowError(
|
||||
`Can't migrate workflows and credentials to the user with the ID ${member.id}. That user was created via LDAP and will be deleted as well.`,
|
||||
);
|
||||
});
|
||||
|
||||
test("transfers all workflows and credentials to the user's personal project", async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
|
||||
const memberProject = await getPersonalProject(member);
|
||||
const workflow = await createWorkflow({}, member);
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
user: member,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
const normalMember = await createMember();
|
||||
const normalMemberProject = await getPersonalProject(normalMember);
|
||||
const workflow2 = await createWorkflow({}, normalMember);
|
||||
const credential2 = await saveCredential(randomCredentialPayload(), {
|
||||
user: normalMember,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([`--userId=${normalMember.id}`]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// LDAP user is deleted
|
||||
await expect(getUserById(member.id)).rejects.toThrowError(EntityNotFoundError);
|
||||
await expect(findProject(memberProject.id)).rejects.toThrowError(EntityNotFoundError);
|
||||
|
||||
// Their workflow and credential have been migrated to the normal user.
|
||||
await expect(
|
||||
Container.get(SharedWorkflowRepository).findOneBy({
|
||||
workflowId: workflow.id,
|
||||
projectId: normalMemberProject.id,
|
||||
}),
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
Container.get(SharedCredentialsRepository).findOneBy({
|
||||
credentialsId: credential.id,
|
||||
projectId: normalMemberProject.id,
|
||||
}),
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
// Non LDAP user is not deleted
|
||||
await expect(getUserById(normalMember.id)).resolves.not.toThrowError();
|
||||
await expect(
|
||||
Container.get(WorkflowRepository).findOneBy({ id: workflow2.id }),
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
Container.get(CredentialsRepository).findOneBy({ id: credential2.id }),
|
||||
).resolves.not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('--projectId', () => {
|
||||
test('fails if the project does not exist', async () => {
|
||||
const projectId = uuid();
|
||||
await expect(command.run([`--projectId=${projectId}`])).rejects.toThrowError(
|
||||
`Could not find the project with the ID ${projectId}.`,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if the user to migrate to is also an LDAP user', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
|
||||
const memberProject = await getPersonalProject(member);
|
||||
|
||||
await expect(command.run([`--projectId=${memberProject.id}`])).rejects.toThrowError(
|
||||
`Can't migrate workflows and credentials to the project with the ID ${memberProject.id}. That project is a personal project belonging to a user that was created via LDAP and will be deleted as well.`,
|
||||
);
|
||||
});
|
||||
|
||||
test('transfers all workflows and credentials to a personal project', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
|
||||
const memberProject = await getPersonalProject(member);
|
||||
const workflow = await createWorkflow({}, member);
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
user: member,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
const normalMember = await createMember();
|
||||
const normalMemberProject = await getPersonalProject(normalMember);
|
||||
const workflow2 = await createWorkflow({}, normalMember);
|
||||
const credential2 = await saveCredential(randomCredentialPayload(), {
|
||||
user: normalMember,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([`--projectId=${normalMemberProject.id}`]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// LDAP user is deleted
|
||||
await expect(getUserById(member.id)).rejects.toThrowError(EntityNotFoundError);
|
||||
await expect(findProject(memberProject.id)).rejects.toThrowError(EntityNotFoundError);
|
||||
|
||||
// Their workflow and credential have been migrated to the normal user.
|
||||
await expect(
|
||||
Container.get(SharedWorkflowRepository).findOneBy({
|
||||
workflowId: workflow.id,
|
||||
projectId: normalMemberProject.id,
|
||||
}),
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
Container.get(SharedCredentialsRepository).findOneBy({
|
||||
credentialsId: credential.id,
|
||||
projectId: normalMemberProject.id,
|
||||
}),
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
// Non LDAP user is not deleted
|
||||
await expect(getUserById(normalMember.id)).resolves.not.toThrowError();
|
||||
await expect(
|
||||
Container.get(WorkflowRepository).findOneBy({ id: workflow2.id }),
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
Container.get(CredentialsRepository).findOneBy({ id: credential2.id }),
|
||||
).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
test('transfers all workflows and credentials to a team project', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
|
||||
const memberProject = await getPersonalProject(member);
|
||||
const workflow = await createWorkflow({}, member);
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
user: member,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
const normalMember = await createMember();
|
||||
const workflow2 = await createWorkflow({}, normalMember);
|
||||
const credential2 = await saveCredential(randomCredentialPayload(), {
|
||||
user: normalMember,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
const teamProject = await createTeamProject();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([`--projectId=${teamProject.id}`]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// LDAP user is deleted
|
||||
await expect(getUserById(member.id)).rejects.toThrowError(EntityNotFoundError);
|
||||
await expect(findProject(memberProject.id)).rejects.toThrowError(EntityNotFoundError);
|
||||
|
||||
// Their workflow and credential have been migrated to the team project.
|
||||
await expect(
|
||||
Container.get(SharedWorkflowRepository).findOneBy({
|
||||
workflowId: workflow.id,
|
||||
projectId: teamProject.id,
|
||||
}),
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
Container.get(SharedCredentialsRepository).findOneBy({
|
||||
credentialsId: credential.id,
|
||||
projectId: teamProject.id,
|
||||
}),
|
||||
).resolves.not.toBeNull();
|
||||
|
||||
// Non LDAP user is not deleted
|
||||
await expect(getUserById(normalMember.id)).resolves.not.toThrowError();
|
||||
await expect(
|
||||
Container.get(WorkflowRepository).findOneBy({ id: workflow2.id }),
|
||||
).resolves.not.toBeNull();
|
||||
await expect(
|
||||
Container.get(CredentialsRepository).findOneBy({ id: credential2.id }),
|
||||
).resolves.not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { ClearLicenseCommand } from '@/commands/license/clear';
|
||||
import { License } from '@/license';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const command = setupTestCommand(ClearLicenseCommand);
|
||||
|
||||
test('license:clear invokes clear() to release any floating entitlements and deletes the license cert from the DB', async () => {
|
||||
const license = Container.get(License);
|
||||
|
||||
const manager = {
|
||||
clear: jest.fn().mockImplementation(async () => {
|
||||
await license.saveCertStr('');
|
||||
}),
|
||||
};
|
||||
|
||||
const initSpy = jest.spyOn(license, 'init').mockImplementation(async () => {
|
||||
Object.defineProperty(license, 'manager', {
|
||||
value: manager,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
const clearSpy = jest.spyOn(license, 'clear');
|
||||
const saveCertStrSpy = jest.spyOn(license, 'saveCertStr');
|
||||
|
||||
await command.run();
|
||||
|
||||
expect(initSpy).toHaveBeenCalledTimes(1);
|
||||
expect(clearSpy).toHaveBeenCalledTimes(1);
|
||||
expect(saveCertStrSpy).toHaveBeenCalledWith('');
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
mockInstance,
|
||||
testDb,
|
||||
createWorkflowWithTriggerAndHistory,
|
||||
getWorkflowById,
|
||||
} from '@n8n/backend-test-utils';
|
||||
|
||||
import { PublishWorkflowCommand } from '@/commands/publish/workflow';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const command = setupTestCommand(PublishWorkflowCommand);
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['WorkflowEntity', 'WorkflowHistory']);
|
||||
});
|
||||
|
||||
test('publish:workflow can publish a specific workflow version', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([`--id=${workflow.id}`, `--versionId=${workflow.versionId}`]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const updatedWorkflow = await getWorkflowById(workflow.id);
|
||||
|
||||
expect(updatedWorkflow).toMatchObject({
|
||||
activeVersionId: workflow.versionId,
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('publish:workflow does not publish when --all flag is used', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--all', `--id=${workflow.id}`, `--versionId=${workflow.versionId}`]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// Verify the workflow was not published (--all flag prevents publishing)
|
||||
const unchangedWorkflow = await getWorkflowById(workflow.id);
|
||||
expect(unchangedWorkflow).toMatchObject({
|
||||
activeVersionId: null,
|
||||
active: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('publish:workflow publishes current version when --versionId is missing', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([`--id=${workflow.id}`]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const updatedWorkflow = await getWorkflowById(workflow.id);
|
||||
expect(updatedWorkflow).toMatchObject({
|
||||
activeVersionId: workflow.versionId,
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('unpublish:workflow throws error when workflow does not exist', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const nonExistentWorkflowId = 'non-existent-workflow-id';
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
await expect(command.run([`--id=${nonExistentWorkflowId}`])).rejects.toThrow(
|
||||
`Workflow "${nonExistentWorkflowId}" not found.`,
|
||||
);
|
||||
});
|
||||
|
||||
test('publish:workflow throws error when version does not exist', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
const nonExistentVersionId = 'non-existent-version';
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
await expect(
|
||||
command.run([`--id=${workflow.id}`, `--versionId=${nonExistentVersionId}`]),
|
||||
).rejects.toThrow(`Version "${nonExistentVersionId}" not found for workflow "${workflow.id}".`);
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
getPersonalProject,
|
||||
mockInstance,
|
||||
createWorkflow,
|
||||
testDb,
|
||||
randomCredentialPayload,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import {
|
||||
CredentialsEntity,
|
||||
CredentialsRepository,
|
||||
SharedCredentialsRepository,
|
||||
SharedWorkflowRepository,
|
||||
UserRepository,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { Reset } from '@/commands/user-management/reset';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { NodeTypes } from '@/node-types';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
import { encryptCredentialData, saveCredential } from '../shared/db/credentials';
|
||||
import { createMember, createUser } from '../shared/db/users';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
mockInstance(NodeTypes);
|
||||
const command = setupTestCommand(Reset);
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
});
|
||||
|
||||
test('user-management:reset should reset DB to default user state', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const owner = await createUser({ role: GLOBAL_OWNER_ROLE });
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
|
||||
// should be deleted
|
||||
const member = await createMember();
|
||||
|
||||
// should be re-owned
|
||||
const workflow = await createWorkflow({}, member);
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
user: member,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
// dangling credentials should also be re-owned
|
||||
const danglingCredential = await Container.get(CredentialsRepository).save(
|
||||
await encryptCredentialData(Object.assign(new CredentialsEntity(), randomCredentialPayload())),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run();
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
|
||||
// check if the owner account was reset:
|
||||
await expect(
|
||||
Container.get(UserRepository).findOneBy({ role: { slug: GLOBAL_OWNER_ROLE.slug } }),
|
||||
).resolves.toMatchObject({
|
||||
email: null,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
password: null,
|
||||
personalizationAnswers: null,
|
||||
});
|
||||
|
||||
// all members were deleted:
|
||||
const members = await Container.get(UserRepository).findOneBy({
|
||||
role: { slug: 'global:member' },
|
||||
});
|
||||
expect(members).toBeNull();
|
||||
|
||||
// all workflows are owned by the owner:
|
||||
await expect(
|
||||
Container.get(SharedWorkflowRepository).findBy({ workflowId: workflow.id }),
|
||||
).resolves.toMatchObject([{ projectId: ownerProject.id, role: 'workflow:owner' }]);
|
||||
|
||||
// all credentials are owned by the owner
|
||||
await expect(
|
||||
Container.get(SharedCredentialsRepository).findBy({ credentialsId: credential.id }),
|
||||
).resolves.toMatchObject([{ projectId: ownerProject.id, role: 'credential:owner' }]);
|
||||
|
||||
// all dangling credentials are owned by the owner
|
||||
await expect(
|
||||
Container.get(SharedCredentialsRepository).findBy({ credentialsId: danglingCredential.id }),
|
||||
).resolves.toMatchObject([{ projectId: ownerProject.id, role: 'credential:owner' }]);
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
mockInstance,
|
||||
testDb,
|
||||
createManyActiveWorkflows,
|
||||
getWorkflowById,
|
||||
} from '@n8n/backend-test-utils';
|
||||
|
||||
import { UnpublishWorkflowCommand } from '@/commands/unpublish/workflow';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const command = setupTestCommand(UnpublishWorkflowCommand);
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['WorkflowEntity', 'WorkflowHistory']);
|
||||
});
|
||||
|
||||
test('unpublish:workflow can unpublish all workflows', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflows = await createManyActiveWorkflows(2);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--all']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const workflow1 = await getWorkflowById(workflows[0].id);
|
||||
const workflow2 = await getWorkflowById(workflows[1].id);
|
||||
|
||||
expect(workflow1).toMatchObject({
|
||||
activeVersionId: null,
|
||||
active: false,
|
||||
});
|
||||
|
||||
expect(workflow2).toMatchObject({
|
||||
activeVersionId: null,
|
||||
active: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('unpublish:workflow can unpublish a specific workflow', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflows = await createManyActiveWorkflows(2);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([`--id=${workflows[0].id}`]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const unpublishedWorkflow = await getWorkflowById(workflows[0].id);
|
||||
const activeWorkflow = await getWorkflowById(workflows[1].id);
|
||||
|
||||
expect(unpublishedWorkflow).toMatchObject({
|
||||
activeVersionId: null,
|
||||
active: false,
|
||||
});
|
||||
expect(activeWorkflow).toMatchObject({
|
||||
activeVersionId: workflows[1].versionId,
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('unpublish:workflow does nothing when neither --all nor --id is provided', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflows = await createManyActiveWorkflows(2);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// Verify workflows are still active
|
||||
const workflow1 = await getWorkflowById(workflows[0].id);
|
||||
const workflow2 = await getWorkflowById(workflows[1].id);
|
||||
|
||||
expect(workflow1).toMatchObject({
|
||||
activeVersionId: workflows[0].versionId,
|
||||
active: true,
|
||||
});
|
||||
|
||||
expect(workflow2).toMatchObject({
|
||||
activeVersionId: workflows[1].versionId,
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('unpublish:workflow does nothing when both --all and --id are provided', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflows = await createManyActiveWorkflows(2);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--all', '--id=123']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// Verify workflows are still active
|
||||
const workflow1 = await getWorkflowById(workflows[0].id);
|
||||
const workflow2 = await getWorkflowById(workflows[1].id);
|
||||
|
||||
expect(workflow1).toMatchObject({
|
||||
activeVersionId: workflows[0].versionId,
|
||||
active: true,
|
||||
});
|
||||
|
||||
expect(workflow2).toMatchObject({
|
||||
activeVersionId: workflows[1].versionId,
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('unpublish:workflow throws error when workflow does not exist', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const nonExistentWorkflowId = 'non-existent-workflow-id';
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
await expect(command.run([`--id=${nonExistentWorkflowId}`])).rejects.toThrow(
|
||||
`Workflow "${nonExistentWorkflowId}" not found.`,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
mockInstance,
|
||||
testDb,
|
||||
createWorkflowWithTriggerAndHistory,
|
||||
createManyActiveWorkflows,
|
||||
getAllWorkflows,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { UpdateWorkflowCommand } from '@/commands/update/workflow';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const command = setupTestCommand(UpdateWorkflowCommand);
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['WorkflowEntity', 'WorkflowHistory']);
|
||||
});
|
||||
|
||||
test('update:workflow does not publish when trying to publish all workflows', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflows = await Promise.all([
|
||||
createWorkflowWithTriggerAndHistory({}),
|
||||
createWorkflowWithTriggerAndHistory({}),
|
||||
]);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--all', '--active=true']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// Verify workflows were NOT published (publishing all is no longer supported)
|
||||
const workflowRepo = Container.get(WorkflowRepository);
|
||||
const workflow1 = await workflowRepo.findOneBy({ id: workflows[0].id });
|
||||
const workflow2 = await workflowRepo.findOneBy({ id: workflows[1].id });
|
||||
|
||||
expect(workflow1?.activeVersionId).toBeNull();
|
||||
expect(workflow2?.activeVersionId).toBeNull();
|
||||
});
|
||||
|
||||
test('update:workflow can unpublish all workflows', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflows = await createManyActiveWorkflows(2);
|
||||
|
||||
// Verify activeVersionId is set
|
||||
const workflowRepo = Container.get(WorkflowRepository);
|
||||
let workflow1 = await workflowRepo.findOneBy({ id: workflows[0].id });
|
||||
let workflow2 = await workflowRepo.findOneBy({ id: workflows[1].id });
|
||||
expect(workflow1?.activeVersionId).toBe(workflows[0].versionId);
|
||||
expect(workflow2?.activeVersionId).toBe(workflows[1].versionId);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run(['--all', '--active=false']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// Verify activeVersionId is cleared
|
||||
workflow1 = await workflowRepo.findOne({
|
||||
where: { id: workflows[0].id },
|
||||
relations: ['activeVersion'],
|
||||
});
|
||||
workflow2 = await workflowRepo.findOne({
|
||||
where: { id: workflows[1].id },
|
||||
relations: ['activeVersion'],
|
||||
});
|
||||
|
||||
expect(workflow1?.activeVersionId).toBeNull();
|
||||
expect(workflow1?.activeVersion).toBeNull();
|
||||
expect(workflow2?.activeVersionId).toBeNull();
|
||||
expect(workflow2?.activeVersion).toBeNull();
|
||||
});
|
||||
|
||||
test('update:workflow publishes current version when --active=true (backwards compatibility)', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflow = await createWorkflowWithTriggerAndHistory();
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([`--id=${workflow.id}`, '--active=true']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
// Verify workflow was published with current version
|
||||
const workflowRepo = Container.get(WorkflowRepository);
|
||||
const updatedWorkflow = await workflowRepo.findOneBy({ id: workflow.id });
|
||||
|
||||
expect(updatedWorkflow?.activeVersionId).toBe(workflow.versionId);
|
||||
expect(updatedWorkflow?.active).toBe(true);
|
||||
});
|
||||
|
||||
test('update:workflow can unpublish a specific workflow', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflows = (await createManyActiveWorkflows(2)).sort((wf1, wf2) =>
|
||||
wf1.id.localeCompare(wf2.id),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await command.run([`--id=${workflows[0].id}`, '--active=false']);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const after = (await getAllWorkflows()).sort((wf1, wf2) => wf1.id.localeCompare(wf2.id));
|
||||
expect(after).toMatchObject([
|
||||
{ activeVersionId: null },
|
||||
{ activeVersionId: workflows[1].versionId },
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
process.argv[2] = 'worker';
|
||||
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { ExecutionsConfig } from '@n8n/config';
|
||||
import { Container } from '@n8n/di';
|
||||
import { BinaryDataService } from 'n8n-core';
|
||||
|
||||
import { Worker } from '@/commands/worker';
|
||||
import config from '@/config';
|
||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { LogStreamingEventRelay } from '@/events/relays/log-streaming.event-relay';
|
||||
import { ExternalHooks } from '@/external-hooks';
|
||||
import { License } from '@/license';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { CommunityPackagesService } from '@/modules/community-packages/community-packages.service';
|
||||
import { Push } from '@/push';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import { Subscriber } from '@/scaling/pubsub/subscriber.service';
|
||||
import { ScalingService } from '@/scaling/scaling.service';
|
||||
import { TaskBrokerServer } from '@/task-runners/task-broker/task-broker-server';
|
||||
import { JsTaskRunnerProcess } from '@/task-runners/task-runner-process-js';
|
||||
import { PyTaskRunnerProcess } from '@/task-runners/task-runner-process-py';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
Container.get(ExecutionsConfig).mode = 'queue';
|
||||
config.set('binaryDataManager.availableModes', 'filesystem');
|
||||
mockInstance(LoadNodesAndCredentials);
|
||||
const binaryDataService = mockInstance(BinaryDataService);
|
||||
const communityPackagesService = mockInstance(CommunityPackagesService);
|
||||
const externalHooks = mockInstance(ExternalHooks);
|
||||
const license = mockInstance(License, { loadCertStr: async () => '' });
|
||||
const messageEventBus = mockInstance(MessageEventBus);
|
||||
const logStreamingEventRelay = mockInstance(LogStreamingEventRelay);
|
||||
const scalingService = mockInstance(ScalingService);
|
||||
const taskBrokerServer = mockInstance(TaskBrokerServer);
|
||||
const taskRunnerProcess = mockInstance(JsTaskRunnerProcess);
|
||||
mockInstance(PyTaskRunnerProcess);
|
||||
mockInstance(Publisher);
|
||||
mockInstance(Subscriber);
|
||||
mockInstance(Telemetry);
|
||||
mockInstance(Push);
|
||||
|
||||
const command = setupTestCommand(Worker);
|
||||
|
||||
test('worker initializes all its components', async () => {
|
||||
Container.get(ExecutionsConfig).mode = 'regular'; // should be overridden
|
||||
|
||||
await command.run();
|
||||
|
||||
expect(license.init).toHaveBeenCalledTimes(1);
|
||||
expect(binaryDataService.init).toHaveBeenCalledTimes(1);
|
||||
expect(communityPackagesService.init).toHaveBeenCalledTimes(1);
|
||||
expect(externalHooks.init).toHaveBeenCalledTimes(1);
|
||||
expect(messageEventBus.initialize).toHaveBeenCalledTimes(1);
|
||||
expect(scalingService.setupQueue).toHaveBeenCalledTimes(1);
|
||||
expect(scalingService.setupWorker).toHaveBeenCalledTimes(1);
|
||||
expect(logStreamingEventRelay.init).toHaveBeenCalledTimes(1);
|
||||
expect(messageEventBus.send).toHaveBeenCalledTimes(1);
|
||||
expect(taskBrokerServer.start).toHaveBeenCalledTimes(1);
|
||||
expect(taskRunnerProcess.start).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(Container.get(ExecutionsConfig).mode).toBe('queue');
|
||||
});
|
||||
Reference in New Issue
Block a user