mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-03 17:07:29 -08:00
782 lines
25 KiB
TypeScript
782 lines
25 KiB
TypeScript
import { mock } from 'jest-mock-extended';
|
|
import type {
|
|
ICredentialType,
|
|
INodeType,
|
|
INodeTypeDescription,
|
|
IVersionedNodeType,
|
|
} from 'n8n-workflow';
|
|
import { deepCopy } from 'n8n-workflow';
|
|
import fs from 'node:fs';
|
|
import fsPromises from 'node:fs/promises';
|
|
|
|
jest.mock('node:fs');
|
|
jest.mock('node:fs/promises');
|
|
const mockFs = mock<typeof fs>();
|
|
const mockFsPromises = mock<typeof fsPromises>();
|
|
fs.readFileSync = mockFs.readFileSync;
|
|
fsPromises.readFile = mockFsPromises.readFile;
|
|
|
|
jest.mock('fast-glob', () => async (pattern: string) => {
|
|
return pattern.endsWith('.node.js')
|
|
? ['dist/Node1/Node1.node.js', 'dist/Node2/Node2.node.js']
|
|
: ['dist/Credential1.js'];
|
|
});
|
|
|
|
import * as classLoader from '@/ClassLoader';
|
|
import {
|
|
CustomDirectoryLoader,
|
|
PackageDirectoryLoader,
|
|
LazyPackageDirectoryLoader,
|
|
} from '@/DirectoryLoader';
|
|
|
|
describe('DirectoryLoader', () => {
|
|
const directory = '/not/a/real/path';
|
|
const packageJson = JSON.stringify({
|
|
name: 'n8n-nodes-testing',
|
|
n8n: {
|
|
credentials: ['dist/Credential1.js'],
|
|
nodes: ['dist/Node1/Node1.node.js', 'dist/Node2/Node2.node.js'],
|
|
},
|
|
});
|
|
|
|
const createNode = (name: string, credential?: string) =>
|
|
mock<INodeType>({
|
|
description: {
|
|
name,
|
|
version: 1,
|
|
icon: `file:${name}.svg`,
|
|
iconUrl: undefined,
|
|
credentials: credential ? [{ name: credential }] : [],
|
|
properties: [],
|
|
},
|
|
});
|
|
|
|
const createCredential = (name: string) =>
|
|
mock<ICredentialType>({
|
|
name,
|
|
icon: `file:${name}.svg`,
|
|
iconUrl: undefined,
|
|
extends: undefined,
|
|
properties: [],
|
|
});
|
|
|
|
let mockCredential1: ICredentialType, mockNode1: INodeType, mockNode2: INodeType;
|
|
|
|
beforeEach(() => {
|
|
mockCredential1 = createCredential('credential1');
|
|
mockNode1 = createNode('node1', 'credential1');
|
|
mockNode2 = createNode('node2');
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
//@ts-expect-error overwrite a readonly property
|
|
classLoader.loadClassInIsolation = jest.fn((_: string, className: string) => {
|
|
if (className === 'Node1') return mockNode1;
|
|
if (className === 'Node2') return mockNode2;
|
|
if (className === 'Credential1') return mockCredential1;
|
|
throw new Error(`${className} is invalid`);
|
|
});
|
|
|
|
describe('CustomDirectoryLoader', () => {
|
|
it('should load custom nodes and credentials', async () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
expect(loader.packageName).toEqual('CUSTOM');
|
|
|
|
await loader.loadAll();
|
|
|
|
expect(loader.isLazyLoaded).toBe(false);
|
|
expect(mockFsPromises.readFile).not.toHaveBeenCalled();
|
|
expect(classLoader.loadClassInIsolation).toHaveBeenCalledTimes(3);
|
|
|
|
expect(loader.nodesByCredential).toEqual({ credential1: ['node1'] });
|
|
expect(loader.credentialTypes).toEqual({
|
|
credential1: { sourcePath: 'dist/Credential1.js', type: mockCredential1 },
|
|
});
|
|
expect(loader.nodeTypes).toEqual({
|
|
node1: { sourcePath: 'dist/Node1/Node1.node.js', type: mockNode1 },
|
|
node2: { sourcePath: 'dist/Node2/Node2.node.js', type: mockNode2 },
|
|
});
|
|
expect(mockCredential1.iconUrl).toBe('icons/CUSTOM/dist/credential1.svg');
|
|
expect(mockNode1.description.iconUrl).toBe('icons/CUSTOM/dist/Node1/node1.svg');
|
|
expect(mockNode2.description.iconUrl).toBe('icons/CUSTOM/dist/Node2/node2.svg');
|
|
|
|
expect(mockFs.readFileSync).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('PackageDirectoryLoader', () => {
|
|
it('should load nodes and credentials from an installed package', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
|
|
const loader = new PackageDirectoryLoader(directory);
|
|
expect(loader.packageName).toEqual('n8n-nodes-testing');
|
|
|
|
await loader.loadAll();
|
|
|
|
expect(loader.isLazyLoaded).toBe(false);
|
|
expect(mockFsPromises.readFile).not.toHaveBeenCalled();
|
|
expect(classLoader.loadClassInIsolation).toHaveBeenCalledTimes(3);
|
|
|
|
expect(loader.nodesByCredential).toEqual({ credential1: ['node1'] });
|
|
expect(loader.credentialTypes).toEqual({
|
|
credential1: { sourcePath: 'dist/Credential1.js', type: mockCredential1 },
|
|
});
|
|
expect(loader.nodeTypes).toEqual({
|
|
node1: { sourcePath: 'dist/Node1/Node1.node.js', type: mockNode1 },
|
|
node2: { sourcePath: 'dist/Node2/Node2.node.js', type: mockNode2 },
|
|
});
|
|
expect(mockCredential1.iconUrl).toBe('icons/n8n-nodes-testing/dist/credential1.svg');
|
|
expect(mockNode1.description.iconUrl).toBe('icons/n8n-nodes-testing/dist/Node1/node1.svg');
|
|
expect(mockNode2.description.iconUrl).toBe('icons/n8n-nodes-testing/dist/Node2/node2.svg');
|
|
});
|
|
|
|
it('should throw error when package.json is missing', async () => {
|
|
mockFs.readFileSync.mockImplementationOnce(() => {
|
|
throw new Error('ENOENT');
|
|
});
|
|
|
|
expect(() => new PackageDirectoryLoader(directory)).toThrow();
|
|
});
|
|
|
|
it('should throw error when package.json is invalid', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue('invalid json');
|
|
|
|
expect(() => new PackageDirectoryLoader(directory)).toThrow('Failed to parse JSON');
|
|
});
|
|
|
|
it('should do nothing if package.json has no n8n field', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(
|
|
JSON.stringify({
|
|
name: 'n8n-nodes-testing',
|
|
}),
|
|
);
|
|
|
|
const loader = new PackageDirectoryLoader(directory);
|
|
await loader.loadAll();
|
|
|
|
expect(loader.nodeTypes).toEqual({});
|
|
expect(loader.credentialTypes).toEqual({});
|
|
expect(classLoader.loadClassInIsolation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should hide httpRequestNode property when credential has supported nodes', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
mockCredential1.httpRequestNode = mock<ICredentialType['httpRequestNode']>({ hidden: false });
|
|
|
|
const loader = new PackageDirectoryLoader(directory);
|
|
await loader.loadAll();
|
|
|
|
expect(mockCredential1.httpRequestNode?.hidden).toBe(true);
|
|
});
|
|
|
|
it('should not modify httpRequestNode when credential has no supported nodes', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
mockCredential1.httpRequestNode = mock<ICredentialType['httpRequestNode']>({ hidden: false });
|
|
mockNode1.description.credentials = [];
|
|
|
|
const loader = new PackageDirectoryLoader(directory);
|
|
await loader.loadAll();
|
|
|
|
expect(mockCredential1.httpRequestNode?.hidden).toBe(false);
|
|
});
|
|
|
|
it('should inherit iconUrl from supported node when credential has no icon', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
mockCredential1.icon = undefined;
|
|
|
|
const loader = new PackageDirectoryLoader(directory);
|
|
await loader.loadAll();
|
|
|
|
expect(mockCredential1.supportedNodes).toEqual(['node1']);
|
|
expect(mockCredential1.iconUrl).toBe(mockNode1.description.iconUrl);
|
|
});
|
|
});
|
|
|
|
describe('LazyPackageDirectoryLoader', () => {
|
|
it('should skip loading nodes and credentials from a lazy-loadable package', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
mockFsPromises.readFile.mockResolvedValue('[]');
|
|
|
|
const loader = new LazyPackageDirectoryLoader(directory);
|
|
expect(loader.packageName).toEqual('n8n-nodes-testing');
|
|
|
|
await loader.loadAll();
|
|
|
|
expect(loader.isLazyLoaded).toBe(true);
|
|
expect(mockFsPromises.readFile).toHaveBeenCalledTimes(4);
|
|
expect(classLoader.loadClassInIsolation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should fall back to non-lazy loading if any json file fails to parse', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
mockFsPromises.readFile.mockRejectedValue(new Error('Failed to read file'));
|
|
|
|
const loader = new LazyPackageDirectoryLoader(directory);
|
|
await loader.loadAll();
|
|
|
|
expect(loader.isLazyLoaded).toBe(false);
|
|
expect(mockFsPromises.readFile).toHaveBeenCalled();
|
|
expect(classLoader.loadClassInIsolation).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('should only load included nodes when includeNodes is set', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
|
|
mockFsPromises.readFile.mockImplementation(async (path) => {
|
|
if (typeof path !== 'string') throw new Error('Invalid path');
|
|
|
|
if (path.endsWith('known/nodes.json')) {
|
|
return JSON.stringify({
|
|
node1: { className: 'Node1', sourcePath: 'dist/Node1/Node1.node.js' },
|
|
node2: { className: 'Node2', sourcePath: 'dist/Node2/Node2.node.js' },
|
|
});
|
|
}
|
|
if (path.endsWith('known/credentials.json')) {
|
|
return JSON.stringify({});
|
|
}
|
|
if (path.endsWith('types/nodes.json')) {
|
|
return JSON.stringify([
|
|
{ name: 'n8n-nodes-testing.node1' },
|
|
{ name: 'n8n-nodes-testing.node2' },
|
|
]);
|
|
}
|
|
if (path.endsWith('types/credentials.json')) {
|
|
return JSON.stringify([]);
|
|
}
|
|
throw new Error('File not found');
|
|
});
|
|
|
|
const loader = new LazyPackageDirectoryLoader(directory, [], ['n8n-nodes-testing.node1']);
|
|
await loader.loadAll();
|
|
|
|
expect(loader.isLazyLoaded).toBe(true);
|
|
expect(loader.known.nodes).toEqual({
|
|
node1: { className: 'Node1', sourcePath: 'dist/Node1/Node1.node.js' },
|
|
});
|
|
expect(loader.types.nodes).toHaveLength(1);
|
|
expect(loader.types.nodes[0].name).toBe('n8n-nodes-testing.node1');
|
|
expect(classLoader.loadClassInIsolation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should load no nodes when includeNodes does not match any nodes', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
|
|
mockFsPromises.readFile.mockImplementation(async (path) => {
|
|
if (typeof path !== 'string') throw new Error('Invalid path');
|
|
|
|
if (path.endsWith('known/nodes.json')) {
|
|
return JSON.stringify({
|
|
node1: { className: 'Node1', sourcePath: 'dist/Node1/Node1.node.js' },
|
|
node2: { className: 'Node2', sourcePath: 'dist/Node2/Node2.node.js' },
|
|
});
|
|
}
|
|
if (path.endsWith('known/credentials.json')) {
|
|
return JSON.stringify({});
|
|
}
|
|
if (path.endsWith('types/nodes.json')) {
|
|
return JSON.stringify([
|
|
{ name: 'n8n-nodes-testing.node1' },
|
|
{ name: 'n8n-nodes-testing.node2' },
|
|
]);
|
|
}
|
|
if (path.endsWith('types/credentials.json')) {
|
|
return JSON.stringify([]);
|
|
}
|
|
throw new Error('File not found');
|
|
});
|
|
|
|
const loader = new LazyPackageDirectoryLoader(
|
|
directory,
|
|
[],
|
|
['n8n-nodes-testing.nonexistent'],
|
|
);
|
|
await loader.loadAll();
|
|
|
|
expect(loader.isLazyLoaded).toBe(true);
|
|
expect(loader.known.nodes).toEqual({});
|
|
expect(loader.types.nodes).toHaveLength(0);
|
|
expect(classLoader.loadClassInIsolation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should exclude specified nodes when excludeNodes is set', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
|
|
mockFsPromises.readFile.mockImplementation(async (path) => {
|
|
if (typeof path !== 'string') throw new Error('Invalid path');
|
|
|
|
if (path.endsWith('known/nodes.json')) {
|
|
return JSON.stringify({
|
|
node1: { className: 'Node1', sourcePath: 'dist/Node1/Node1.node.js' },
|
|
node2: { className: 'Node2', sourcePath: 'dist/Node2/Node2.node.js' },
|
|
});
|
|
}
|
|
if (path.endsWith('known/credentials.json')) {
|
|
return JSON.stringify({});
|
|
}
|
|
if (path.endsWith('types/nodes.json')) {
|
|
return JSON.stringify([
|
|
{ name: 'n8n-nodes-testing.node1' },
|
|
{ name: 'n8n-nodes-testing.node2' },
|
|
]);
|
|
}
|
|
if (path.endsWith('types/credentials.json')) {
|
|
return JSON.stringify([]);
|
|
}
|
|
throw new Error('File not found');
|
|
});
|
|
|
|
const loader = new LazyPackageDirectoryLoader(directory, ['n8n-nodes-testing.node1']);
|
|
await loader.loadAll();
|
|
|
|
expect(loader.isLazyLoaded).toBe(true);
|
|
expect(loader.known.nodes).toEqual({
|
|
node2: { className: 'Node2', sourcePath: 'dist/Node2/Node2.node.js' },
|
|
});
|
|
expect(loader.types.nodes).toHaveLength(1);
|
|
expect(loader.types.nodes[0].name).toBe('n8n-nodes-testing.node2');
|
|
expect(classLoader.loadClassInIsolation).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('reset()', () => {
|
|
it('should reset all properties to their initial state', async () => {
|
|
mockFs.readFileSync.calledWith(`${directory}/package.json`).mockReturnValue(packageJson);
|
|
|
|
const loader = new PackageDirectoryLoader(directory);
|
|
await loader.loadAll();
|
|
|
|
// Verify loader has loaded data
|
|
expect(loader.nodeTypes).not.toEqual({});
|
|
expect(loader.credentialTypes).not.toEqual({});
|
|
expect(loader.types.nodes.length).toBeGreaterThan(0);
|
|
expect(loader.types.credentials.length).toBeGreaterThan(0);
|
|
expect(loader.loadedNodes.length).toBeGreaterThan(0);
|
|
expect(Object.keys(loader.known.nodes).length).toBeGreaterThan(0);
|
|
expect(Object.keys(loader.known.credentials).length).toBeGreaterThan(0);
|
|
|
|
// Reset the loader
|
|
loader.reset();
|
|
|
|
// Verify all properties are reset
|
|
expect(loader.nodeTypes).toEqual({});
|
|
expect(loader.credentialTypes).toEqual({});
|
|
expect(loader.types.nodes).toEqual([]);
|
|
expect(loader.types.credentials).toEqual([]);
|
|
expect(loader.loadedNodes).toEqual([]);
|
|
expect(loader.known.nodes).toEqual({});
|
|
expect(loader.known.credentials).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('getVersionedNodeTypeAll', () => {
|
|
it('should return array with single node for non-versioned node', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const node = createNode('node1');
|
|
|
|
const result = loader.getVersionedNodeTypeAll(node);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toBe(node);
|
|
});
|
|
|
|
it('should return all versions of a versioned node', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const nodeV1 = createNode('test');
|
|
const nodeV2 = createNode('test');
|
|
nodeV1.description.version = 1;
|
|
nodeV2.description.version = 2;
|
|
|
|
const versionedNode = mock<IVersionedNodeType>({
|
|
description: { name: 'test', codex: {} },
|
|
currentVersion: 2,
|
|
nodeVersions: {
|
|
1: nodeV1,
|
|
2: nodeV2,
|
|
},
|
|
});
|
|
|
|
const result = loader.getVersionedNodeTypeAll(versionedNode);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result).toEqual([nodeV2, nodeV1]);
|
|
expect(result[0].description.name).toBe('test');
|
|
expect(result[1].description.name).toBe('test');
|
|
});
|
|
});
|
|
|
|
describe('getCredentialsForNode', () => {
|
|
it('should return empty array if node has no credentials', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const node = createNode('node1');
|
|
|
|
const result = loader.getCredentialsForNode(node);
|
|
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toEqual(0);
|
|
});
|
|
|
|
it('should return credentials for non-versioned node', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const node = createNode('node1', 'testCred');
|
|
|
|
const result = loader.getCredentialsForNode(node);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe('testCred');
|
|
});
|
|
|
|
it('should return unique credentials from all versions of a versioned node', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const nodeV1 = createNode('test', 'cred1');
|
|
const nodeV2 = createNode('test', 'cred2');
|
|
|
|
const versionedNode = mock<IVersionedNodeType>({
|
|
description: { name: 'test' },
|
|
currentVersion: 2,
|
|
nodeVersions: {
|
|
1: nodeV1,
|
|
2: nodeV2,
|
|
},
|
|
});
|
|
|
|
const result = loader.getCredentialsForNode(versionedNode);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0].name).toBe('cred1');
|
|
expect(result[1].name).toBe('cred2');
|
|
});
|
|
|
|
it('should remove duplicate credentials from different versions', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const nodeV1 = createNode('test', 'cred1');
|
|
const nodeV2 = createNode('test', 'cred1'); // Same credential
|
|
|
|
const versionedNode = mock<IVersionedNodeType>({
|
|
description: { name: 'test' },
|
|
currentVersion: 2,
|
|
nodeVersions: {
|
|
1: nodeV1,
|
|
2: nodeV2,
|
|
},
|
|
});
|
|
|
|
const result = loader.getCredentialsForNode(versionedNode);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].name).toBe('cred1');
|
|
});
|
|
});
|
|
|
|
describe('loadCredentialFromFile', () => {
|
|
it('should load credential and store it correctly', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Credential1.js';
|
|
|
|
loader.loadCredentialFromFile(filePath);
|
|
|
|
expect(loader.credentialTypes).toEqual({
|
|
credential1: {
|
|
type: mockCredential1,
|
|
sourcePath: filePath,
|
|
},
|
|
});
|
|
|
|
expect(loader.known.credentials).toEqual({
|
|
credential1: {
|
|
className: mockCredential1.constructor.name,
|
|
sourcePath: filePath,
|
|
extends: undefined,
|
|
supportedNodes: undefined,
|
|
},
|
|
});
|
|
|
|
expect(loader.types.credentials).toEqual([mockCredential1]);
|
|
});
|
|
|
|
it('should update credential icon paths', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Credential1.js';
|
|
|
|
const credWithIcon = createCredential('credentialWithIcon');
|
|
credWithIcon.icon = {
|
|
light: 'file:light.svg',
|
|
dark: 'file:dark.svg',
|
|
};
|
|
|
|
jest.spyOn(classLoader, 'loadClassInIsolation').mockReturnValueOnce(credWithIcon);
|
|
|
|
loader.loadCredentialFromFile(filePath);
|
|
|
|
expect(credWithIcon.iconUrl).toEqual({
|
|
light: 'icons/CUSTOM/dist/light.svg',
|
|
dark: 'icons/CUSTOM/dist/dark.svg',
|
|
});
|
|
expect(credWithIcon.icon).toBeUndefined();
|
|
});
|
|
|
|
it('should add toJSON method to credential type', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Credential1.js';
|
|
|
|
const credWithAuth = createCredential('credWithAuth');
|
|
credWithAuth.authenticate = jest.fn();
|
|
|
|
jest.spyOn(classLoader, 'loadClassInIsolation').mockReturnValueOnce(credWithAuth);
|
|
|
|
loader.loadCredentialFromFile(filePath);
|
|
|
|
const serialized = deepCopy(credWithAuth);
|
|
expect(serialized.authenticate).toEqual({});
|
|
});
|
|
|
|
it('should store credential extends and supported nodes info', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Credential1.js';
|
|
|
|
const extendingCred = createCredential('extendingCred');
|
|
extendingCred.extends = ['baseCredential'];
|
|
|
|
jest.spyOn(classLoader, 'loadClassInIsolation').mockReturnValueOnce(extendingCred);
|
|
|
|
// Set up nodesByCredential before loading
|
|
loader.nodesByCredential.extendingCred = ['node1', 'node2'];
|
|
|
|
loader.loadCredentialFromFile(filePath);
|
|
|
|
expect(loader.known.credentials.extendingCred).toEqual({
|
|
className: extendingCred.constructor.name,
|
|
sourcePath: filePath,
|
|
extends: ['baseCredential'],
|
|
supportedNodes: ['node1', 'node2'],
|
|
});
|
|
});
|
|
|
|
it('should throw error if credential class cannot be loaded', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/InvalidCred.js';
|
|
|
|
jest.spyOn(classLoader, 'loadClassInIsolation').mockImplementationOnce(() => {
|
|
throw new TypeError('Class not found');
|
|
});
|
|
|
|
expect(() => loader.loadCredentialFromFile(filePath)).toThrow('Class could not be found');
|
|
});
|
|
});
|
|
|
|
describe('getCredential', () => {
|
|
it('should return existing loaded credential type', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Credential1.js';
|
|
|
|
loader.loadCredentialFromFile(filePath);
|
|
|
|
const result = loader.getCredential('credential1');
|
|
expect(result).toEqual({
|
|
type: mockCredential1,
|
|
sourcePath: filePath,
|
|
});
|
|
});
|
|
|
|
it('should load credential from known credentials if not already loaded', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Credential1.js';
|
|
|
|
// Setup known credentials without loading
|
|
loader.known.credentials.credential1 = {
|
|
className: 'Credential1',
|
|
sourcePath: filePath,
|
|
};
|
|
|
|
const result = loader.getCredential('credential1');
|
|
|
|
expect(result).toEqual({
|
|
type: mockCredential1,
|
|
sourcePath: filePath,
|
|
});
|
|
expect(classLoader.loadClassInIsolation).toHaveBeenCalledWith(
|
|
expect.stringContaining(filePath),
|
|
'Credential1',
|
|
);
|
|
});
|
|
|
|
it('should throw UnrecognizedCredentialTypeError if credential type is not found', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
|
|
expect(() => loader.getCredential('nonexistent')).toThrow(
|
|
'Unrecognized credential type: nonexistent',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('loadNodeFromFile', () => {
|
|
it('should load node and store it correctly', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Node1/Node1.node.js';
|
|
|
|
loader.loadNodeFromFile(filePath);
|
|
|
|
expect(loader.nodeTypes).toEqual({
|
|
node1: {
|
|
type: mockNode1,
|
|
sourcePath: filePath,
|
|
},
|
|
});
|
|
|
|
expect(loader.known.nodes).toEqual({
|
|
node1: {
|
|
className: mockNode1.constructor.name,
|
|
sourcePath: filePath,
|
|
},
|
|
});
|
|
|
|
expect(loader.types.nodes).toEqual([mockNode1.description]);
|
|
expect(loader.loadedNodes).toEqual([{ name: 'node1', version: 1 }]);
|
|
});
|
|
|
|
it('should update node icon paths', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Node1/Node1.node.js';
|
|
|
|
const nodeWithIcon = createNode('nodeWithIcon');
|
|
nodeWithIcon.description.icon = {
|
|
light: 'file:light.svg',
|
|
dark: 'file:dark.svg',
|
|
};
|
|
|
|
jest.spyOn(classLoader, 'loadClassInIsolation').mockReturnValueOnce(nodeWithIcon);
|
|
|
|
loader.loadNodeFromFile(filePath);
|
|
|
|
expect(nodeWithIcon.description.iconUrl).toEqual({
|
|
light: 'icons/CUSTOM/dist/Node1/light.svg',
|
|
dark: 'icons/CUSTOM/dist/Node1/dark.svg',
|
|
});
|
|
expect(nodeWithIcon.description.icon).toBeUndefined();
|
|
});
|
|
|
|
it('should skip node if included in excludeNodes', () => {
|
|
const loader = new CustomDirectoryLoader(directory, ['CUSTOM.node1']);
|
|
const filePath = 'dist/Node1/Node1.node.js';
|
|
|
|
loader.loadNodeFromFile(filePath);
|
|
|
|
expect(loader.nodeTypes).toEqual({});
|
|
expect(loader.known.nodes).toEqual({});
|
|
expect(loader.types.nodes).toEqual([]);
|
|
expect(loader.loadedNodes).toEqual([]);
|
|
});
|
|
|
|
it('should skip node if not in includeNodes', () => {
|
|
const loader = new CustomDirectoryLoader(directory, [], ['CUSTOM.other']);
|
|
const filePath = 'dist/Node1/Node1.node.js';
|
|
|
|
loader.loadNodeFromFile(filePath);
|
|
|
|
expect(loader.nodeTypes).toEqual({});
|
|
expect(loader.known.nodes).toEqual({});
|
|
expect(loader.types.nodes).toEqual([]);
|
|
expect(loader.loadedNodes).toEqual([]);
|
|
});
|
|
|
|
it('should handle versioned nodes correctly', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Node1/Node1.node.js';
|
|
|
|
const nodeV1 = createNode('test');
|
|
const nodeV2 = createNode('test');
|
|
nodeV1.description.version = 1;
|
|
nodeV2.description.version = 2;
|
|
|
|
const versionedNode = mock<IVersionedNodeType>({
|
|
description: { name: 'test', codex: {}, iconUrl: undefined, icon: undefined },
|
|
currentVersion: 2,
|
|
nodeVersions: {
|
|
1: nodeV1,
|
|
2: nodeV2,
|
|
},
|
|
});
|
|
|
|
jest.spyOn(classLoader, 'loadClassInIsolation').mockReturnValueOnce(versionedNode);
|
|
|
|
loader.loadNodeFromFile(filePath);
|
|
|
|
expect(loader.loadedNodes).toEqual([{ name: 'test', version: 2 }]);
|
|
|
|
const nodes = loader.types.nodes as INodeTypeDescription[];
|
|
expect(nodes).toHaveLength(2);
|
|
expect(nodes[0]?.version).toBe(2);
|
|
expect(nodes[1]?.version).toBe(1);
|
|
});
|
|
|
|
it('should store credential associations correctly', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Node1/Node1.node.js';
|
|
|
|
const nodeWithCreds = createNode('testNode', 'testCred');
|
|
jest.spyOn(classLoader, 'loadClassInIsolation').mockReturnValueOnce(nodeWithCreds);
|
|
|
|
loader.loadNodeFromFile(filePath);
|
|
|
|
expect(loader.nodesByCredential).toEqual({
|
|
testCred: ['testNode'],
|
|
});
|
|
});
|
|
|
|
it('should throw error if node class cannot be loaded', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/InvalidNode/InvalidNode.node.js';
|
|
|
|
jest.spyOn(classLoader, 'loadClassInIsolation').mockImplementationOnce(() => {
|
|
throw new TypeError('Class not found');
|
|
});
|
|
|
|
expect(() => loader.loadNodeFromFile(filePath)).toThrow('Class could not be found');
|
|
});
|
|
});
|
|
|
|
describe('getNode', () => {
|
|
it('should return existing loaded node type', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Node1/Node1.node.js';
|
|
|
|
loader.loadNodeFromFile(filePath);
|
|
|
|
const result = loader.getNode('node1');
|
|
expect(result).toEqual({
|
|
type: mockNode1,
|
|
sourcePath: filePath,
|
|
});
|
|
});
|
|
|
|
it('should load node from known nodes if not already loaded', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
const filePath = 'dist/Node1/Node1.node.js';
|
|
|
|
// Setup known nodes without loading
|
|
loader.known.nodes.node1 = {
|
|
className: 'Node1',
|
|
sourcePath: filePath,
|
|
};
|
|
|
|
const result = loader.getNode('node1');
|
|
|
|
expect(result).toEqual({
|
|
type: mockNode1,
|
|
sourcePath: filePath,
|
|
});
|
|
expect(classLoader.loadClassInIsolation).toHaveBeenCalledWith(
|
|
expect.stringContaining(filePath),
|
|
'Node1',
|
|
);
|
|
});
|
|
|
|
it('should throw UnrecognizedNodeTypeError if node type is not found', () => {
|
|
const loader = new CustomDirectoryLoader(directory);
|
|
|
|
expect(() => loader.getNode('nonexistent')).toThrow(
|
|
'Unrecognized node type: CUSTOM.nonexistent',
|
|
);
|
|
});
|
|
});
|
|
});
|