Skip to content

Commit

Permalink
Merge branch 'n8n-io:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
sutidor authored Dec 11, 2024
2 parents 157c4bc + c572c06 commit 6224ab6
Show file tree
Hide file tree
Showing 74 changed files with 2,545 additions and 1,329 deletions.
2 changes: 1 addition & 1 deletion cypress/e2e/11-inline-expression-editor.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe('Inline expression editor', () => {

// Run workflow
ndv.actions.close();
WorkflowPage.actions.executeNode('No Operation');
WorkflowPage.actions.executeNode('No Operation', { anchor: 'topLeft' });
WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openInlineExpressionEditor();

Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/13-pinning.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('Data pinning', () => {
it('Should be able to pin data from canvas (context menu or shortcut)', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
workflowPage.actions.openContextMenu(EDIT_FIELDS_SET_NODE_NAME, 'overflow-button');
workflowPage.actions.openContextMenu(EDIT_FIELDS_SET_NODE_NAME, { method: 'overflow-button' });
workflowPage.getters
.contextMenuAction('toggle_pin')
.parent()
Expand Down
6 changes: 6 additions & 0 deletions cypress/pages/workflow-executions-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export class WorkflowExecutionsTab extends BasePage {

actions = {
toggleNodeEnabled: (nodeName: string) => {
cy.ifCanvasVersion(
() => {},
() => {
cy.get('body').click(); // Cancel selection if it exists
},
);
workflowPage.getters.canvasNodeByName(nodeName).click();
cy.get('body').type('d', { force: true });
},
Expand Down
13 changes: 7 additions & 6 deletions cypress/pages/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BasePage } from './base';
import { NodeCreator } from './features/node-creator';
import { META_KEY } from '../constants';
import type { OpenContextMenuOptions } from '../types';
import { getVisibleSelect } from '../utils';
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';

Expand Down Expand Up @@ -96,7 +97,7 @@ export class WorkflowPage extends BasePage {
disabledNodes: () =>
cy.ifCanvasVersion(
() => cy.get('.node-box.disabled'),
() => cy.get('[data-test-id="canvas-trigger-node"][class*="disabled"]'),
() => cy.get('[data-test-id*="node"][class*="disabled"]'),
),
selectedNodes: () =>
cy.ifCanvasVersion(
Expand Down Expand Up @@ -272,14 +273,14 @@ export class WorkflowPage extends BasePage {
},
openContextMenu: (
nodeTypeName?: string,
method: 'right-click' | 'overflow-button' = 'right-click',
{ method = 'right-click', anchor = 'center' }: OpenContextMenuOptions = {},
) => {
const target = nodeTypeName
? this.getters.canvasNodeByName(nodeTypeName)
: this.getters.nodeViewBackground();

if (method === 'right-click') {
target.rightclick(nodeTypeName ? 'center' : 'topLeft', { force: true });
target.rightclick(nodeTypeName ? anchor : 'topLeft', { force: true });
} else {
target.realHover();
target.find('[data-test-id="overflow-node-button"]').click({ force: true });
Expand All @@ -296,8 +297,8 @@ export class WorkflowPage extends BasePage {
this.actions.openContextMenu(nodeTypeName);
this.actions.contextMenuAction('delete');
},
executeNode: (nodeTypeName: string) => {
this.actions.openContextMenu(nodeTypeName);
executeNode: (nodeTypeName: string, options?: OpenContextMenuOptions) => {
this.actions.openContextMenu(nodeTypeName, options);
this.actions.contextMenuAction('execute');
},
addStickyFromContextMenu: () => {
Expand All @@ -324,7 +325,7 @@ export class WorkflowPage extends BasePage {
this.actions.contextMenuAction('toggle_pin');
},
openNodeFromContextMenu: (nodeTypeName: string) => {
this.actions.openContextMenu(nodeTypeName, 'overflow-button');
this.actions.openContextMenu(nodeTypeName, { method: 'overflow-button' });
this.actions.contextMenuAction('open');
},
selectAllFromContextMenu: () => {
Expand Down
5 changes: 5 additions & 0 deletions cypress/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ export interface ExecutionResponse {
results: Execution[];
};
}

export type OpenContextMenuOptions = {
method?: 'right-click' | 'overflow-button';
anchor?: 'topRight' | 'topLeft' | 'center' | 'bottomRight' | 'bottomLeft';
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function getOutputParserSchema(outputParser: N8nOutputParser): ZodObject<any, an
}

async function extractBinaryMessages(ctx: IExecuteFunctions) {
const binaryData = ctx.getInputData(0, 'main')?.[0]?.binary ?? {};
const binaryData = ctx.getInputData()?.[0]?.binary ?? {};
const binaryMessages = await Promise.all(
Object.values(binaryData)
.filter((data) => data.mimeType.startsWith('image/'))
Expand Down Expand Up @@ -260,7 +260,7 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise<INodeE
['human', '{input}'],
];

const hasBinaryData = this.getInputData(0, 'main')?.[0]?.binary !== undefined;
const hasBinaryData = this.getInputData()?.[0]?.binary !== undefined;
if (hasBinaryData && passthroughBinaryImages) {
const binaryMessage = await extractBinaryMessages(this);
messages.push(binaryMessage);
Expand Down
5 changes: 2 additions & 3 deletions packages/@n8n/nodes-langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
"clean": "rimraf dist .turbo",
"dev": "pnpm run watch",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json && pnpm n8n-copy-icons && pnpm build:metadata",
"build:metadata": "pnpm n8n-generate-known && pnpm n8n-generate-ui-types",
"build": "tsc -p tsconfig.build.json && pnpm n8n-copy-icons && pnpm n8n-generate-metadata",
"format": "biome format --write .",
"format:check": "biome ci .",
"lint": "eslint nodes credentials --quiet",
"lintfix": "eslint nodes credentials --fix",
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\" --onSuccess \"pnpm n8n-generate-ui-types\"",
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\" --onSuccess \"pnpm n8n-generate-metadata\"",
"test": "jest",
"test:dev": "jest --watch"
},
Expand Down
140 changes: 110 additions & 30 deletions packages/cli/src/__tests__/credential-types.test.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,121 @@
import { Container } from 'typedi';
import { mock } from 'jest-mock-extended';
import { UnrecognizedCredentialTypeError } from 'n8n-core';
import type { ICredentialType, LoadedClass } from 'n8n-workflow';

import { CredentialTypes } from '@/credential-types';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { mockInstance } from '@test/mocking';
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';

describe('CredentialTypes', () => {
const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
loadedCredentials: {
fakeFirstCredential: {
type: {
name: 'fakeFirstCredential',
displayName: 'Fake First Credential',
properties: [],
},
sourcePath: '',
},
fakeSecondCredential: {
type: {
name: 'fakeSecondCredential',
displayName: 'Fake Second Credential',
properties: [],
},
sourcePath: '',
},
},
const loadNodesAndCredentials = mock<LoadNodesAndCredentials>();

const credentialTypes = new CredentialTypes(loadNodesAndCredentials);

const testCredential: LoadedClass<ICredentialType> = {
sourcePath: '',
type: mock(),
};

loadNodesAndCredentials.getCredential.mockImplementation((credentialType) => {
if (credentialType === 'testCredential') return testCredential;
throw new UnrecognizedCredentialTypeError(credentialType);
});

beforeEach(() => {
jest.clearAllMocks();
});

describe('getByName', () => {
test('Should throw error when calling invalid credential name', () => {
expect(() => credentialTypes.getByName('unknownCredential')).toThrowError('c');
});

test('Should return correct credential type for valid name', () => {
expect(credentialTypes.getByName('testCredential')).toStrictEqual(testCredential.type);
});
});

describe('recognizes', () => {
test('Should recognize credential type that exists in knownCredentials', () => {
const credentialTypes = new CredentialTypes(
mock<LoadNodesAndCredentials>({
loadedCredentials: {},
knownCredentials: { testCredential: mock({ supportedNodes: [] }) },
}),
);

expect(credentialTypes.recognizes('testCredential')).toBe(true);
});

test('Should recognize credential type that exists in loadedCredentials', () => {
const credentialTypes = new CredentialTypes(
mock<LoadNodesAndCredentials>({
loadedCredentials: { testCredential },
knownCredentials: {},
}),
);

expect(credentialTypes.recognizes('testCredential')).toBe(true);
});

test('Should not recognize unknown credential type', () => {
expect(credentialTypes.recognizes('unknownCredential')).toBe(false);
});
});

const credentialTypes = Container.get(CredentialTypes);
describe('getSupportedNodes', () => {
test('Should return supported nodes for known credential type', () => {
const supportedNodes = ['node1', 'node2'];
const credentialTypes = new CredentialTypes(
mock<LoadNodesAndCredentials>({
knownCredentials: { testCredential: mock({ supportedNodes }) },
}),
);

expect(credentialTypes.getSupportedNodes('testCredential')).toEqual(supportedNodes);
});

test('Should throw error when calling invalid credential name', () => {
expect(() => credentialTypes.getByName('fakeThirdCredential')).toThrowError();
test('Should return empty array for unknown credential type supported nodes', () => {
expect(credentialTypes.getSupportedNodes('unknownCredential')).toBeEmptyArray();
});
});

test('Should return correct credential type for valid name', () => {
const mockedCredentialTypes = mockNodesAndCredentials.loadedCredentials;
expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual(
mockedCredentialTypes.fakeFirstCredential.type,
);
describe('getParentTypes', () => {
test('Should return parent types for credential type with extends', () => {
const credentialTypes = new CredentialTypes(
mock<LoadNodesAndCredentials>({
knownCredentials: {
childType: { extends: ['parentType1', 'parentType2'] },
parentType1: { extends: ['grandparentType'] },
parentType2: { extends: [] },
grandparentType: { extends: [] },
},
}),
);

const parentTypes = credentialTypes.getParentTypes('childType');
expect(parentTypes).toContain('parentType1');
expect(parentTypes).toContain('parentType2');
expect(parentTypes).toContain('grandparentType');
});

test('Should return empty array for credential type without extends', () => {
const credentialTypes = new CredentialTypes(
mock<LoadNodesAndCredentials>({
knownCredentials: { testCredential: { extends: [] } },
}),
);

expect(credentialTypes.getParentTypes('testCredential')).toBeEmptyArray();
});

test('Should return empty array for unknown credential type parent types', () => {
const credentialTypes = new CredentialTypes(
mock<LoadNodesAndCredentials>({
knownCredentials: {},
}),
);

expect(credentialTypes.getParentTypes('unknownCredential')).toBeEmptyArray();
});
});
});
76 changes: 20 additions & 56 deletions packages/cli/src/__tests__/credentials-helper.test.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,30 @@
import { mock } from 'jest-mock-extended';
import type {
IAuthenticateGeneric,
ICredentialDataDecryptedObject,
ICredentialType,
IHttpRequestOptions,
INode,
INodeProperties,
INodeTypes,
} from 'n8n-workflow';
import { NodeConnectionType, deepCopy } from 'n8n-workflow';
import { Workflow } from 'n8n-workflow';
import Container from 'typedi';
import { deepCopy, Workflow } from 'n8n-workflow';

import { CredentialTypes } from '@/credential-types';
import { CredentialsHelper } from '@/credentials-helper';
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { NodeTypes } from '@/node-types';
import { mockInstance } from '@test/mocking';
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';

describe('CredentialsHelper', () => {
mockInstance(CredentialsRepository);
mockInstance(SharedCredentialsRepository);
const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
loadedNodes: {
'test.set': {
sourcePath: '',
type: {
description: {
displayName: 'Set',
name: 'set',
group: ['input'],
version: 1,
description: 'Sets a value',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
properties: [
{
displayName: 'Value1',
name: 'value1',
type: 'string',
default: 'default-value1',
},
{
displayName: 'Value2',
name: 'value2',
type: 'string',
default: 'default-value2',
},
],
},
},
},
},
});
const nodeTypes = mock<INodeTypes>();
const mockNodesAndCredentials = mock<LoadNodesAndCredentials>();

const nodeTypes = mockInstance(NodeTypes);
const credentialsHelper = new CredentialsHelper(
new CredentialTypes(mockNodesAndCredentials),
mock(),
mock(),
mock(),
mock(),
);

describe('authenticate', () => {
const tests: Array<{
Expand Down Expand Up @@ -272,19 +239,16 @@ describe('CredentialsHelper', () => {

for (const testData of tests) {
test(testData.description, async () => {
//@ts-expect-error `loadedCredentials` is a getter and we are replacing it here with a property
mockNodesAndCredentials.loadedCredentials = {
[testData.input.credentialType.name]: {
type: testData.input.credentialType,
sourcePath: '',
},
};
const { credentialType } = testData.input;

const credentialsHelper = Container.get(CredentialsHelper);
mockNodesAndCredentials.getCredential.calledWith(credentialType.name).mockReturnValue({
type: credentialType,
sourcePath: '',
});

const result = await credentialsHelper.authenticate(
testData.input.credentials,
testData.input.credentialType.name,
credentialType.name,
deepCopy(incomingRequestOptions),
workflow,
node,
Expand Down
Loading

0 comments on commit 6224ab6

Please sign in to comment.