Files
n8n-enterprise-unlocked/packages/nodes-base/nodes/Github/__tests__/node/Github.node.test.ts
कारतोफ्फेलस्क्रिप्ट™ 979f9e6327 refactor: Overhaul nodes-testing setup - Part 3 (no-changelog) (#14967)
2025-04-29 17:42:21 +02:00

530 lines
15 KiB
TypeScript

import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import nock from 'nock';
import { Github } from '../../Github.node';
describe('Test Github Node', () => {
describe('Workflow Dispatch', () => {
const now = 1683028800000;
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
const usersResponse = {
total_count: 12,
items: [
{
login: 'testOwner',
id: 1,
},
],
};
const repositoriesResponse = {
total_count: 40,
items: [
{
id: 3081286,
name: 'testRepository',
},
],
};
const workflowsResponse = {
total_count: 2,
workflows: [
{
id: workflowId,
node_id: 'MDg6V29ya2Zsb3cxNjEzMzU=',
name: 'CI',
path: '.github/workflows/blank.yaml',
state: 'active',
created_at: '2020-01-08T23:48:37.000-08:00',
updated_at: '2020-01-08T23:50:21.000-08:00',
url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/161335',
html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/161335',
badge_url: 'https://github.com/octo-org/octo-repo/workflows/CI/badge.svg',
},
{
id: 269289,
node_id: 'MDE4OldvcmtmbG93IFNlY29uZGFyeTI2OTI4OQ==',
name: 'Linter',
path: '.github/workflows/linter.yaml',
state: 'active',
created_at: '2020-01-08T23:48:37.000-08:00',
updated_at: '2020-01-08T23:50:21.000-08:00',
url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/269289',
html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/269289',
badge_url: 'https://github.com/octo-org/octo-repo/workflows/Linter/badge.svg',
},
],
};
beforeAll(async () => {
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
});
beforeEach(async () => {
const baseUrl = 'https://api.github.com';
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/search/users')
.query(true)
.reply(200, usersResponse)
.get('/search/repositories')
.query(true)
.reply(200, repositoriesResponse)
.get(`/repos/${owner}/${repository}/actions/workflows`)
.reply(200, workflowsResponse)
.post(`/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`, {
ref: 'main',
inputs: {},
})
.reply(200, {});
});
new NodeTestHarness().setupTests({
workflowFiles: ['GithubTestWorkflow.json'],
});
});
describe('Error Handling', () => {
let githubNode: Github;
let mockExecutionContext: any;
beforeEach(() => {
githubNode = new Github();
mockExecutionContext = {
getNode: jest.fn().mockReturnValue({ name: 'Github' }),
getNodeParameter: jest.fn(),
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
continueOnFail: jest.fn().mockReturnValue(false),
putExecutionToWait: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({
server: 'https://api.github.com',
user: 'test',
accessToken: 'test',
}),
helpers: {
returnJsonArray: jest.fn().mockReturnValue([{ json: {} }]),
httpRequest: jest.fn(),
httpRequestWithAuthentication: jest.fn(),
requestWithAuthentication: jest
.fn()
.mockImplementation(async (_credentialType, options) => {
if (options.uri.includes('dispatches') && options.method === 'POST') {
const error: any = new Error('Not Found');
error.statusCode = 404;
error.message = 'Not Found';
throw error;
}
return {};
}),
request: jest.fn(),
constructExecutionMetaData: jest.fn().mockReturnValue([{ json: {} }]),
assertBinaryData: jest.fn(),
prepareBinaryData: jest.fn(),
},
getWorkflowDataProxy: jest.fn().mockReturnValue({
$execution: {
resumeUrl: 'https://example.com/webhook',
},
}),
};
});
it('should throw NodeOperationError for invalid JSON inputs', async () => {
mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'inputs') {
return 'invalid json';
}
if (parameterName === 'resource') {
return 'workflow';
}
if (parameterName === 'operation') {
return 'dispatchAndWait';
}
if (parameterName === 'authentication') {
return 'accessToken';
}
return '';
});
await expect(async () => {
await githubNode.execute.call(mockExecutionContext);
}).rejects.toThrow(NodeOperationError);
});
it('should throw NodeOperationError for 404 errors when dispatching a workflow', async () => {
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
mockExecutionContext.helpers.requestWithAuthentication.mockRejectedValueOnce({
statusCode: 404,
message: 'Not Found',
});
mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'owner') {
return owner;
}
if (parameterName === 'repository') {
return repository;
}
if (parameterName === 'workflowId') {
return workflowId;
}
if (parameterName === 'inputs') {
return '{}';
}
if (parameterName === 'ref') {
return 'main';
}
if (parameterName === 'resource') {
return 'workflow';
}
if (parameterName === 'operation') {
return 'dispatchAndWait';
}
if (parameterName === 'authentication') {
return 'accessToken';
}
return '';
});
await expect(async () => {
await githubNode.execute.call(mockExecutionContext);
}).rejects.toThrow(/The workflow to dispatch could not be found/);
});
it('should throw NodeApiError for general API errors', async () => {
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
mockExecutionContext.helpers.requestWithAuthentication.mockRejectedValueOnce({
statusCode: 500,
message: 'Internal Server Error',
});
mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'owner') {
return owner;
}
if (parameterName === 'repository') {
return repository;
}
if (parameterName === 'workflowId') {
return workflowId;
}
if (parameterName === 'inputs') {
return '{}';
}
if (parameterName === 'ref') {
return 'main';
}
if (parameterName === 'resource') {
return 'workflow';
}
if (parameterName === 'operation') {
return 'dispatch';
}
if (parameterName === 'authentication') {
return 'accessToken';
}
return '';
});
await expect(async () => {
await githubNode.execute.call(mockExecutionContext);
}).rejects.toThrow();
});
it('should throw NodeApiError for general API errors in dispatchAndWait operation', async () => {
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
mockExecutionContext.getWorkflowDataProxy = jest.fn().mockReturnValue({
$execution: {
resumeUrl: 'https://example.com/webhook',
},
});
mockExecutionContext.helpers.requestWithAuthentication.mockRejectedValueOnce({
statusCode: 500,
message: 'Internal Server Error',
});
mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'owner') {
return owner;
}
if (parameterName === 'repository') {
return repository;
}
if (parameterName === 'workflowId') {
return workflowId;
}
if (parameterName === 'inputs') {
return '{}';
}
if (parameterName === 'ref') {
return 'main';
}
if (parameterName === 'resource') {
return 'workflow';
}
if (parameterName === 'operation') {
return 'dispatchAndWait';
}
if (parameterName === 'authentication') {
return 'accessToken';
}
return '';
});
await expect(async () => {
await githubNode.execute.call(mockExecutionContext);
}).rejects.toThrow(NodeApiError);
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'POST',
uri: expect.stringContaining(
`/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`,
),
body: expect.objectContaining({
ref: 'main',
inputs: expect.objectContaining({
resumeUrl: 'https://example.com/webhook',
}),
}),
}),
);
});
});
describe('Workflow Operations', () => {
let githubNode: Github;
let mockExecutionContext: any;
beforeEach(() => {
githubNode = new Github();
mockExecutionContext = {
getNode: jest.fn().mockReturnValue({ name: 'Github' }),
getNodeParameter: jest.fn(),
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
continueOnFail: jest.fn().mockReturnValue(false),
getCredentials: jest.fn().mockResolvedValue({
server: 'https://api.github.com',
user: 'test',
accessToken: 'test',
}),
helpers: {
returnJsonArray: jest.fn().mockReturnValue([{ json: {} }]),
requestWithAuthentication: jest.fn().mockResolvedValue({}),
constructExecutionMetaData: jest.fn().mockReturnValue([{ json: {} }]),
},
};
});
it('should use extractValue for workflowId in disable operation', async () => {
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
mockExecutionContext.getNodeParameter.mockImplementation(
(parameterName: string, _itemIndex: number, defaultValue: string, options?: any) => {
if (parameterName === 'owner') {
return owner;
}
if (parameterName === 'repository') {
return repository;
}
if (parameterName === 'workflowId') {
expect(options).toBeDefined();
expect(options.extractValue).toBe(true);
return workflowId;
}
if (parameterName === 'resource') {
return 'workflow';
}
if (parameterName === 'operation') {
return 'disable';
}
if (parameterName === 'authentication') {
return 'accessToken';
}
return defaultValue;
},
);
await githubNode.execute.call(mockExecutionContext);
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'PUT',
uri: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}/disable`,
}),
);
});
it('should use extractValue for workflowId in enable operation', async () => {
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
mockExecutionContext.getNodeParameter.mockImplementation(
(parameterName: string, _itemIndex: number, defaultValue: string, options?: any) => {
if (parameterName === 'owner') {
return owner;
}
if (parameterName === 'repository') {
return repository;
}
if (parameterName === 'workflowId') {
expect(options).toBeDefined();
expect(options.extractValue).toBe(true);
return workflowId;
}
if (parameterName === 'resource') {
return 'workflow';
}
if (parameterName === 'operation') {
return 'enable';
}
if (parameterName === 'authentication') {
return 'accessToken';
}
return defaultValue;
},
);
await githubNode.execute.call(mockExecutionContext);
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'PUT',
uri: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}/enable`,
}),
);
});
it('should use extractValue for workflowId in get operation', async () => {
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
mockExecutionContext.getNodeParameter.mockImplementation(
(parameterName: string, _itemIndex: number, defaultValue: string, options?: any) => {
if (parameterName === 'owner') {
return owner;
}
if (parameterName === 'repository') {
return repository;
}
if (parameterName === 'workflowId') {
expect(options).toBeDefined();
expect(options.extractValue).toBe(true);
return workflowId;
}
if (parameterName === 'resource') {
return 'workflow';
}
if (parameterName === 'operation') {
return 'get';
}
if (parameterName === 'authentication') {
return 'accessToken';
}
return defaultValue;
},
);
await githubNode.execute.call(mockExecutionContext);
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'GET',
uri: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}`,
}),
);
});
it('should use extractValue for workflowId in getUsage operation', async () => {
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
mockExecutionContext.getNodeParameter.mockImplementation(
(parameterName: string, _itemIndex: number, defaultValue: string, options?: any) => {
if (parameterName === 'owner') {
return owner;
}
if (parameterName === 'repository') {
return repository;
}
if (parameterName === 'workflowId') {
expect(options).toBeDefined();
expect(options.extractValue).toBe(true);
return workflowId;
}
if (parameterName === 'resource') {
return 'workflow';
}
if (parameterName === 'operation') {
return 'getUsage';
}
if (parameterName === 'authentication') {
return 'accessToken';
}
return defaultValue;
},
);
await githubNode.execute.call(mockExecutionContext);
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'GET',
uri: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}/timing`,
}),
);
});
});
describe('Parameter Extraction', () => {
it('should use extractValue for workflowId parameter', () => {
const githubNode = new Github();
const description = githubNode.description;
const workflowIdParam = description.properties.find((prop) => prop.name === 'workflowId');
expect(workflowIdParam).toBeDefined();
expect(workflowIdParam?.type).toBe('resourceLocator');
const workflowOperations = description.properties.find(
(prop) =>
prop.name === 'operation' && prop.displayOptions?.show?.resource?.includes('workflow'),
);
expect(workflowOperations).toBeDefined();
expect(workflowOperations?.options).toEqual(
expect.arrayContaining([
expect.objectContaining({ value: 'disable' }),
expect.objectContaining({ value: 'dispatch' }),
expect.objectContaining({ value: 'enable' }),
expect.objectContaining({ value: 'get' }),
expect.objectContaining({ value: 'getUsage' }),
]),
);
});
});
});