n8n-nodes-pytenable
Version:
Un nodo de n8n para interactuar con la API de Tenable usando Pytenable en un sandbox de Docker.
156 lines (126 loc) • 5.92 kB
text/typescript
import {jest, describe, it, expect, beforeEach} from '@jest/globals';
import type { IExecuteFunctions, INodeExecutionData, INodeType } from 'n8n-workflow';
import { PassThrough } from 'stream';
const mockAttach = jest.fn();
const mockStart = jest.fn();
const mockContainer = {
attach: mockAttach,
start: mockStart,
};
const mockCreateContainer = jest.fn().mockResolvedValue(mockContainer);
jest.mock('dockerode', () => {
return jest.fn().mockImplementation(() => {
return {
createContainer: mockCreateContainer,
};
});
});
// A helper to create the mock Docker stream header for multiplexed streams
const createDockerStreamChunk = (channel: 1 | 2, data: string): Buffer => {
const header = Buffer.alloc(8);
header.writeUInt8(channel, 0); // 1 for stdout, 2 for stderr
header.writeUInt32BE(data.length, 4);
return Buffer.concat([header, Buffer.from(data)]);
};
describe('Pytenable Node with Docker', () => {
let executeFunctions: IExecuteFunctions;
let Pytenable: new () => INodeType;
beforeEach(async () => {
const module = await import('./Pytenable.node.js');
Pytenable = module.Pytenable;
jest.clearAllMocks();
mockAttach.mockClear();
mockStart.mockClear();
mockCreateContainer.mockClear();
jest.setTimeout(30000);
executeFunctions = {
getNode: jest.fn().mockReturnValue({
getContext: jest.fn().mockReturnValue({
executionId: 'test-execution',
node: { name: 'Pytenable', type: 'Pytenable', id: 'test-node-id' },
}),
}),
getNodeParameter: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({}),
getInputData: jest.fn(),
helpers: {
returnJsonArray: jest.fn(data => data as INodeExecutionData[]),
constructExecutionMetaData: jest.fn((data, options) =>
data.map((item: Record<string, unknown>) => ({ json: item, pairedItem: options.pairedItem }))
),
},
} as unknown as IExecuteFunctions;
});
it('should execute for all items at once successfully', async () => {
// Arrange
(executeFunctions.getNodeParameter as jest.Mock)
.mockReturnValueOnce('runOnceForAllItems')
.mockReturnValueOnce('return [{"status": "ok"}]');
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: { id: 1 } }]);
(executeFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation((data) => data.map((item: Record<string, unknown>) => ({json: item})));
const mockStream = new PassThrough();
(mockStream.end as jest.Mock) = jest.fn();
mockAttach.mockResolvedValue(mockStream);
mockStart.mockImplementation((cb: (err: Error | null) => void) => cb(null));
// Act
const executionPromise = new Pytenable().execute.call(executeFunctions);
// Simulate container output and end the stream manually for the node's listener
await new Promise(resolve => setImmediate(resolve)); // Wait for listeners to attach
const output = JSON.stringify([{ status: 'ok' }]);
mockStream.write(createDockerStreamChunk(1, output));
mockStream.emit('end');
const result = await executionPromise;
// Assert
expect(result[0]).toEqual([expect.objectContaining({ json: { status: 'ok' } })]);
expect(mockCreateContainer).toHaveBeenCalledTimes(1);
});
it('should execute for each item individually and pair results', async () => {
// Arrange
(executeFunctions.getNodeParameter as jest.Mock)
.mockReturnValueOnce('runOnceForEachItem')
.mockReturnValueOnce('return [{"id": _item["id"] * 2}]');
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{ json: { id: 1 } }, { json: { id: 2 } }]);
(executeFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation(data => data);
(executeFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation((data) => data.map((item: Record<string, unknown>) => ({ json: item })));
const mockStreams = [new PassThrough(), new PassThrough()];
mockStreams.forEach(s => { (s.end as jest.Mock) = jest.fn(); });
mockAttach
.mockResolvedValueOnce(mockStreams[0])
.mockResolvedValueOnce(mockStreams[1]);
mockStart.mockImplementation((cb: (err: Error | null) => void) => cb(null));
// Act
const executionPromise = new Pytenable().execute.call(executeFunctions);
// Simulate responses for each item
await new Promise(resolve => setImmediate(resolve));
mockStreams[0].write(createDockerStreamChunk(1, JSON.stringify([{ id: 2 }])));
mockStreams[0].emit('end');
await new Promise(resolve => setImmediate(resolve));
mockStreams[1].write(createDockerStreamChunk(1, JSON.stringify([{ id: 4 }])));
mockStreams[1].emit('end');
const result = await executionPromise;
// Assert
const returnData = result[0];
expect(returnData.length).toBe(2);
expect(returnData[0]).toEqual({ json: { id: 2 }, pairedItem: { item: 0 } });
expect(returnData[1]).toEqual({ json: { id: 4 }, pairedItem: { item: 1 } });
});
it('should handle Python script error from stderr', async () => {
// Arrange
(executeFunctions.getNodeParameter as jest.Mock)
.mockReturnValueOnce('runOnceForAllItems')
.mockReturnValueOnce('raise Exception("Boom!")');
(executeFunctions.getInputData as jest.Mock).mockReturnValue([{}]);
const mockStream = new PassThrough();
(mockStream.end as jest.Mock) = jest.fn();
mockAttach.mockResolvedValue(mockStream);
mockStart.mockImplementation((cb: (err: Error | null) => void) => cb(null));
// Act
const executionPromise = new Pytenable().execute.call(executeFunctions);
await new Promise(resolve => setImmediate(resolve));
const errorOutput = "Traceback... Exception: Boom!";
mockStream.write(createDockerStreamChunk(2, errorOutput));
mockStream.emit('end');
// Assert
await expect(executionPromise).rejects.toThrow('Error en el contenedor de Python: Traceback... Exception: Boom!');
});
});