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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,184 @@
import type { IExecuteFunctions, IHookFunctions } from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import {
githubApiRequest,
getFileSha,
githubApiRequestAllItems,
isBase64,
validateJSON,
} from '../GenericFunctions';
const mockExecuteHookFunctions = {
getNodeParameter: jest.fn().mockImplementation((param: string) => {
if (param === 'authentication') return 'accessToken';
return undefined;
}),
getCredentials: jest.fn().mockResolvedValue({
server: 'https://api.github.com',
}),
helpers: {
requestWithAuthentication: jest.fn(),
},
getCurrentNodeParameter: jest.fn(),
getWebhookName: jest.fn(),
getWebhookDescription: jest.fn(),
getNodeWebhookUrl: jest.fn(),
getNode: jest.fn().mockReturnValue({
id: 'test-node-id',
name: 'test-node',
}),
} as unknown as IExecuteFunctions | IHookFunctions;
describe('GenericFunctions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('githubApiRequest', () => {
it('should make a successful API request', async () => {
const method = 'GET';
const endpoint = '/repos/test-owner/test-repo';
const body = {};
const responseData = { id: 123, name: 'test-repo' };
(mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await githubApiRequest.call(mockExecuteHookFunctions, method, endpoint, body);
expect(result).toEqual(responseData);
expect(mockExecuteHookFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'githubApi',
{
method: 'GET',
headers: { 'User-Agent': 'n8n' },
body: {},
qs: undefined,
uri: 'https://api.github.com/repos/test-owner/test-repo',
json: true,
},
);
});
it('should throw a NodeApiError on API failure', async () => {
const method = 'GET';
const endpoint = '/repos/test-owner/test-repo';
const body = {};
const error = new Error('API Error');
(mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock).mockRejectedValue(
error,
);
await expect(
githubApiRequest.call(mockExecuteHookFunctions, method, endpoint, body),
).rejects.toThrow(NodeApiError);
});
});
describe('getFileSha', () => {
it('should return the SHA of a file', async () => {
const owner = 'test-owner';
const repository = 'test-repo';
const filePath = 'README.md';
const branch = 'main';
const responseData = { sha: 'abc123' };
(mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getFileSha.call(
mockExecuteHookFunctions,
owner,
repository,
filePath,
branch,
);
expect(result).toBe('abc123');
expect(mockExecuteHookFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'githubApi',
{
method: 'GET',
headers: { 'User-Agent': 'n8n' },
body: {},
qs: { ref: 'main' },
uri: 'https://api.github.com/repos/test-owner/test-repo/contents/README.md',
json: true,
},
);
});
it('should throw a NodeOperationError if SHA is missing', async () => {
const owner = 'test-owner';
const repository = 'test-repo';
const filePath = 'README.md';
const responseData = {};
(mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
await expect(
getFileSha.call(mockExecuteHookFunctions, owner, repository, filePath),
).rejects.toThrow(NodeOperationError);
});
});
describe('githubApiRequestAllItems', () => {
it('should fetch all items with pagination', async () => {
const method = 'GET';
const endpoint = '/repos/test-owner/test-repo/issues';
const body = {};
const query = { state: 'open' };
const responseData1 = [{ id: 1, title: 'Issue 1' }];
const responseData2 = [{ id: 2, title: 'Issue 2' }];
(mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock)
.mockResolvedValueOnce({ headers: { link: 'next' }, body: responseData1 })
.mockResolvedValueOnce({ headers: {}, body: responseData2 });
const result = await githubApiRequestAllItems.call(
mockExecuteHookFunctions,
method,
endpoint,
body,
query,
);
expect(result).toEqual([...responseData1, ...responseData2]);
expect(mockExecuteHookFunctions.helpers.requestWithAuthentication).toHaveBeenCalledTimes(2);
});
});
describe('isBase64', () => {
it('should return true for valid Base64 strings', () => {
expect(isBase64('aGVsbG8gd29ybGQ=')).toBe(true);
expect(isBase64('Zm9vYmFy')).toBe(true);
});
it('should return false for invalid Base64 strings', () => {
expect(isBase64('not base64')).toBe(false);
expect(isBase64('123!@#')).toBe(false);
});
});
describe('validateJSON', () => {
it('should return parsed JSON for valid JSON strings', () => {
const jsonString = '{"key": "value"}';
const result = validateJSON(jsonString);
expect(result).toEqual({ key: 'value' });
});
it('should return undefined for invalid JSON strings', () => {
const invalidJsonString = 'not json';
const result = validateJSON(invalidJsonString);
expect(result).toBeUndefined();
});
});
});
@@ -0,0 +1,261 @@
import type { IExecuteFunctions } from 'n8n-workflow';
import { Github } from '../Github.node';
import * as GenericFunctions from '../GenericFunctions';
jest.mock('../GenericFunctions', () => ({
...jest.requireActual('../GenericFunctions'),
githubApiRequest: jest.fn(),
getFileSha: jest.fn(),
}));
describe('Github Node - File Create/Edit Operations', () => {
let github: Github;
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
beforeEach(() => {
github = new Github();
jest.clearAllMocks();
mockExecuteFunctions = {
getNodeParameter: jest.fn(),
getInputData: jest.fn().mockReturnValue([{ json: {} }]),
getNode: jest.fn().mockReturnValue({
id: 'test-node-id',
name: 'Github',
type: 'n8n-nodes-base.github',
typeVersion: 1,
position: [0, 0],
parameters: {},
}),
helpers: {
assertBinaryData: jest.fn(),
getBinaryDataBuffer: jest.fn(),
requestWithAuthentication: jest.fn(),
returnJsonArray: jest.fn((data) => (Array.isArray(data) ? data : [data])),
constructExecutionMetaData: jest.fn((data) => data),
},
getCredentials: jest.fn().mockResolvedValue({
accessToken: 'test-token',
server: 'https://api.github.com',
}),
continueOnFail: jest.fn().mockReturnValue(false),
} as unknown as jest.Mocked<IExecuteFunctions>;
});
describe('File Create - Binary Data', () => {
it('should handle binary data by converting buffer to base64', async () => {
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
(paramName: string, _itemIndex: number, fallback?: any) => {
const params: Record<string, any> = {
resource: 'file',
operation: 'create',
owner: 'test-owner',
repository: 'test-repo',
filePath: 'test/file.txt',
commitMessage: 'Add test file',
binaryData: true,
binaryPropertyName: 'data',
additionalParameters: {},
};
return params[paramName] ?? fallback;
},
);
const mockBinaryData = {
id: 'test-id',
data: 'base64data',
mimeType: 'text/plain',
fileName: 'test.txt',
};
const expectedBuffer = Buffer.from('test content');
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
(mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(
expectedBuffer,
);
(GenericFunctions.githubApiRequest as jest.Mock).mockResolvedValue({
content: {
name: 'file.txt',
path: 'test/file.txt',
sha: 'abc123',
},
});
const result = await github.execute.call(mockExecuteFunctions);
expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'data');
expect(GenericFunctions.githubApiRequest).toHaveBeenCalledWith(
'PUT',
'/repos/test-owner/test-repo/contents/test%2Ffile.txt',
expect.objectContaining({
content: expectedBuffer.toString('base64'),
message: 'Add test file',
}),
{},
);
expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(0);
});
});
describe('File Create - Text Content', () => {
it('should use base64 content as-is when fileContent is already base64', async () => {
const base64Content = 'dGVzdCBjb250ZW50';
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
(paramName: string, _itemIndex: number, fallback?: any) => {
const params: Record<string, any> = {
resource: 'file',
operation: 'create',
owner: 'test-owner',
repository: 'test-repo',
filePath: 'test/file.txt',
commitMessage: 'Add test file',
binaryData: false,
fileContent: base64Content,
additionalParameters: {},
};
return params[paramName] ?? fallback;
},
);
(GenericFunctions.githubApiRequest as jest.Mock).mockResolvedValue({
content: {
name: 'file.txt',
path: 'test/file.txt',
sha: 'abc123',
},
});
const result = await github.execute.call(mockExecuteFunctions);
expect(GenericFunctions.githubApiRequest).toHaveBeenCalledWith(
'PUT',
'/repos/test-owner/test-repo/contents/test%2Ffile.txt',
expect.objectContaining({
content: base64Content,
message: 'Add test file',
}),
{},
);
expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(0);
});
it('should convert plain text to base64 when fileContent is not base64', async () => {
const plainTextContent = 'Hello, World! This is plain text.';
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
(paramName: string, _itemIndex: number, fallback?: any) => {
const params: Record<string, any> = {
resource: 'file',
operation: 'create',
owner: 'test-owner',
repository: 'test-repo',
filePath: 'test/file.txt',
commitMessage: 'Add test file',
binaryData: false,
fileContent: plainTextContent,
additionalParameters: {},
};
return params[paramName] ?? fallback;
},
);
(GenericFunctions.githubApiRequest as jest.Mock).mockResolvedValue({
content: {
name: 'file.txt',
path: 'test/file.txt',
sha: 'abc123',
},
});
const result = await github.execute.call(mockExecuteFunctions);
const expectedBase64 = Buffer.from(plainTextContent).toString('base64');
expect(GenericFunctions.githubApiRequest).toHaveBeenCalledWith(
'PUT',
'/repos/test-owner/test-repo/contents/test%2Ffile.txt',
expect.objectContaining({
content: expectedBase64,
message: 'Add test file',
}),
{},
);
expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(0);
});
});
describe('File Edit - Binary Data', () => {
it('should get file SHA and convert buffer to base64 for edit operation', async () => {
(mockExecuteFunctions.getNodeParameter as jest.Mock).mockImplementation(
(paramName: string, _itemIndex: number, fallback?: any) => {
const params: Record<string, any> = {
resource: 'file',
operation: 'edit',
owner: 'test-owner',
repository: 'test-repo',
filePath: 'test/file.txt',
commitMessage: 'Update test file',
binaryData: true,
binaryPropertyName: 'data',
additionalParameters: {},
};
return params[paramName] ?? fallback;
},
);
const mockBinaryData = {
id: 'test-id',
data: 'old-base64-data',
mimeType: 'text/plain',
fileName: 'test.txt',
};
const expectedBuffer = Buffer.from('updated content');
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
(mockExecuteFunctions.helpers.getBinaryDataBuffer as jest.Mock).mockResolvedValue(
expectedBuffer,
);
(GenericFunctions.getFileSha as jest.Mock).mockResolvedValue('existing-sha-123');
(GenericFunctions.githubApiRequest as jest.Mock).mockResolvedValue({
content: {
name: 'file.txt',
path: 'test/file.txt',
sha: 'new-sha-456',
},
});
const result = await github.execute.call(mockExecuteFunctions);
expect(GenericFunctions.getFileSha).toHaveBeenCalledWith(
'test-owner',
'test-repo',
'test/file.txt',
undefined,
);
expect(mockExecuteFunctions.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'data');
expect(GenericFunctions.githubApiRequest).toHaveBeenCalledWith(
'PUT',
'/repos/test-owner/test-repo/contents/test%2Ffile.txt',
expect.objectContaining({
content: expectedBuffer.toString('base64'),
message: 'Update test file',
sha: 'existing-sha-123',
}),
{},
);
expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(0);
});
});
});
@@ -0,0 +1,142 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Github Node - Organization getRepositories', () => {
const credentials = {
githubApi: {
accessToken: 'test-token',
server: 'https://api.github.com',
user: 'testuser',
},
};
describe('Basic getRepositories Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/orgs/testorg/repos')
.query(true)
.reply(200, [
{
id: 1296269,
name: 'hello-world',
full_name: 'testorg/hello-world',
owner: {
login: 'testorg',
id: 1,
type: 'Organization',
},
private: false,
html_url: 'https://github.com/testorg/hello-world',
description: 'My first repository on GitHub!',
fork: false,
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
pushed_at: '2011-01-26T19:06:43Z',
clone_url: 'https://github.com/testorg/hello-world.git',
size: 108,
stargazers_count: 80,
watchers_count: 9,
language: 'C',
forks_count: 9,
archived: false,
disabled: false,
open_issues_count: 0,
license: {
key: 'mit',
name: 'MIT License',
},
visibility: 'public',
default_branch: 'master',
},
{
id: 1296270,
name: 'test-repo',
full_name: 'testorg/test-repo',
owner: {
login: 'testorg',
id: 1,
type: 'Organization',
},
private: true,
html_url: 'https://github.com/testorg/test-repo',
description: 'Test repository',
fork: false,
created_at: '2011-01-27T19:01:12Z',
updated_at: '2011-01-27T19:14:43Z',
pushed_at: '2011-01-27T19:06:43Z',
clone_url: 'https://github.com/testorg/test-repo.git',
size: 256,
stargazers_count: 42,
watchers_count: 15,
language: 'JavaScript',
forks_count: 3,
archived: false,
disabled: false,
open_issues_count: 2,
license: {
key: 'apache-2.0',
name: 'Apache License 2.0',
},
visibility: 'private',
default_branch: 'main',
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getRepositories.workflow.json'],
});
});
describe('Paginated getRepositories Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/orgs/testorg/repos')
.query({ per_page: 1 })
.reply(200, [
{
id: 1296269,
name: 'hello-world',
full_name: 'testorg/hello-world',
owner: {
login: 'testorg',
id: 1,
type: 'Organization',
},
private: false,
html_url: 'https://github.com/testorg/hello-world',
description: 'My first repository on GitHub!',
fork: false,
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
pushed_at: '2011-01-26T19:06:43Z',
clone_url: 'https://github.com/testorg/hello-world.git',
size: 108,
stargazers_count: 80,
watchers_count: 9,
language: 'C',
forks_count: 9,
archived: false,
disabled: false,
open_issues_count: 0,
license: {
key: 'mit',
name: 'MIT License',
},
visibility: 'public',
default_branch: 'master',
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getRepositoriesLimit.workflow.json'],
});
});
});
@@ -0,0 +1,374 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Github Node - Repository getIssues', () => {
const credentials = {
githubApi: {
accessToken: 'test-token',
server: 'https://api.github.com',
user: 'testuser',
},
};
describe('Basic getIssues Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/repos/testowner/testrepo/issues')
.query(true)
.reply(200, [
{
url: 'https://api.github.com/repos/testowner/testrepo/issues/1',
repository_url: 'https://api.github.com/repos/testowner/testrepo',
labels_url: 'https://api.github.com/repos/testowner/testrepo/issues/1/labels{/name}',
comments_url: 'https://api.github.com/repos/testowner/testrepo/issues/1/comments',
events_url: 'https://api.github.com/repos/testowner/testrepo/issues/1/events',
html_url: 'https://github.com/testowner/testrepo/issues/1',
id: 1,
number: 1,
title: 'Found a bug',
user: {
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045946,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDY=',
url: 'https://api.github.com/repos/testowner/testrepo/labels/bug',
name: 'bug',
description: "Something isn't working",
color: 'd73a49',
default: true,
},
],
state: 'open',
locked: false,
assignee: null,
assignees: [],
milestone: null,
comments: 0,
created_at: '2011-04-22T13:33:48Z',
updated_at: '2011-04-22T13:33:48Z',
closed_at: null,
author_association: 'COLLABORATOR',
active_lock_reason: null,
body: "I'm having a problem with this.",
reactions: {
url: 'https://api.github.com/repos/testowner/testrepo/issues/1/reactions',
total_count: 0,
'+1': 0,
'-1': 0,
laugh: 0,
hooray: 0,
confused: 0,
heart: 0,
rocket: 0,
eyes: 0,
},
timeline_url: 'https://api.github.com/repos/testowner/testrepo/issues/1/timeline',
performed_via_github_app: null,
state_reason: null,
},
{
url: 'https://api.github.com/repos/testowner/testrepo/issues/2',
repository_url: 'https://api.github.com/repos/testowner/testrepo',
labels_url: 'https://api.github.com/repos/testowner/testrepo/issues/2/labels{/name}',
comments_url: 'https://api.github.com/repos/testowner/testrepo/issues/2/comments',
events_url: 'https://api.github.com/repos/testowner/testrepo/issues/2/events',
html_url: 'https://github.com/testowner/testrepo/issues/2',
id: 2,
number: 2,
title: 'Feature request',
user: {
login: 'anotheruser',
id: 2,
node_id: 'MDQ6VXNlcjI=',
avatar_url: 'https://github.com/images/error/anotheruser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/anotheruser',
html_url: 'https://github.com/anotheruser',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045947,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDc=',
url: 'https://api.github.com/repos/testowner/testrepo/labels/enhancement',
name: 'enhancement',
description: 'New feature or request',
color: 'a2eeef',
default: true,
},
],
state: 'open',
locked: false,
assignee: {
login: 'assigneduser',
id: 3,
node_id: 'MDQ6VXNlcjM=',
avatar_url: 'https://github.com/images/error/assigneduser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/assigneduser',
html_url: 'https://github.com/assigneduser',
type: 'User',
site_admin: false,
},
assignees: [
{
login: 'assigneduser',
id: 3,
node_id: 'MDQ6VXNlcjM=',
avatar_url: 'https://github.com/images/error/assigneduser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/assigneduser',
html_url: 'https://github.com/assigneduser',
type: 'User',
site_admin: false,
},
],
milestone: {
url: 'https://api.github.com/repos/testowner/testrepo/milestones/1',
html_url: 'https://github.com/testowner/testrepo/milestone/1',
labels_url: 'https://api.github.com/repos/testowner/testrepo/milestones/1/labels',
id: 1002604,
number: 1,
state: 'open',
title: 'v1.0',
description: 'Tracking milestone for version 1.0',
creator: {
login: 'testowner',
id: 4,
node_id: 'MDQ6VXNlcjQ=',
avatar_url: 'https://github.com/images/error/testowner_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testowner',
html_url: 'https://github.com/testowner',
type: 'User',
site_admin: false,
},
open_issues: 4,
closed_issues: 8,
created_at: '2011-04-10T20:09:31Z',
updated_at: '2014-03-03T18:58:10Z',
closed_at: null,
due_on: '2018-09-22T23:39:01Z',
node_id: 'MDk6TWlsZXN0b25lMTAwMjYwNA==',
},
comments: 3,
created_at: '2011-04-22T13:33:48Z',
updated_at: '2011-04-22T13:33:48Z',
closed_at: null,
author_association: 'COLLABORATOR',
active_lock_reason: null,
body: 'It would be great if we could add this feature.',
reactions: {
url: 'https://api.github.com/repos/testowner/testrepo/issues/2/reactions',
total_count: 5,
'+1': 3,
'-1': 1,
laugh: 0,
hooray: 0,
confused: 0,
heart: 1,
rocket: 0,
eyes: 0,
},
timeline_url: 'https://api.github.com/repos/testowner/testrepo/issues/2/timeline',
performed_via_github_app: null,
state_reason: null,
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getRepositoryIssues.workflow.json'],
});
});
describe('Limited getIssues Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/repos/testowner/testrepo/issues')
.query({ per_page: 1 })
.reply(200, [
{
url: 'https://api.github.com/repos/testowner/testrepo/issues/1',
repository_url: 'https://api.github.com/repos/testowner/testrepo',
labels_url: 'https://api.github.com/repos/testowner/testrepo/issues/1/labels{/name}',
comments_url: 'https://api.github.com/repos/testowner/testrepo/issues/1/comments',
events_url: 'https://api.github.com/repos/testowner/testrepo/issues/1/events',
html_url: 'https://github.com/testowner/testrepo/issues/1',
id: 1,
number: 1,
title: 'Found a bug',
user: {
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045946,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDY=',
url: 'https://api.github.com/repos/testowner/testrepo/labels/bug',
name: 'bug',
description: "Something isn't working",
color: 'd73a49',
default: true,
},
],
state: 'open',
locked: false,
assignee: null,
assignees: [],
milestone: null,
comments: 0,
created_at: '2011-04-22T13:33:48Z',
updated_at: '2011-04-22T13:33:48Z',
closed_at: null,
author_association: 'COLLABORATOR',
active_lock_reason: null,
body: "I'm having a problem with this.",
reactions: {
url: 'https://api.github.com/repos/testowner/testrepo/issues/1/reactions',
total_count: 0,
'+1': 0,
'-1': 0,
laugh: 0,
hooray: 0,
confused: 0,
heart: 0,
rocket: 0,
eyes: 0,
},
timeline_url: 'https://api.github.com/repos/testowner/testrepo/issues/1/timeline',
performed_via_github_app: null,
state_reason: null,
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getRepositoryIssuesLimit.workflow.json'],
});
});
describe('Filtered getIssues Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/repos/testowner/testrepo/issues')
.query({ state: 'closed', labels: 'bug', assignee: 'testuser', per_page: 100, page: 1 })
.reply(200, [
{
url: 'https://api.github.com/repos/testowner/testrepo/issues/3',
repository_url: 'https://api.github.com/repos/testowner/testrepo',
labels_url: 'https://api.github.com/repos/testowner/testrepo/issues/3/labels{/name}',
comments_url: 'https://api.github.com/repos/testowner/testrepo/issues/3/comments',
events_url: 'https://api.github.com/repos/testowner/testrepo/issues/3/events',
html_url: 'https://github.com/testowner/testrepo/issues/3',
id: 3,
number: 3,
title: 'Fixed bug',
user: {
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045946,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDY=',
url: 'https://api.github.com/repos/testowner/testrepo/labels/bug',
name: 'bug',
description: "Something isn't working",
color: 'd73a49',
default: true,
},
],
state: 'closed',
locked: false,
assignee: {
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
assignees: [
{
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
],
milestone: null,
comments: 2,
created_at: '2011-04-20T13:33:48Z',
updated_at: '2011-04-25T13:33:48Z',
closed_at: '2011-04-25T13:33:48Z',
author_association: 'COLLABORATOR',
active_lock_reason: null,
body: 'This bug has been fixed.',
reactions: {
url: 'https://api.github.com/repos/testowner/testrepo/issues/3/reactions',
total_count: 1,
'+1': 1,
'-1': 0,
laugh: 0,
hooray: 0,
confused: 0,
heart: 0,
rocket: 0,
eyes: 0,
},
timeline_url: 'https://api.github.com/repos/testowner/testrepo/issues/3/timeline',
performed_via_github_app: null,
state_reason: 'completed',
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getRepositoryIssuesFiltered.workflow.json'],
});
});
});
@@ -0,0 +1,142 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Github Node - User getRepositories', () => {
const credentials = {
githubApi: {
accessToken: 'test-token',
server: 'https://api.github.com',
user: 'testuser',
},
};
describe('Basic User getRepositories Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/users/testuser/repos')
.query(true)
.reply(200, [
{
id: 1296269,
name: 'hello-world',
full_name: 'testuser/hello-world',
owner: {
login: 'testuser',
id: 1,
type: 'User',
},
private: false,
html_url: 'https://github.com/testuser/hello-world',
description: 'My first repository on GitHub!',
fork: false,
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
pushed_at: '2011-01-26T19:06:43Z',
clone_url: 'https://github.com/testuser/hello-world.git',
size: 108,
stargazers_count: 80,
watchers_count: 9,
language: 'C',
forks_count: 9,
archived: false,
disabled: false,
open_issues_count: 0,
license: {
key: 'mit',
name: 'MIT License',
},
visibility: 'public',
default_branch: 'master',
},
{
id: 1296270,
name: 'my-app',
full_name: 'testuser/my-app',
owner: {
login: 'testuser',
id: 1,
type: 'User',
},
private: false,
html_url: 'https://github.com/testuser/my-app',
description: 'My awesome application',
fork: false,
created_at: '2011-02-26T19:01:12Z',
updated_at: '2011-02-26T19:14:43Z',
pushed_at: '2011-02-26T19:06:43Z',
clone_url: 'https://github.com/testuser/my-app.git',
size: 512,
stargazers_count: 156,
watchers_count: 45,
language: 'JavaScript',
forks_count: 23,
archived: false,
disabled: false,
open_issues_count: 5,
license: {
key: 'apache-2.0',
name: 'Apache License 2.0',
},
visibility: 'public',
default_branch: 'main',
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getUserRepositories.workflow.json'],
});
});
describe('Limited User getRepositories Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/users/testuser/repos')
.query({ per_page: 1 })
.reply(200, [
{
id: 1296269,
name: 'hello-world',
full_name: 'testuser/hello-world',
owner: {
login: 'testuser',
id: 1,
type: 'User',
},
private: false,
html_url: 'https://github.com/testuser/hello-world',
description: 'My first repository on GitHub!',
fork: false,
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
pushed_at: '2011-01-26T19:06:43Z',
clone_url: 'https://github.com/testuser/hello-world.git',
size: 108,
stargazers_count: 80,
watchers_count: 9,
language: 'C',
forks_count: 9,
archived: false,
disabled: false,
open_issues_count: 0,
license: {
key: 'mit',
name: 'MIT License',
},
visibility: 'public',
default_branch: 'master',
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getUserRepositoriesLimit.workflow.json'],
});
});
});
@@ -0,0 +1,609 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Github Node - User getUserIssues', () => {
const credentials = {
githubApi: {
accessToken: 'test-token',
server: 'https://api.github.com',
user: 'testuser',
},
};
describe('Basic getUserIssues Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/issues')
.query(true)
.reply(200, [
{
url: 'https://api.github.com/repos/someowner/somerepo/issues/1',
repository_url: 'https://api.github.com/repos/someowner/somerepo',
labels_url: 'https://api.github.com/repos/someowner/somerepo/issues/1/labels{/name}',
comments_url: 'https://api.github.com/repos/someowner/somerepo/issues/1/comments',
events_url: 'https://api.github.com/repos/someowner/somerepo/issues/1/events',
html_url: 'https://github.com/someowner/somerepo/issues/1',
id: 1,
number: 1,
title: 'Issue assigned to me',
user: {
login: 'issueauthor',
id: 5,
node_id: 'MDQ6VXNlcjU=',
avatar_url: 'https://github.com/images/error/issueauthor_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/issueauthor',
html_url: 'https://github.com/issueauthor',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045946,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDY=',
url: 'https://api.github.com/repos/someowner/somerepo/labels/bug',
name: 'bug',
description: "Something isn't working",
color: 'd73a49',
default: true,
},
],
state: 'open',
locked: false,
assignee: {
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
assignees: [
{
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
],
milestone: null,
comments: 0,
created_at: '2011-04-22T13:33:48Z',
updated_at: '2011-04-22T13:33:48Z',
closed_at: null,
author_association: 'NONE',
active_lock_reason: null,
body: 'This is an issue assigned to me.',
reactions: {
url: 'https://api.github.com/repos/someowner/somerepo/issues/1/reactions',
total_count: 0,
'+1': 0,
'-1': 0,
laugh: 0,
hooray: 0,
confused: 0,
heart: 0,
rocket: 0,
eyes: 0,
},
timeline_url: 'https://api.github.com/repos/someowner/somerepo/issues/1/timeline',
performed_via_github_app: null,
state_reason: null,
repository: {
id: 1296269,
node_id: 'MDEwOlJlcG9zaXRvcnkxMjk2MjY5',
name: 'somerepo',
full_name: 'someowner/somerepo',
owner: {
login: 'someowner',
id: 6,
node_id: 'MDQ6VXNlcjY=',
avatar_url: 'https://github.com/images/error/someowner_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/someowner',
html_url: 'https://github.com/someowner',
type: 'User',
site_admin: false,
},
private: false,
html_url: 'https://github.com/someowner/somerepo',
description: 'Repository with issues assigned to testuser',
fork: false,
url: 'https://api.github.com/repos/someowner/somerepo',
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
pushed_at: '2011-01-26T19:06:43Z',
git_url: 'git://github.com/someowner/somerepo.git',
ssh_url: 'git@github.com:someowner/somerepo.git',
clone_url: 'https://github.com/someowner/somerepo.git',
size: 108,
stargazers_count: 80,
watchers_count: 9,
language: 'C',
has_issues: true,
has_projects: true,
has_wiki: true,
has_pages: false,
forks_count: 9,
mirror_url: null,
archived: false,
disabled: false,
open_issues_count: 0,
license: {
key: 'mit',
name: 'MIT License',
spdx_id: 'MIT',
url: 'https://api.github.com/licenses/mit',
node_id: 'MDc6TGljZW5zZW1pdA==',
},
forks: 9,
open_issues: 0,
watchers: 9,
default_branch: 'master',
},
},
{
url: 'https://api.github.com/repos/anotherowner/anotherrepo/issues/5',
repository_url: 'https://api.github.com/repos/anotherowner/anotherrepo',
labels_url:
'https://api.github.com/repos/anotherowner/anotherrepo/issues/5/labels{/name}',
comments_url: 'https://api.github.com/repos/anotherowner/anotherrepo/issues/5/comments',
events_url: 'https://api.github.com/repos/anotherowner/anotherrepo/issues/5/events',
html_url: 'https://github.com/anotherowner/anotherrepo/issues/5',
id: 5,
number: 5,
title: 'Enhancement request assigned to me',
user: {
login: 'requestor',
id: 7,
node_id: 'MDQ6VXNlcjc=',
avatar_url: 'https://github.com/images/error/requestor_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/requestor',
html_url: 'https://github.com/requestor',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045947,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDc=',
url: 'https://api.github.com/repos/anotherowner/anotherrepo/labels/enhancement',
name: 'enhancement',
description: 'New feature or request',
color: 'a2eeef',
default: true,
},
{
id: 208045948,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDg=',
url: 'https://api.github.com/repos/anotherowner/anotherrepo/labels/good-first-issue',
name: 'good first issue',
description: 'Good for newcomers',
color: '7057ff',
default: true,
},
],
state: 'open',
locked: false,
assignee: {
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
assignees: [
{
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
],
milestone: null,
comments: 1,
created_at: '2011-04-22T13:33:48Z',
updated_at: '2011-04-22T13:33:48Z',
closed_at: null,
author_association: 'CONTRIBUTOR',
active_lock_reason: null,
body: 'Please add this enhancement.',
reactions: {
url: 'https://api.github.com/repos/anotherowner/anotherrepo/issues/5/reactions',
total_count: 2,
'+1': 2,
'-1': 0,
laugh: 0,
hooray: 0,
confused: 0,
heart: 0,
rocket: 0,
eyes: 0,
},
timeline_url: 'https://api.github.com/repos/anotherowner/anotherrepo/issues/5/timeline',
performed_via_github_app: null,
state_reason: null,
repository: {
id: 1296270,
node_id: 'MDEwOlJlcG9zaXRvcnkxMjk2Mjcw',
name: 'anotherrepo',
full_name: 'anotherowner/anotherrepo',
owner: {
login: 'anotherowner',
id: 8,
node_id: 'MDQ6VXNlcjg=',
avatar_url: 'https://github.com/images/error/anotherowner_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/anotherowner',
html_url: 'https://github.com/anotherowner',
type: 'User',
site_admin: false,
},
private: false,
html_url: 'https://github.com/anotherowner/anotherrepo',
description: 'Another repository with issues for testuser',
fork: false,
url: 'https://api.github.com/repos/anotherowner/anotherrepo',
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
pushed_at: '2011-01-26T19:06:43Z',
git_url: 'git://github.com/anotherowner/anotherrepo.git',
ssh_url: 'git@github.com:anotherowner/anotherrepo.git',
clone_url: 'https://github.com/anotherowner/anotherrepo.git',
size: 256,
stargazers_count: 42,
watchers_count: 15,
language: 'JavaScript',
has_issues: true,
has_projects: true,
has_wiki: true,
has_pages: false,
forks_count: 3,
mirror_url: null,
archived: false,
disabled: false,
open_issues_count: 5,
license: {
key: 'apache-2.0',
name: 'Apache License 2.0',
spdx_id: 'Apache-2.0',
url: 'https://api.github.com/licenses/apache-2.0',
node_id: 'MDc6TGljZW5zZWFwYWNoZS0yLjA=',
},
forks: 3,
open_issues: 5,
watchers: 15,
default_branch: 'main',
},
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getUserIssues.workflow.json'],
});
});
describe('Limited getUserIssues Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/issues')
.query({ per_page: 1 })
.reply(200, [
{
url: 'https://api.github.com/repos/someowner/somerepo/issues/1',
repository_url: 'https://api.github.com/repos/someowner/somerepo',
labels_url: 'https://api.github.com/repos/someowner/somerepo/issues/1/labels{/name}',
comments_url: 'https://api.github.com/repos/someowner/somerepo/issues/1/comments',
events_url: 'https://api.github.com/repos/someowner/somerepo/issues/1/events',
html_url: 'https://github.com/someowner/somerepo/issues/1',
id: 1,
number: 1,
title: 'Issue assigned to me',
user: {
login: 'issueauthor',
id: 5,
node_id: 'MDQ6VXNlcjU=',
avatar_url: 'https://github.com/images/error/issueauthor_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/issueauthor',
html_url: 'https://github.com/issueauthor',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045946,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDY=',
url: 'https://api.github.com/repos/someowner/somerepo/labels/bug',
name: 'bug',
description: "Something isn't working",
color: 'd73a49',
default: true,
},
],
state: 'open',
locked: false,
assignee: {
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
assignees: [
{
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
],
milestone: null,
comments: 0,
created_at: '2011-04-22T13:33:48Z',
updated_at: '2011-04-22T13:33:48Z',
closed_at: null,
author_association: 'NONE',
active_lock_reason: null,
body: 'This is an issue assigned to me.',
reactions: {
url: 'https://api.github.com/repos/someowner/somerepo/issues/1/reactions',
total_count: 0,
'+1': 0,
'-1': 0,
laugh: 0,
hooray: 0,
confused: 0,
heart: 0,
rocket: 0,
eyes: 0,
},
timeline_url: 'https://api.github.com/repos/someowner/somerepo/issues/1/timeline',
performed_via_github_app: null,
state_reason: null,
repository: {
id: 1296269,
node_id: 'MDEwOlJlcG9zaXRvcnkxMjk2MjY5',
name: 'somerepo',
full_name: 'someowner/somerepo',
owner: {
login: 'someowner',
id: 6,
node_id: 'MDQ6VXNlcjY=',
avatar_url: 'https://github.com/images/error/someowner_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/someowner',
html_url: 'https://github.com/someowner',
type: 'User',
site_admin: false,
},
private: false,
html_url: 'https://github.com/someowner/somerepo',
description: 'Repository with issues assigned to testuser',
fork: false,
url: 'https://api.github.com/repos/someowner/somerepo',
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
pushed_at: '2011-01-26T19:06:43Z',
git_url: 'git://github.com/someowner/somerepo.git',
ssh_url: 'git@github.com:someowner/somerepo.git',
clone_url: 'https://github.com/someowner/somerepo.git',
size: 108,
stargazers_count: 80,
watchers_count: 9,
language: 'C',
has_issues: true,
has_projects: true,
has_wiki: true,
has_pages: false,
forks_count: 9,
mirror_url: null,
archived: false,
disabled: false,
open_issues_count: 0,
license: {
key: 'mit',
name: 'MIT License',
spdx_id: 'MIT',
url: 'https://api.github.com/licenses/mit',
node_id: 'MDc6TGljZW5zZW1pdA==',
},
forks: 9,
open_issues: 0,
watchers: 9,
default_branch: 'master',
},
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getUserIssuesLimit.workflow.json'],
});
});
describe('Filtered getUserIssues Operation', () => {
beforeAll(() => {
const mock = nock('https://api.github.com');
mock
.get('/issues')
.query({ state: 'closed', labels: 'enhancement', per_page: 100, page: 1 })
.reply(200, [
{
url: 'https://api.github.com/repos/testowner/closedrepo/issues/10',
repository_url: 'https://api.github.com/repos/testowner/closedrepo',
labels_url: 'https://api.github.com/repos/testowner/closedrepo/issues/10/labels{/name}',
comments_url: 'https://api.github.com/repos/testowner/closedrepo/issues/10/comments',
events_url: 'https://api.github.com/repos/testowner/closedrepo/issues/10/events',
html_url: 'https://github.com/testowner/closedrepo/issues/10',
id: 10,
number: 10,
title: 'Completed enhancement',
user: {
login: 'enhancementauthor',
id: 9,
node_id: 'MDQ6VXNlcjk=',
avatar_url: 'https://github.com/images/error/enhancementauthor_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/enhancementauthor',
html_url: 'https://github.com/enhancementauthor',
type: 'User',
site_admin: false,
},
labels: [
{
id: 208045947,
node_id: 'MDU6TGFiZWwyMDgwNDU5NDc=',
url: 'https://api.github.com/repos/testowner/closedrepo/labels/enhancement',
name: 'enhancement',
description: 'New feature or request',
color: 'a2eeef',
default: true,
},
],
state: 'closed',
locked: false,
assignee: {
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
assignees: [
{
login: 'testuser',
id: 1,
node_id: 'MDQ6VXNlcjE=',
avatar_url: 'https://github.com/images/error/testuser_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testuser',
html_url: 'https://github.com/testuser',
type: 'User',
site_admin: false,
},
],
milestone: null,
comments: 5,
created_at: '2011-04-10T13:33:48Z',
updated_at: '2011-04-30T13:33:48Z',
closed_at: '2011-04-30T13:33:48Z',
author_association: 'CONTRIBUTOR',
active_lock_reason: null,
body: 'Enhancement has been completed successfully.',
reactions: {
url: 'https://api.github.com/repos/testowner/closedrepo/issues/10/reactions',
total_count: 3,
'+1': 2,
'-1': 0,
laugh: 0,
hooray: 1,
confused: 0,
heart: 0,
rocket: 0,
eyes: 0,
},
timeline_url: 'https://api.github.com/repos/testowner/closedrepo/issues/10/timeline',
performed_via_github_app: null,
state_reason: 'completed',
repository: {
id: 1296271,
node_id: 'MDEwOlJlcG9zaXRvcnkxMjk2Mjcx',
name: 'closedrepo',
full_name: 'testowner/closedrepo',
owner: {
login: 'testowner',
id: 10,
node_id: 'MDQ6VXNlcjEw',
avatar_url: 'https://github.com/images/error/testowner_happy.gif',
gravatar_id: '',
url: 'https://api.github.com/users/testowner',
html_url: 'https://github.com/testowner',
type: 'User',
site_admin: false,
},
private: false,
html_url: 'https://github.com/testowner/closedrepo',
description: 'Repository with closed enhancement issues',
fork: false,
url: 'https://api.github.com/repos/testowner/closedrepo',
created_at: '2011-01-26T19:01:12Z',
updated_at: '2011-01-26T19:14:43Z',
pushed_at: '2011-01-26T19:06:43Z',
git_url: 'git://github.com/testowner/closedrepo.git',
ssh_url: 'git@github.com:testowner/closedrepo.git',
clone_url: 'https://github.com/testowner/closedrepo.git',
size: 128,
stargazers_count: 25,
watchers_count: 5,
language: 'Python',
has_issues: true,
has_projects: true,
has_wiki: true,
has_pages: false,
forks_count: 2,
mirror_url: null,
archived: false,
disabled: false,
open_issues_count: 0,
license: {
key: 'mit',
name: 'MIT License',
spdx_id: 'MIT',
url: 'https://api.github.com/licenses/mit',
node_id: 'MDc6TGljZW5zZW1pdA==',
},
forks: 2,
open_issues: 0,
watchers: 5,
default_branch: 'main',
},
},
]);
});
new NodeTestHarness().setupTests({
credentials,
workflowFiles: ['getUserIssuesFiltered.workflow.json'],
});
});
});
@@ -0,0 +1,159 @@
import { createHmac, timingSafeEqual } from 'crypto';
import { verifySignature } from '../GithubTriggerHelpers';
jest.mock('crypto', () => ({
...jest.requireActual('crypto'),
createHmac: jest.fn().mockReturnValue({
update: jest.fn().mockReturnThis(),
digest: jest
.fn()
.mockReturnValue('757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17'),
}),
timingSafeEqual: jest.fn(),
}));
describe('GithubTriggerHelpers', () => {
let mockWebhookFunctions: {
getWorkflowStaticData: jest.Mock;
getRequestObject: jest.Mock;
};
const testWebhookSecret = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2';
const testBody =
'{"action":"opened","pull_request":{"id":123},"repository":{"full_name":"owner/repo"}}';
const testSignature = 'sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17';
beforeEach(() => {
jest.clearAllMocks();
mockWebhookFunctions = {
getWorkflowStaticData: jest.fn(),
getRequestObject: jest.fn(),
};
// Default mock return values
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({
webhookSecret: testWebhookSecret,
});
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-hub-signature-256') return testSignature;
return null;
}),
rawBody: testBody,
});
});
describe('verifySignature', () => {
it('should return true when no webhook secret is stored (backwards compatibility)', () => {
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(true);
expect(mockWebhookFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
});
it('should return false when signature header is missing', () => {
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockReturnValue(null),
rawBody: testBody,
});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
});
it('should return false when signature does not start with sha256=', () => {
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-hub-signature-256') return 'invalid-format-signature';
return null;
}),
rawBody: testBody,
});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
});
it('should return false when rawBody is missing', () => {
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-hub-signature-256') return testSignature;
return null;
}),
rawBody: undefined,
});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
});
it('should return true when signature is valid', () => {
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(true);
expect(createHmac).toHaveBeenCalledWith('sha256', testWebhookSecret);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should return false when signature is invalid', () => {
(timingSafeEqual as jest.Mock).mockReturnValue(false);
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
expect(createHmac).toHaveBeenCalledWith('sha256', testWebhookSecret);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should handle Buffer rawBody correctly', () => {
const bufferBody = Buffer.from(testBody);
mockWebhookFunctions.getRequestObject.mockReturnValue({
header: jest.fn().mockImplementation((header) => {
if (header === 'x-hub-signature-256') return testSignature;
return null;
}),
rawBody: bufferBody,
});
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(true);
const mockHmac = createHmac('sha256', testWebhookSecret);
expect(mockHmac.update).toHaveBeenCalledWith(bufferBody);
});
it('should return false when computed and provided signatures have different lengths', () => {
// Mock a different length signature
const mockHmacInstance = {
update: jest.fn().mockReturnThis(),
digest: jest.fn().mockReturnValue('short'),
};
(createHmac as jest.Mock).mockReturnValue(mockHmacInstance);
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
// timingSafeEqual should not be called if lengths don't match
expect(timingSafeEqual).not.toHaveBeenCalled();
});
it('should return false when an error occurs during verification', () => {
(createHmac as jest.Mock).mockImplementation(() => {
throw new Error('Crypto error');
});
const result = verifySignature.call(mockWebhookFunctions as never);
expect(result).toBe(false);
});
});
});
@@ -0,0 +1,499 @@
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import { getUsers, getRepositories, getWorkflows, getRefs } from '../SearchFunctions';
const mockLoadOptionsFunctions = {
getNodeParameter: jest.fn(),
getCredentials: jest.fn().mockResolvedValue({
server: 'https://api.github.com',
}),
helpers: {
requestWithAuthentication: jest.fn(),
},
getCurrentNodeParameter: jest.fn(),
} as unknown as ILoadOptionsFunctions;
describe('Search Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('getUsers', () => {
it('should fetch users', async () => {
const filter = 'test-user';
const responseData = {
items: [
{ login: 'test-user-1', html_url: 'https://github.com/test-user-1' },
{ login: 'test-user-2', html_url: 'https://github.com/test-user-2' },
],
total_count: 2,
};
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getUsers.call(mockLoadOptionsFunctions, filter);
expect(result).toEqual({
results: [
{ name: 'test-user-1', value: 'test-user-1', url: 'https://github.com/test-user-1' },
{ name: 'test-user-2', value: 'test-user-2', url: 'https://github.com/test-user-2' },
],
paginationToken: undefined,
});
expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'githubOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: expect.objectContaining({ page: 1 }),
}),
);
});
it('should handle pagination', async () => {
const filter = 'test-user';
const responseData = {
items: [
{ login: 'test-user-1', html_url: 'https://github.com/test-user-1' },
{ login: 'test-user-2', html_url: 'https://github.com/test-user-2' },
],
total_count: 200,
};
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getUsers.call(mockLoadOptionsFunctions, filter);
expect(result).toEqual({
results: [
{ name: 'test-user-1', value: 'test-user-1', url: 'https://github.com/test-user-1' },
{ name: 'test-user-2', value: 'test-user-2', url: 'https://github.com/test-user-2' },
],
paginationToken: 2,
});
});
it('should use paginationToken when provided', async () => {
const filter = 'test-user';
const paginationToken = '3';
const responseData = {
items: [
{ login: 'test-user-5', html_url: 'https://github.com/test-user-5' },
{ login: 'test-user-6', html_url: 'https://github.com/test-user-6' },
],
total_count: 200,
};
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getUsers.call(mockLoadOptionsFunctions, filter, paginationToken);
expect(result).toEqual({
results: [
{ name: 'test-user-5', value: 'test-user-5', url: 'https://github.com/test-user-5' },
{ name: 'test-user-6', value: 'test-user-6', url: 'https://github.com/test-user-6' },
],
paginationToken: undefined,
});
expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'githubOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: expect.objectContaining({ page: 3 }),
}),
);
});
});
describe('getRepositories', () => {
it('should fetch repositories', async () => {
const filter = 'test-repo';
const owner = 'test-owner';
const responseData = {
items: [
{ name: 'test-repo-1', html_url: 'https://github.com/test-owner/test-repo-1' },
{ name: 'test-repo-2', html_url: 'https://github.com/test-owner/test-repo-2' },
],
total_count: 2,
};
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockReturnValue(owner);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getRepositories.call(mockLoadOptionsFunctions, filter);
expect(result).toEqual({
results: [
{
name: 'test-repo-1',
value: 'test-repo-1',
url: 'https://github.com/test-owner/test-repo-1',
},
{
name: 'test-repo-2',
value: 'test-repo-2',
url: 'https://github.com/test-owner/test-repo-2',
},
],
paginationToken: undefined,
});
});
it('should fetch repositories without filter', async () => {
const owner = 'test-owner';
const responseData = {
items: [
{ name: 'test-repo-1', html_url: 'https://github.com/test-owner/test-repo-1' },
{ name: 'test-repo-2', html_url: 'https://github.com/test-owner/test-repo-2' },
],
total_count: 2,
};
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockReturnValue(owner);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getRepositories.call(mockLoadOptionsFunctions);
expect(result).toEqual({
results: [
{
name: 'test-repo-1',
value: 'test-repo-1',
url: 'https://github.com/test-owner/test-repo-1',
},
{
name: 'test-repo-2',
value: 'test-repo-2',
url: 'https://github.com/test-owner/test-repo-2',
},
],
paginationToken: undefined,
});
});
it('should use paginationToken when provided', async () => {
const filter = 'test-repo';
const paginationToken = '3';
const owner = 'test-owner';
const responseData = {
items: [
{ name: 'test-repo-5', html_url: 'https://github.com/test-owner/test-repo-5' },
{ name: 'test-repo-6', html_url: 'https://github.com/test-owner/test-repo-6' },
],
total_count: 200,
};
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockReturnValue(owner);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getRepositories.call(mockLoadOptionsFunctions, filter, paginationToken);
expect(result).toEqual({
results: [
{
name: 'test-repo-5',
value: 'test-repo-5',
url: 'https://github.com/test-owner/test-repo-5',
},
{
name: 'test-repo-6',
value: 'test-repo-6',
url: 'https://github.com/test-owner/test-repo-6',
},
],
paginationToken: undefined,
});
expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'githubOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: expect.objectContaining({ page: 3 }),
}),
);
});
it('should handle empty repositories', async () => {
const filter = 'test-repo';
const owner = 'test-owner';
const responseData = {
items: [],
total_count: 0,
};
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockReturnValue(owner);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getRepositories.call(mockLoadOptionsFunctions, filter);
expect(result).toEqual({
results: [],
paginationToken: undefined,
});
});
});
describe('getWorkflows', () => {
it('should fetch workflows', async () => {
const owner = 'test-owner';
const repository = 'test-repo';
const responseData = {
workflows: [
{ id: '1', name: 'workflow-1' },
{ id: '2', name: 'workflow-2' },
],
total_count: 2,
};
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock)
.mockReturnValueOnce(owner)
.mockReturnValueOnce(repository);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getWorkflows.call(mockLoadOptionsFunctions);
expect(result).toEqual({
results: [
{ name: 'workflow-1', value: '1' },
{ name: 'workflow-2', value: '2' },
],
paginationToken: undefined,
});
});
it('should handle pagination', async () => {
const owner = 'test-owner';
const repository = 'test-repo';
const responseData = {
workflows: [
{ id: '1', name: 'workflow-1' },
{ id: '2', name: 'workflow-2' },
],
total_count: 200,
};
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock)
.mockReturnValueOnce(owner)
.mockReturnValueOnce(repository);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getWorkflows.call(mockLoadOptionsFunctions);
expect(result).toEqual({
results: [
{ name: 'workflow-1', value: '1' },
{ name: 'workflow-2', value: '2' },
],
paginationToken: 2,
});
});
it('should use paginationToken when provided and return next page token', async () => {
const paginationToken = '1';
const owner = 'test-owner';
const repository = 'test-repo';
const responseData = {
workflows: [
{ id: '3', name: 'workflow-3' },
{ id: '4', name: 'workflow-4' },
],
total_count: 300,
};
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock)
.mockReturnValueOnce(owner)
.mockReturnValueOnce(repository);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getWorkflows.call(mockLoadOptionsFunctions, paginationToken);
expect(result).toEqual({
results: [
{ name: 'workflow-3', value: '3' },
{ name: 'workflow-4', value: '4' },
],
paginationToken: 2,
});
expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'githubOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: expect.objectContaining({ page: 1 }),
}),
);
});
it('should handle empty workflows', async () => {
const owner = 'test-owner';
const repository = 'test-repo';
const responseData = {
workflows: [],
total_count: 0,
};
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock)
.mockReturnValueOnce(owner)
.mockReturnValueOnce(repository);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
responseData,
);
const result = await getWorkflows.call(mockLoadOptionsFunctions);
expect(result).toEqual({
results: [],
paginationToken: undefined,
});
});
});
describe('getRefs', () => {
it('should fetch branches and tags using git/refs endpoint', async () => {
const owner = 'test-owner';
const repository = 'test-repo';
const refsResponse = [
{ ref: 'refs/heads/Main' },
{ ref: 'refs/heads/Dev' },
{ ref: 'refs/tags/v1.0.0' },
{ ref: 'refs/tags/v2.0.0' },
{ ref: 'refs/Pull/123/head' },
];
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockImplementation(
(param: string) => {
if (param === 'owner') return owner;
if (param === 'repository') return repository;
},
);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
refsResponse,
);
const result = await getRefs.call(mockLoadOptionsFunctions);
expect(result).toEqual({
results: [
{ name: 'Main', value: 'Main', description: 'Branch: Main' },
{ name: 'Dev', value: 'Dev', description: 'Branch: Dev' },
{ name: 'v1.0.0', value: 'v1.0.0', description: 'Tag: v1.0.0' },
{ name: 'v2.0.0', value: 'v2.0.0', description: 'Tag: v2.0.0' },
{ name: '123/head', value: '123/head', description: 'Pull: 123/head' },
],
paginationToken: undefined,
});
});
it('should use paginationToken when provided', async () => {
const paginationToken = '3';
const owner = 'test-owner';
const repository = 'test-repo';
const refsResponse = [{ ref: 'refs/heads/branch-5' }, { ref: 'refs/heads/branch-6' }];
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockImplementation(
(param: string) => {
if (param === 'owner') return owner;
if (param === 'repository') return repository;
},
);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
refsResponse,
);
const result = await getRefs.call(mockLoadOptionsFunctions, undefined, paginationToken);
expect(result).toEqual({
results: [
{ name: 'branch-5', value: 'branch-5', description: 'Branch: branch-5' },
{ name: 'branch-6', value: 'branch-6', description: 'Branch: branch-6' },
],
paginationToken: undefined,
});
expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'githubOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: expect.objectContaining({ page: 3 }),
}),
);
});
it('should filter refs based on the provided filter', async () => {
const owner = 'test-owner';
const repository = 'test-repo';
const refsResponse = [
{ ref: 'refs/heads/main' },
{ ref: 'refs/heads/dev' },
{ ref: 'refs/tags/v1.0.0' },
{ ref: 'refs/tags/v2.0.0' },
];
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockImplementation(
(param: string) => {
if (param === 'owner') return owner;
if (param === 'repository') return repository;
},
);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
refsResponse,
);
const result = await getRefs.call(mockLoadOptionsFunctions, 'v1');
expect(result).toEqual({
results: [{ name: 'v1.0.0', value: 'v1.0.0', description: 'Tag: v1.0.0' }],
});
});
it('should handle pagination correctly', async () => {
const owner = 'test-owner';
const repository = 'test-repo';
const refsResponse = Array(100)
.fill(0)
.map((_, i) => ({
ref: i % 2 === 0 ? `refs/heads/branch-${i}` : `refs/tags/tag-${i}`,
}));
(mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockImplementation(
(param: string) => {
if (param === 'owner') return owner;
if (param === 'repository') return repository;
},
);
(mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
refsResponse,
);
const result = await getRefs.call(mockLoadOptionsFunctions);
expect(result.paginationToken).toBe(2);
expect(result.results.length).toBe(100);
});
});
});
@@ -0,0 +1,257 @@
import { Github } from '../Github.node';
import { GithubTrigger } from '../GithubTrigger.node';
interface ValidationRule {
type: string;
properties: {
regex: string;
errorMessage: string;
};
}
describe('GitHub Node URL Pattern Tests', () => {
let githubNode: Github;
let githubTriggerNode: GithubTrigger;
const getOwnerUrlMode = () => {
const ownerParam = githubNode.description.properties.find((prop) => prop.name === 'owner');
return ownerParam?.modes?.find((mode) => mode.name === 'url');
};
const getRepositoryUrlMode = () => {
const repoParam = githubNode.description.properties.find((prop) => prop.name === 'repository');
return repoParam?.modes?.find((mode) => mode.name === 'url');
};
const getOwnerExtractRegex = () => {
const mode = getOwnerUrlMode();
return new RegExp(mode?.extractValue?.regex ?? '');
};
const getOwnerValidationRegex = () => {
const mode = getOwnerUrlMode();
const validation = mode?.validation?.[0] as ValidationRule;
return new RegExp(validation?.properties?.regex ?? '');
};
const getRepositoryExtractRegex = () => {
const mode = getRepositoryUrlMode();
return new RegExp(mode?.extractValue?.regex ?? '');
};
const getRepositoryValidationRegex = () => {
const mode = getRepositoryUrlMode();
const validation = mode?.validation?.[0] as ValidationRule;
return new RegExp(validation?.properties?.regex ?? '');
};
// Helper functions for GithubTrigger node
const getTriggerOwnerUrlMode = () => {
const ownerParam = githubTriggerNode.description.properties.find(
(prop) => prop.name === 'owner',
);
return ownerParam?.modes?.find((mode) => mode.name === 'url');
};
const getTriggerRepositoryUrlMode = () => {
const repoParam = githubTriggerNode.description.properties.find(
(prop) => prop.name === 'repository',
);
return repoParam?.modes?.find((mode) => mode.name === 'url');
};
const getTriggerOwnerExtractRegex = () => {
const mode = getTriggerOwnerUrlMode();
return new RegExp(mode?.extractValue?.regex ?? '');
};
const getTriggerOwnerValidationRegex = () => {
const mode = getTriggerOwnerUrlMode();
const validation = mode?.validation?.[0] as ValidationRule;
return new RegExp(validation?.properties?.regex ?? '');
};
const getTriggerRepositoryExtractRegex = () => {
const mode = getTriggerRepositoryUrlMode();
return new RegExp(mode?.extractValue?.regex ?? '');
};
const getTriggerRepositoryValidationRegex = () => {
const mode = getTriggerRepositoryUrlMode();
const validation = mode?.validation?.[0] as ValidationRule;
return new RegExp(validation?.properties?.regex ?? '');
};
beforeEach(() => {
githubNode = new Github();
githubTriggerNode = new GithubTrigger();
});
describe('GitHub Node Resource Locator Patterns', () => {
describe('Owner URL Pattern', () => {
it('should extract owner from github.com URL', () => {
const regex = getOwnerExtractRegex();
const url = 'https://github.com/n8n-io';
const match = url.match(regex);
expect(match?.[1]).toBe('n8n-io');
});
it('should extract owner from custom GitHub URL', () => {
const regex = getOwnerExtractRegex();
const url = 'https://github.company.com/acme-corp';
const match = url.match(regex);
expect(match?.[1]).toBe('acme-corp');
});
it('should validate github.com URL', () => {
const validationRegex = getOwnerValidationRegex();
const url = 'https://github.com/n8n-io';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate custom GitHub URL', () => {
const validationRegex = getOwnerValidationRegex();
const url = 'https://github.company.com/acme-corp';
expect(validationRegex.test(url)).toBe(true);
});
it('should reject invalid URLs', () => {
const validationRegex = getOwnerValidationRegex();
expect(validationRegex.test('not-a-url')).toBe(false);
expect(validationRegex.test('http://github.com/user')).toBe(false);
expect(validationRegex.test('https://')).toBe(false);
});
});
describe('Repository URL Pattern', () => {
it('should extract repository from github.com URL', () => {
const regex = getRepositoryExtractRegex();
const url = 'https://github.com/n8n-io/n8n';
const match = url.match(regex);
expect(match?.[1]).toBe('n8n');
});
it('should extract repository from custom GitHub URL', () => {
const regex = getRepositoryExtractRegex();
const url = 'https://github.company.com/acme-corp/my-repo';
const match = url.match(regex);
expect(match?.[1]).toBe('my-repo');
});
it('should validate github.com repository URL', () => {
const validationRegex = getRepositoryValidationRegex();
const url = 'https://github.com/n8n-io/n8n';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate custom GitHub repository URL', () => {
const validationRegex = getRepositoryValidationRegex();
const url = 'https://github.company.com/acme-corp/my-repo';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate URLs with additional paths', () => {
const validationRegex = getRepositoryValidationRegex();
expect(validationRegex.test('https://github.com/n8n-io/n8n/issues/123')).toBe(true);
expect(validationRegex.test('https://github.company.com/org/repo/pulls')).toBe(true);
});
it('should reject invalid repository URLs', () => {
const validationRegex = getRepositoryValidationRegex();
expect(validationRegex.test('https://github.com/user')).toBe(false);
expect(validationRegex.test('not-a-url')).toBe(false);
expect(validationRegex.test('https://')).toBe(false);
});
});
});
describe('GitHub Trigger Node Resource Locator Patterns', () => {
describe('Owner URL Pattern', () => {
it('should extract owner from github.com URL', () => {
const regex = getTriggerOwnerExtractRegex();
const url = 'https://github.com/n8n-io';
const match = url.match(regex);
expect(match?.[1]).toBe('n8n-io');
});
it('should extract owner from custom GitHub URL', () => {
const regex = getTriggerOwnerExtractRegex();
const url = 'https://github.company.com/my-org';
const match = url.match(regex);
expect(match?.[1]).toBe('my-org');
});
it('should validate github.com URL', () => {
const validationRegex = getTriggerOwnerValidationRegex();
const url = 'https://github.com/n8n-io';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate custom GitHub URL', () => {
const validationRegex = getTriggerOwnerValidationRegex();
const url = 'https://github.company.com/my-org';
expect(validationRegex.test(url)).toBe(true);
});
});
describe('Repository URL Pattern', () => {
it('should extract repository from github.com URL', () => {
const regex = getTriggerRepositoryExtractRegex();
const url = 'https://github.com/n8n-io/n8n';
const match = url.match(regex);
expect(match?.[1]).toBe('n8n');
});
it('should extract repository from custom GitHub URL', () => {
const regex = getTriggerRepositoryExtractRegex();
const url = 'https://github.company.com/my-org/my-repo';
const match = url.match(regex);
expect(match?.[1]).toBe('my-repo');
});
it('should validate github.com repository URL', () => {
const validationRegex = getTriggerRepositoryValidationRegex();
const url = 'https://github.com/n8n-io/n8n';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate custom GitHub repository URL', () => {
const validationRegex = getTriggerRepositoryValidationRegex();
const url = 'https://github.company.com/my-org/my-repo';
expect(validationRegex.test(url)).toBe(true);
});
});
});
describe('URL Pattern Edge Cases', () => {
it('should handle URLs with subdomains', () => {
const ownerRegex = getOwnerExtractRegex();
const repoRegex = getRepositoryExtractRegex();
// Test complex custom URLs
expect('https://git.internal.company.com/dev-team'.match(ownerRegex)?.[1]).toBe('dev-team');
expect('https://github.acme.corp/engineering/backend-api'.match(repoRegex)?.[1]).toBe(
'backend-api',
);
});
it('should handle URLs with ports', () => {
const ownerRegex = getOwnerExtractRegex();
const repoRegex = getRepositoryExtractRegex();
// Test URLs with ports
expect('https://github.local:8080/testuser'.match(ownerRegex)?.[1]).toBe('testuser');
expect('https://git.company.com:443/org/project'.match(repoRegex)?.[1]).toBe('project');
});
it('should handle URLs with additional path segments', () => {
const ownerValidationRegex = getOwnerValidationRegex();
const repoValidationRegex = getRepositoryValidationRegex();
// Test URLs with extra paths
expect(ownerValidationRegex.test('https://github.com/user/settings')).toBe(true);
expect(repoValidationRegex.test('https://github.com/user/repo/issues/123')).toBe(true);
expect(repoValidationRegex.test('https://git.company.com/org/project/pulls')).toBe(true);
});
});
});
@@ -0,0 +1,144 @@
{
"name": "Github Organization getRepositories Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "organization",
"operation": "getRepositories",
"owner": {
"__rl": true,
"value": "testorg",
"mode": "name"
},
"returnAll": true
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get Organization Repositories",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Repositories Response"
}
],
"pinData": {
"Repositories Response": [
{
"json": {
"id": 1296269,
"name": "hello-world",
"full_name": "testorg/hello-world",
"owner": {
"login": "testorg",
"id": 1,
"type": "Organization"
},
"private": false,
"html_url": "https://github.com/testorg/hello-world",
"description": "My first repository on GitHub!",
"fork": false,
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"pushed_at": "2011-01-26T19:06:43Z",
"clone_url": "https://github.com/testorg/hello-world.git",
"size": 108,
"stargazers_count": 80,
"watchers_count": 9,
"language": "C",
"forks_count": 9,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "mit",
"name": "MIT License"
},
"visibility": "public",
"default_branch": "master"
}
},
{
"json": {
"id": 1296270,
"name": "test-repo",
"full_name": "testorg/test-repo",
"owner": {
"login": "testorg",
"id": 1,
"type": "Organization"
},
"private": true,
"html_url": "https://github.com/testorg/test-repo",
"description": "Test repository",
"fork": false,
"created_at": "2011-01-27T19:01:12Z",
"updated_at": "2011-01-27T19:14:43Z",
"pushed_at": "2011-01-27T19:06:43Z",
"clone_url": "https://github.com/testorg/test-repo.git",
"size": 256,
"stargazers_count": 42,
"watchers_count": 15,
"language": "JavaScript",
"forks_count": 3,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": {
"key": "apache-2.0",
"name": "Apache License 2.0"
},
"visibility": "private",
"default_branch": "main"
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get Organization Repositories",
"type": "main",
"index": 0
}
]
]
},
"Get Organization Repositories": {
"main": [
[
{
"node": "Repositories Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,111 @@
{
"name": "Github Organization getRepositories Limit Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "organization",
"operation": "getRepositories",
"owner": {
"__rl": true,
"value": "testorg",
"mode": "name"
},
"returnAll": false,
"limit": 1
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get Organization Repositories Limited",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Repositories Response"
}
],
"pinData": {
"Repositories Response": [
{
"json": {
"id": 1296269,
"name": "hello-world",
"full_name": "testorg/hello-world",
"owner": {
"login": "testorg",
"id": 1,
"type": "Organization"
},
"private": false,
"html_url": "https://github.com/testorg/hello-world",
"description": "My first repository on GitHub!",
"fork": false,
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"pushed_at": "2011-01-26T19:06:43Z",
"clone_url": "https://github.com/testorg/hello-world.git",
"size": 108,
"stargazers_count": 80,
"watchers_count": 9,
"language": "C",
"forks_count": 9,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "mit",
"name": "MIT License"
},
"visibility": "public",
"default_branch": "master"
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get Organization Repositories Limited",
"type": "main",
"index": 0
}
]
]
},
"Get Organization Repositories Limited": {
"main": [
[
{
"node": "Repositories Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,255 @@
{
"name": "Github Repository getIssues Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "repository",
"operation": "getIssues",
"owner": {
"__rl": true,
"value": "testowner",
"mode": "name"
},
"repository": {
"__rl": true,
"value": "testrepo",
"mode": "name"
},
"returnAll": true,
"getRepositoryIssuesFilters": {}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get Repository Issues",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Issues Response"
}
],
"pinData": {
"Issues Response": [
{
"json": {
"url": "https://api.github.com/repos/testowner/testrepo/issues/1",
"repository_url": "https://api.github.com/repos/testowner/testrepo",
"labels_url": "https://api.github.com/repos/testowner/testrepo/issues/1/labels{/name}",
"comments_url": "https://api.github.com/repos/testowner/testrepo/issues/1/comments",
"events_url": "https://api.github.com/repos/testowner/testrepo/issues/1/events",
"html_url": "https://github.com/testowner/testrepo/issues/1",
"id": 1,
"number": 1,
"title": "Found a bug",
"user": {
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/testowner/testrepo/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "d73a49",
"default": true
}
],
"state": "open",
"locked": false,
"assignee": null,
"assignees": [],
"milestone": null,
"comments": 0,
"created_at": "2011-04-22T13:33:48Z",
"updated_at": "2011-04-22T13:33:48Z",
"closed_at": null,
"author_association": "COLLABORATOR",
"active_lock_reason": null,
"body": "I'm having a problem with this.",
"reactions": {
"url": "https://api.github.com/repos/testowner/testrepo/issues/1/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/testowner/testrepo/issues/1/timeline",
"performed_via_github_app": null,
"state_reason": null
}
},
{
"json": {
"url": "https://api.github.com/repos/testowner/testrepo/issues/2",
"repository_url": "https://api.github.com/repos/testowner/testrepo",
"labels_url": "https://api.github.com/repos/testowner/testrepo/issues/2/labels{/name}",
"comments_url": "https://api.github.com/repos/testowner/testrepo/issues/2/comments",
"events_url": "https://api.github.com/repos/testowner/testrepo/issues/2/events",
"html_url": "https://github.com/testowner/testrepo/issues/2",
"id": 2,
"number": 2,
"title": "Feature request",
"user": {
"login": "anotheruser",
"id": 2,
"node_id": "MDQ6VXNlcjI=",
"avatar_url": "https://github.com/images/error/anotheruser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/anotheruser",
"html_url": "https://github.com/anotheruser",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 208045947,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDc=",
"url": "https://api.github.com/repos/testowner/testrepo/labels/enhancement",
"name": "enhancement",
"description": "New feature or request",
"color": "a2eeef",
"default": true
}
],
"state": "open",
"locked": false,
"assignee": {
"login": "assigneduser",
"id": 3,
"node_id": "MDQ6VXNlcjM=",
"avatar_url": "https://github.com/images/error/assigneduser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/assigneduser",
"html_url": "https://github.com/assigneduser",
"type": "User",
"site_admin": false
},
"assignees": [
{
"login": "assigneduser",
"id": 3,
"node_id": "MDQ6VXNlcjM=",
"avatar_url": "https://github.com/images/error/assigneduser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/assigneduser",
"html_url": "https://github.com/assigneduser",
"type": "User",
"site_admin": false
}
],
"milestone": {
"url": "https://api.github.com/repos/testowner/testrepo/milestones/1",
"html_url": "https://github.com/testowner/testrepo/milestone/1",
"labels_url": "https://api.github.com/repos/testowner/testrepo/milestones/1/labels",
"id": 1002604,
"number": 1,
"state": "open",
"title": "v1.0",
"description": "Tracking milestone for version 1.0",
"creator": {
"login": "testowner",
"id": 4,
"node_id": "MDQ6VXNlcjQ=",
"avatar_url": "https://github.com/images/error/testowner_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testowner",
"html_url": "https://github.com/testowner",
"type": "User",
"site_admin": false
},
"open_issues": 4,
"closed_issues": 8,
"created_at": "2011-04-10T20:09:31Z",
"updated_at": "2014-03-03T18:58:10Z",
"closed_at": null,
"due_on": "2018-09-22T23:39:01Z",
"node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA=="
},
"comments": 3,
"created_at": "2011-04-22T13:33:48Z",
"updated_at": "2011-04-22T13:33:48Z",
"closed_at": null,
"author_association": "COLLABORATOR",
"active_lock_reason": null,
"body": "It would be great if we could add this feature.",
"reactions": {
"url": "https://api.github.com/repos/testowner/testrepo/issues/2/reactions",
"total_count": 5,
"+1": 3,
"-1": 1,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 1,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/testowner/testrepo/issues/2/timeline",
"performed_via_github_app": null,
"state_reason": null
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get Repository Issues",
"type": "main",
"index": 0
}
]
]
},
"Get Repository Issues": {
"main": [
[
{
"node": "Issues Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,170 @@
{
"name": "Github Repository getIssues Filtered Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "repository",
"operation": "getIssues",
"owner": {
"__rl": true,
"value": "testowner",
"mode": "name"
},
"repository": {
"__rl": true,
"value": "testrepo",
"mode": "name"
},
"returnAll": true,
"getRepositoryIssuesFilters": {
"state": "closed",
"labels": "bug",
"assignee": "testuser"
}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get Repository Issues Filtered",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Issues Response"
}
],
"pinData": {
"Issues Response": [
{
"json": {
"url": "https://api.github.com/repos/testowner/testrepo/issues/3",
"repository_url": "https://api.github.com/repos/testowner/testrepo",
"labels_url": "https://api.github.com/repos/testowner/testrepo/issues/3/labels{/name}",
"comments_url": "https://api.github.com/repos/testowner/testrepo/issues/3/comments",
"events_url": "https://api.github.com/repos/testowner/testrepo/issues/3/events",
"html_url": "https://github.com/testowner/testrepo/issues/3",
"id": 3,
"number": 3,
"title": "Fixed bug",
"user": {
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/testowner/testrepo/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "d73a49",
"default": true
}
],
"state": "closed",
"locked": false,
"assignee": {
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
},
"assignees": [
{
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
}
],
"milestone": null,
"comments": 2,
"created_at": "2011-04-20T13:33:48Z",
"updated_at": "2011-04-25T13:33:48Z",
"closed_at": "2011-04-25T13:33:48Z",
"author_association": "COLLABORATOR",
"active_lock_reason": null,
"body": "This bug has been fixed.",
"reactions": {
"url": "https://api.github.com/repos/testowner/testrepo/issues/3/reactions",
"total_count": 1,
"+1": 1,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/testowner/testrepo/issues/3/timeline",
"performed_via_github_app": null,
"state_reason": "completed"
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get Repository Issues Filtered",
"type": "main",
"index": 0
}
]
]
},
"Get Repository Issues Filtered": {
"main": [
[
{
"node": "Issues Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,145 @@
{
"name": "Github Repository getIssues Limit Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "repository",
"operation": "getIssues",
"owner": {
"__rl": true,
"value": "testowner",
"mode": "name"
},
"repository": {
"__rl": true,
"value": "testrepo",
"mode": "name"
},
"returnAll": false,
"limit": 1,
"getRepositoryIssuesFilters": {}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get Repository Issues Limited",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Issues Response"
}
],
"pinData": {
"Issues Response": [
{
"json": {
"url": "https://api.github.com/repos/testowner/testrepo/issues/1",
"repository_url": "https://api.github.com/repos/testowner/testrepo",
"labels_url": "https://api.github.com/repos/testowner/testrepo/issues/1/labels{/name}",
"comments_url": "https://api.github.com/repos/testowner/testrepo/issues/1/comments",
"events_url": "https://api.github.com/repos/testowner/testrepo/issues/1/events",
"html_url": "https://github.com/testowner/testrepo/issues/1",
"id": 1,
"number": 1,
"title": "Found a bug",
"user": {
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/testowner/testrepo/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "d73a49",
"default": true
}
],
"state": "open",
"locked": false,
"assignee": null,
"assignees": [],
"milestone": null,
"comments": 0,
"created_at": "2011-04-22T13:33:48Z",
"updated_at": "2011-04-22T13:33:48Z",
"closed_at": null,
"author_association": "COLLABORATOR",
"active_lock_reason": null,
"body": "I'm having a problem with this.",
"reactions": {
"url": "https://api.github.com/repos/testowner/testrepo/issues/1/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/testowner/testrepo/issues/1/timeline",
"performed_via_github_app": null,
"state_reason": null
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get Repository Issues Limited",
"type": "main",
"index": 0
}
]
]
},
"Get Repository Issues Limited": {
"main": [
[
{
"node": "Issues Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,353 @@
{
"name": "Github User getUserIssues Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "user",
"operation": "getUserIssues",
"returnAll": true,
"getUserIssuesFilters": {}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get User Issues",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Issues Response"
}
],
"pinData": {
"Issues Response": [
{
"json": {
"url": "https://api.github.com/repos/someowner/somerepo/issues/1",
"repository_url": "https://api.github.com/repos/someowner/somerepo",
"labels_url": "https://api.github.com/repos/someowner/somerepo/issues/1/labels{/name}",
"comments_url": "https://api.github.com/repos/someowner/somerepo/issues/1/comments",
"events_url": "https://api.github.com/repos/someowner/somerepo/issues/1/events",
"html_url": "https://github.com/someowner/somerepo/issues/1",
"id": 1,
"number": 1,
"title": "Issue assigned to me",
"user": {
"login": "issueauthor",
"id": 5,
"node_id": "MDQ6VXNlcjU=",
"avatar_url": "https://github.com/images/error/issueauthor_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/issueauthor",
"html_url": "https://github.com/issueauthor",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/someowner/somerepo/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "d73a49",
"default": true
}
],
"state": "open",
"locked": false,
"assignee": {
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
},
"assignees": [
{
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
}
],
"milestone": null,
"comments": 0,
"created_at": "2011-04-22T13:33:48Z",
"updated_at": "2011-04-22T13:33:48Z",
"closed_at": null,
"author_association": "NONE",
"active_lock_reason": null,
"body": "This is an issue assigned to me.",
"reactions": {
"url": "https://api.github.com/repos/someowner/somerepo/issues/1/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/someowner/somerepo/issues/1/timeline",
"performed_via_github_app": null,
"state_reason": null,
"repository": {
"id": 1296269,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
"name": "somerepo",
"full_name": "someowner/somerepo",
"owner": {
"login": "someowner",
"id": 6,
"node_id": "MDQ6VXNlcjY=",
"avatar_url": "https://github.com/images/error/someowner_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/someowner",
"html_url": "https://github.com/someowner",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/someowner/somerepo",
"description": "Repository with issues assigned to testuser",
"fork": false,
"url": "https://api.github.com/repos/someowner/somerepo",
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"pushed_at": "2011-01-26T19:06:43Z",
"git_url": "git://github.com/someowner/somerepo.git",
"ssh_url": "git@github.com:someowner/somerepo.git",
"clone_url": "https://github.com/someowner/somerepo.git",
"size": 108,
"stargazers_count": 80,
"watchers_count": 9,
"language": "C",
"has_issues": true,
"has_projects": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 9,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "mit",
"name": "MIT License",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit",
"node_id": "MDc6TGljZW5zZW1pdA=="
},
"forks": 9,
"open_issues": 0,
"watchers": 9,
"default_branch": "master"
}
}
},
{
"json": {
"url": "https://api.github.com/repos/anotherowner/anotherrepo/issues/5",
"repository_url": "https://api.github.com/repos/anotherowner/anotherrepo",
"labels_url": "https://api.github.com/repos/anotherowner/anotherrepo/issues/5/labels{/name}",
"comments_url": "https://api.github.com/repos/anotherowner/anotherrepo/issues/5/comments",
"events_url": "https://api.github.com/repos/anotherowner/anotherrepo/issues/5/events",
"html_url": "https://github.com/anotherowner/anotherrepo/issues/5",
"id": 5,
"number": 5,
"title": "Enhancement request assigned to me",
"user": {
"login": "requestor",
"id": 7,
"node_id": "MDQ6VXNlcjc=",
"avatar_url": "https://github.com/images/error/requestor_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/requestor",
"html_url": "https://github.com/requestor",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 208045947,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDc=",
"url": "https://api.github.com/repos/anotherowner/anotherrepo/labels/enhancement",
"name": "enhancement",
"description": "New feature or request",
"color": "a2eeef",
"default": true
},
{
"id": 208045948,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDg=",
"url": "https://api.github.com/repos/anotherowner/anotherrepo/labels/good-first-issue",
"name": "good first issue",
"description": "Good for newcomers",
"color": "7057ff",
"default": true
}
],
"state": "open",
"locked": false,
"assignee": {
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
},
"assignees": [
{
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
}
],
"milestone": null,
"comments": 1,
"created_at": "2011-04-22T13:33:48Z",
"updated_at": "2011-04-22T13:33:48Z",
"closed_at": null,
"author_association": "CONTRIBUTOR",
"active_lock_reason": null,
"body": "Please add this enhancement.",
"reactions": {
"url": "https://api.github.com/repos/anotherowner/anotherrepo/issues/5/reactions",
"total_count": 2,
"+1": 2,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/anotherowner/anotherrepo/issues/5/timeline",
"performed_via_github_app": null,
"state_reason": null,
"repository": {
"id": 1296270,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2Mjcw",
"name": "anotherrepo",
"full_name": "anotherowner/anotherrepo",
"owner": {
"login": "anotherowner",
"id": 8,
"node_id": "MDQ6VXNlcjg=",
"avatar_url": "https://github.com/images/error/anotherowner_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/anotherowner",
"html_url": "https://github.com/anotherowner",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/anotherowner/anotherrepo",
"description": "Another repository with issues for testuser",
"fork": false,
"url": "https://api.github.com/repos/anotherowner/anotherrepo",
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"pushed_at": "2011-01-26T19:06:43Z",
"git_url": "git://github.com/anotherowner/anotherrepo.git",
"ssh_url": "git@github.com:anotherowner/anotherrepo.git",
"clone_url": "https://github.com/anotherowner/anotherrepo.git",
"size": 256,
"stargazers_count": 42,
"watchers_count": 15,
"language": "JavaScript",
"has_issues": true,
"has_projects": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 3,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 5,
"license": {
"key": "apache-2.0",
"name": "Apache License 2.0",
"spdx_id": "Apache-2.0",
"url": "https://api.github.com/licenses/apache-2.0",
"node_id": "MDc6TGljZW5zZWFwYWNoZS0yLjA="
},
"forks": 3,
"open_issues": 5,
"watchers": 15,
"default_branch": "main"
}
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get User Issues",
"type": "main",
"index": 0
}
]
]
},
"Get User Issues": {
"main": [
[
{
"node": "Issues Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,211 @@
{
"name": "Github User getUserIssues Filtered Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "user",
"operation": "getUserIssues",
"returnAll": true,
"getUserIssuesFilters": {
"state": "closed",
"labels": "enhancement"
}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get User Issues Filtered",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Issues Response"
}
],
"pinData": {
"Issues Response": [
{
"json": {
"url": "https://api.github.com/repos/testowner/closedrepo/issues/10",
"repository_url": "https://api.github.com/repos/testowner/closedrepo",
"labels_url": "https://api.github.com/repos/testowner/closedrepo/issues/10/labels{/name}",
"comments_url": "https://api.github.com/repos/testowner/closedrepo/issues/10/comments",
"events_url": "https://api.github.com/repos/testowner/closedrepo/issues/10/events",
"html_url": "https://github.com/testowner/closedrepo/issues/10",
"id": 10,
"number": 10,
"title": "Completed enhancement",
"user": {
"login": "enhancementauthor",
"id": 9,
"node_id": "MDQ6VXNlcjk=",
"avatar_url": "https://github.com/images/error/enhancementauthor_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/enhancementauthor",
"html_url": "https://github.com/enhancementauthor",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 208045947,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDc=",
"url": "https://api.github.com/repos/testowner/closedrepo/labels/enhancement",
"name": "enhancement",
"description": "New feature or request",
"color": "a2eeef",
"default": true
}
],
"state": "closed",
"locked": false,
"assignee": {
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
},
"assignees": [
{
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
}
],
"milestone": null,
"comments": 5,
"created_at": "2011-04-10T13:33:48Z",
"updated_at": "2011-04-30T13:33:48Z",
"closed_at": "2011-04-30T13:33:48Z",
"author_association": "CONTRIBUTOR",
"active_lock_reason": null,
"body": "Enhancement has been completed successfully.",
"reactions": {
"url": "https://api.github.com/repos/testowner/closedrepo/issues/10/reactions",
"total_count": 3,
"+1": 2,
"-1": 0,
"laugh": 0,
"hooray": 1,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/testowner/closedrepo/issues/10/timeline",
"performed_via_github_app": null,
"state_reason": "completed",
"repository": {
"id": 1296271,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2Mjcx",
"name": "closedrepo",
"full_name": "testowner/closedrepo",
"owner": {
"login": "testowner",
"id": 10,
"node_id": "MDQ6VXNlcjEw",
"avatar_url": "https://github.com/images/error/testowner_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testowner",
"html_url": "https://github.com/testowner",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/testowner/closedrepo",
"description": "Repository with closed enhancement issues",
"fork": false,
"url": "https://api.github.com/repos/testowner/closedrepo",
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"pushed_at": "2011-01-26T19:06:43Z",
"git_url": "git://github.com/testowner/closedrepo.git",
"ssh_url": "git@github.com:testowner/closedrepo.git",
"clone_url": "https://github.com/testowner/closedrepo.git",
"size": 128,
"stargazers_count": 25,
"watchers_count": 5,
"language": "Python",
"has_issues": true,
"has_projects": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 2,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "mit",
"name": "MIT License",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit",
"node_id": "MDc6TGljZW5zZW1pdA=="
},
"forks": 2,
"open_issues": 0,
"watchers": 5,
"default_branch": "main"
}
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get User Issues Filtered",
"type": "main",
"index": 0
}
]
]
},
"Get User Issues Filtered": {
"main": [
[
{
"node": "Issues Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,209 @@
{
"name": "Github User getUserIssues Limit Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "user",
"operation": "getUserIssues",
"returnAll": false,
"limit": 1,
"getUserIssuesFilters": {}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get User Issues Limited",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Issues Response"
}
],
"pinData": {
"Issues Response": [
{
"json": {
"url": "https://api.github.com/repos/someowner/somerepo/issues/1",
"repository_url": "https://api.github.com/repos/someowner/somerepo",
"labels_url": "https://api.github.com/repos/someowner/somerepo/issues/1/labels{/name}",
"comments_url": "https://api.github.com/repos/someowner/somerepo/issues/1/comments",
"events_url": "https://api.github.com/repos/someowner/somerepo/issues/1/events",
"html_url": "https://github.com/someowner/somerepo/issues/1",
"id": 1,
"number": 1,
"title": "Issue assigned to me",
"user": {
"login": "issueauthor",
"id": 5,
"node_id": "MDQ6VXNlcjU=",
"avatar_url": "https://github.com/images/error/issueauthor_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/issueauthor",
"html_url": "https://github.com/issueauthor",
"type": "User",
"site_admin": false
},
"labels": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/someowner/somerepo/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "d73a49",
"default": true
}
],
"state": "open",
"locked": false,
"assignee": {
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
},
"assignees": [
{
"login": "testuser",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/testuser_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/testuser",
"html_url": "https://github.com/testuser",
"type": "User",
"site_admin": false
}
],
"milestone": null,
"comments": 0,
"created_at": "2011-04-22T13:33:48Z",
"updated_at": "2011-04-22T13:33:48Z",
"closed_at": null,
"author_association": "NONE",
"active_lock_reason": null,
"body": "This is an issue assigned to me.",
"reactions": {
"url": "https://api.github.com/repos/someowner/somerepo/issues/1/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/someowner/somerepo/issues/1/timeline",
"performed_via_github_app": null,
"state_reason": null,
"repository": {
"id": 1296269,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
"name": "somerepo",
"full_name": "someowner/somerepo",
"owner": {
"login": "someowner",
"id": 6,
"node_id": "MDQ6VXNlcjY=",
"avatar_url": "https://github.com/images/error/someowner_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/someowner",
"html_url": "https://github.com/someowner",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/someowner/somerepo",
"description": "Repository with issues assigned to testuser",
"fork": false,
"url": "https://api.github.com/repos/someowner/somerepo",
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"pushed_at": "2011-01-26T19:06:43Z",
"git_url": "git://github.com/someowner/somerepo.git",
"ssh_url": "git@github.com:someowner/somerepo.git",
"clone_url": "https://github.com/someowner/somerepo.git",
"size": 108,
"stargazers_count": 80,
"watchers_count": 9,
"language": "C",
"has_issues": true,
"has_projects": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 9,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "mit",
"name": "MIT License",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit",
"node_id": "MDc6TGljZW5zZW1pdA=="
},
"forks": 9,
"open_issues": 0,
"watchers": 9,
"default_branch": "master"
}
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get User Issues Limited",
"type": "main",
"index": 0
}
]
]
},
"Get User Issues Limited": {
"main": [
[
{
"node": "Issues Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,144 @@
{
"name": "Github User getRepositories Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "user",
"operation": "getRepositories",
"owner": {
"__rl": true,
"value": "testuser",
"mode": "name"
},
"returnAll": true
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get User Repositories",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Repositories Response"
}
],
"pinData": {
"Repositories Response": [
{
"json": {
"id": 1296269,
"name": "hello-world",
"full_name": "testuser/hello-world",
"owner": {
"login": "testuser",
"id": 1,
"type": "User"
},
"private": false,
"html_url": "https://github.com/testuser/hello-world",
"description": "My first repository on GitHub!",
"fork": false,
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"pushed_at": "2011-01-26T19:06:43Z",
"clone_url": "https://github.com/testuser/hello-world.git",
"size": 108,
"stargazers_count": 80,
"watchers_count": 9,
"language": "C",
"forks_count": 9,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "mit",
"name": "MIT License"
},
"visibility": "public",
"default_branch": "master"
}
},
{
"json": {
"id": 1296270,
"name": "my-app",
"full_name": "testuser/my-app",
"owner": {
"login": "testuser",
"id": 1,
"type": "User"
},
"private": false,
"html_url": "https://github.com/testuser/my-app",
"description": "My awesome application",
"fork": false,
"created_at": "2011-02-26T19:01:12Z",
"updated_at": "2011-02-26T19:14:43Z",
"pushed_at": "2011-02-26T19:06:43Z",
"clone_url": "https://github.com/testuser/my-app.git",
"size": 512,
"stargazers_count": 156,
"watchers_count": 45,
"language": "JavaScript",
"forks_count": 23,
"archived": false,
"disabled": false,
"open_issues_count": 5,
"license": {
"key": "apache-2.0",
"name": "Apache License 2.0"
},
"visibility": "public",
"default_branch": "main"
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get User Repositories",
"type": "main",
"index": 0
}
]
]
},
"Get User Repositories": {
"main": [
[
{
"node": "Repositories Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,111 @@
{
"name": "Github User getRepositories Limit Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger-id",
"name": "When clicking 'Execute Workflow'"
},
{
"parameters": {
"resource": "user",
"operation": "getRepositories",
"owner": {
"__rl": true,
"value": "testuser",
"mode": "name"
},
"returnAll": false,
"limit": 1
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [200, 0],
"id": "github-node-id",
"name": "Get User Repositories Limited",
"credentials": {
"githubApi": {
"id": "credential-id",
"name": "Test Credentials"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 0],
"id": "noop-id",
"name": "Repositories Response"
}
],
"pinData": {
"Repositories Response": [
{
"json": {
"id": 1296269,
"name": "hello-world",
"full_name": "testuser/hello-world",
"owner": {
"login": "testuser",
"id": 1,
"type": "User"
},
"private": false,
"html_url": "https://github.com/testuser/hello-world",
"description": "My first repository on GitHub!",
"fork": false,
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"pushed_at": "2011-01-26T19:06:43Z",
"clone_url": "https://github.com/testuser/hello-world.git",
"size": 108,
"stargazers_count": 80,
"watchers_count": 9,
"language": "C",
"forks_count": 9,
"archived": false,
"disabled": false,
"open_issues_count": 0,
"license": {
"key": "mit",
"name": "MIT License"
},
"visibility": "public",
"default_branch": "master"
}
}
]
},
"connections": {
"When clicking 'Execute Workflow'": {
"main": [
[
{
"node": "Get User Repositories Limited",
"type": "main",
"index": 0
}
]
]
},
"Get User Repositories Limited": {
"main": [
[
{
"node": "Repositories Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
@@ -0,0 +1,89 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
describe('Test Github Node - Dispatch and Wait', () => {
describe('Workflow Dispatch and Wait', () => {
const now = 1683028800000;
const owner = 'Owner';
const repository = 'test-github-actions';
const workflowId = 145370278;
const ref = 'test-branch';
const usersResponse = {
total_count: 1,
items: [
{
login: owner,
id: 1,
},
],
};
const repositoriesResponse = {
total_count: 1,
items: [
{
id: 3081286,
name: repository,
},
],
};
const workflowsResponse = {
total_count: 1,
workflows: [
{
id: workflowId,
node_id: 'MDg6V29ya2Zsb3cxNjEzMzU=',
name: 'New Test Workflow',
path: '.github/workflows/test.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/${owner}/${repository}/actions/workflows/${workflowId}`,
html_url: `https://github.com/${owner}/${repository}/blob/master/.github/workflows/test.yaml`,
badge_url: `https://github.com/${owner}/${repository}/workflows/New%20Test%20Workflow/badge.svg`,
},
],
};
const refsResponse = [{ ref: `refs/heads/${ref}` }];
beforeAll(async () => {
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
});
beforeEach(async () => {
const baseUrl = 'https://api.github.com';
nock.cleanAll();
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)
.get(`/repos/${owner}/${repository}/git/refs`)
.reply(200, refsResponse)
.post(
`/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`,
(body) => {
return body.ref === ref && body.inputs?.resumeUrl;
},
)
.reply(200, {});
});
afterEach(() => {
nock.cleanAll();
});
new NodeTestHarness().setupTests({
workflowFiles: ['GithubDispatchAndWaitWorkflow.json'],
});
});
});
@@ -0,0 +1,702 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import nock from 'nock';
import * as utilities from '../../../../utils/utilities';
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 });
});
describe('removeTrailingSlash Function', () => {
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: {} }]),
},
};
jest.spyOn(utilities, 'removeTrailingSlash');
jest.mock('../../../../utils/utilities', () => ({
...jest.requireActual('../../../../utils/utilities'),
getFileSha: jest.fn().mockResolvedValue('mockedSHA'),
}));
});
it('should call remove trailing slash', async () => {
mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'operation') {
return 'list';
}
if (parameterName === 'resource') {
return 'file';
}
if (parameterName === 'filePath') {
return 'path/to/file/';
}
if (parameterName === 'owner') {
return 'me';
}
if (parameterName === 'repository') {
return 'repo';
}
return '';
});
await githubNode.execute.call(mockExecutionContext);
expect(utilities.removeTrailingSlash).toHaveBeenCalledWith('path/to/file/');
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'githubOAuth2Api',
{
body: {},
headers: { 'User-Agent': 'n8n' },
json: true,
method: 'GET',
qs: {},
uri: 'https://api.github.com/repos/me/repo/contents/path%2Fto%2Ffile',
},
);
});
});
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' }),
]),
);
});
});
describe('User 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 fetch open issues by default (user:getIssues)', async () => {
mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'resource') return 'user';
if (parameterName === 'operation') return 'getUserIssues';
if (parameterName === 'getUserIssuesFilters') return {};
if (parameterName === 'returnAll') return true;
if (parameterName === 'authentication') return 'accessToken';
return '';
});
mockExecutionContext.helpers.requestWithAuthentication.mockResolvedValue({
body: [
{ id: 1, title: 'Issue 1', state: 'open' },
{ id: 2, title: 'Issue 2', state: 'open' },
],
headers: {},
});
await githubNode.execute.call(mockExecutionContext);
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'GET',
uri: 'https://api.github.com/issues',
qs: expect.not.objectContaining({ state: 'closed' }),
}),
);
});
it('should fetch closed issues when state filter is set to closed (user:getIssues)', async () => {
mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'resource') return 'user';
if (parameterName === 'operation') return 'getUserIssues';
if (parameterName === 'getUserIssuesFilters') return { state: 'closed' };
if (parameterName === 'returnAll') return true;
if (parameterName === 'authentication') return 'accessToken';
return '';
});
mockExecutionContext.helpers.requestWithAuthentication.mockResolvedValue({
body: [{ id: 3, title: 'Issue 3', state: 'closed' }],
headers: {},
});
await githubNode.execute.call(mockExecutionContext);
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'GET',
uri: 'https://api.github.com/issues',
qs: expect.objectContaining({ state: 'closed' }),
}),
);
});
it('should fetch issues with a specific label (user:getIssues)', async () => {
mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
if (parameterName === 'resource') return 'user';
if (parameterName === 'operation') return 'getUserIssues';
if (parameterName === 'getUserIssuesFilters') return { labels: 'bug' };
if (parameterName === 'returnAll') return true;
if (parameterName === 'authentication') return 'accessToken';
return '';
});
mockExecutionContext.helpers.requestWithAuthentication.mockResolvedValue({
body: [{ id: 4, title: 'Issue 4', state: 'open', labels: ['bug'] }],
headers: {},
});
await githubNode.execute.call(mockExecutionContext);
expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
method: 'GET',
uri: 'https://api.github.com/issues',
qs: expect.objectContaining({ labels: 'bug' }),
}),
);
});
});
});
@@ -0,0 +1,64 @@
import type { IWebhookFunctions } from 'n8n-workflow';
import { Github } from '../../Github.node';
describe('Github Node - Webhook Method', () => {
let githubNode: Github;
let mockWebhookFunctions: IWebhookFunctions;
beforeEach(() => {
githubNode = new Github();
mockWebhookFunctions = {
getRequestObject: jest.fn(),
getResponseObject: jest.fn(),
getNodeParameter: jest.fn(),
getNode: jest.fn(),
helpers: {
returnJsonArray: jest.fn(),
},
} as unknown as IWebhookFunctions;
});
it('should process webhook request and return workflowData', async () => {
const sampleWebhookBody = {
action: 'opened',
issue: {
number: 123,
title: 'Test Issue',
body: 'This is a test issue',
user: {
login: 'testuser',
},
},
repository: {
name: 'test-repo',
owner: {
login: 'test-owner',
},
},
};
const mockRequestObject = {
body: sampleWebhookBody,
headers: {
'x-github-event': 'issues',
'x-github-delivery': '72d3162e-cc78-11e3-81ab-4c9367dc0958',
},
};
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue(mockRequestObject);
(mockWebhookFunctions.helpers.returnJsonArray as jest.Mock).mockReturnValue([
sampleWebhookBody,
]);
const result = await githubNode.webhook.call(mockWebhookFunctions);
expect(result).toEqual({
workflowData: [[sampleWebhookBody]],
});
expect(mockWebhookFunctions.getRequestObject).toHaveBeenCalled();
expect(mockWebhookFunctions.helpers.returnJsonArray).toHaveBeenCalledWith(sampleWebhookBody);
});
});
@@ -0,0 +1,76 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [200, -360],
"id": "0889ff06-41e2-4786-a0fe-ca330c3711e7",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "workflow",
"operation": "dispatchAndWait",
"owner": {
"__rl": true,
"value": "Owner",
"mode": "list",
"cachedResultName": "Owner",
"cachedResultUrl": "https://github.com/Owner"
},
"repository": {
"__rl": true,
"value": "test-github-actions",
"mode": "list",
"cachedResultName": "test-github-actions",
"cachedResultUrl": "https://github.com/Owner/test-github-actions"
},
"workflowId": {
"__rl": true,
"value": 145370278,
"mode": "list",
"cachedResultName": "New Test Workflow"
},
"ref": {
"__rl": true,
"value": "test-branch",
"mode": "list",
"cachedResultName": "test-branch"
}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1.1,
"position": [220, 0],
"id": "105cb5f0-bcc3-4397-ba06-bda8cf46c71b",
"name": "Dispatch and Wait for Completion",
"webhookId": "02bfeade-db6c-412a-8627-fe3a9952e8ee",
"credentials": {
"githubApi": {
"id": "RtvkwCTqGZ2sLhB8",
"name": "GitHub account 3"
}
}
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Dispatch and Wait for Completion",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Dispatch and Wait for Completion": [
{
"json": {}
}
]
}
}
@@ -0,0 +1,83 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-300, 260],
"id": "b14bf20f-78b0-490a-bbc6-d02b1af4c03c",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"resource": "workflow",
"workflowId": {
"__rl": true,
"value": 147025216,
"mode": "list",
"cachedResultName": "CI"
},
"owner": {
"__rl": true,
"value": "testOwner",
"mode": "name"
},
"repository": {
"__rl": true,
"value": "testRepository",
"mode": "name"
}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [-80, 260],
"id": "061752c9-507c-4b27-ba18-47b21d487aed",
"name": "GitHub",
"credentials": {
"githubApi": {
"id": "1",
"name": "GitHub account"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [120, 260],
"id": "3bc54e8f-eeba-496d-a95f-bb8927eff671",
"name": "No Operation, do nothing"
}
],
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "GitHub",
"type": "main",
"index": 0
}
]
]
},
"GitHub": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"No Operation, do nothing": [
{
"json": {}
}
]
}
}
@@ -0,0 +1,220 @@
import { GithubTrigger } from '../../GithubTrigger.node';
import * as GenericFunctions from '../../GenericFunctions';
import * as GithubTriggerHelpers from '../../GithubTriggerHelpers';
import { NodeOperationError } from 'n8n-workflow';
describe('GithubTrigger Node', () => {
describe('checkExists webhook method', () => {
let webhookData: Record<string, any>;
let mockThis: any;
beforeEach(() => {
webhookData = {
webhookId: '123456',
webhookEvents: ['push'],
};
mockThis = {
getWorkflowStaticData: () => webhookData,
getNodeParameter: jest.fn().mockImplementation((name: string) => {
if (name === 'owner') return 'some-owner';
if (name === 'repository') return 'some-repo';
}),
};
});
it('should delete webhook data and return false when webhook is not found (404)', async () => {
jest.spyOn(GenericFunctions, 'githubApiRequest').mockRejectedValue({ httpCode: '404' });
const trigger = new GithubTrigger();
const result = await trigger.webhookMethods.default.checkExists.call(mockThis);
expect(result).toBe(false);
expect(webhookData.webhookId).toBeUndefined();
expect(webhookData.webhookEvents).toBeUndefined();
});
});
describe('create webhook method', () => {
let mockThis: any;
let webhookData: Record<string, any>;
beforeEach(() => {
webhookData = {};
mockThis = {
getNodeWebhookUrl: () => 'https://example.com/webhook',
getNodeParameter: jest.fn().mockImplementation((name: string) => {
if (name === 'owner') return 'some-owner';
if (name === 'repository') return 'some-repo';
if (name === 'events') return ['push'];
if (name === 'options') return { insecureSSL: false };
}),
getWorkflowStaticData: () => webhookData,
getNode: () => ({}),
};
});
it('should return true and set webhookId and webhookSecret when creation succeeds', async () => {
const createdWebhook = { id: '789', active: true };
jest.spyOn(GenericFunctions, 'githubApiRequest').mockResolvedValueOnce(createdWebhook);
const trigger = new GithubTrigger();
const result = await trigger.webhookMethods.default.create.call(mockThis);
expect(result).toBe(true);
expect(webhookData.webhookId).toBe('789');
expect(webhookData.webhookSecret).toBeDefined();
expect(typeof webhookData.webhookSecret).toBe('string');
expect(webhookData.webhookSecret.length).toBe(64); // 32 bytes in hex
});
it('should send the secret to GitHub API when creating webhook', async () => {
const createdWebhook = { id: '789', active: true };
const apiRequestSpy = jest
.spyOn(GenericFunctions, 'githubApiRequest')
.mockResolvedValueOnce(createdWebhook);
const trigger = new GithubTrigger();
await trigger.webhookMethods.default.create.call(mockThis);
expect(apiRequestSpy).toHaveBeenCalledWith(
'POST',
'/repos/some-owner/some-repo/hooks',
expect.objectContaining({
config: expect.objectContaining({
secret: expect.any(String),
}),
}),
);
});
it('should handle 422 by checking for existing matching webhook (no secret stored)', async () => {
const existingWebhook = {
id: '123',
events: ['push'],
config: { url: 'https://example.com/webhook' },
};
jest
.spyOn(GenericFunctions, 'githubApiRequest')
.mockRejectedValueOnce({ httpCode: '422' }) // POST fails
.mockResolvedValueOnce([existingWebhook]); // GET returns matching
const trigger = new GithubTrigger();
const result = await trigger.webhookMethods.default.create.call(mockThis);
expect(result).toBe(true);
expect(webhookData.webhookId).toBe('123');
// Existing webhook won't have secret stored (backwards compatibility)
expect(webhookData.webhookSecret).toBeUndefined();
});
it('should throw NodeOperationError if repo is not found (404)', async () => {
jest.spyOn(GenericFunctions, 'githubApiRequest').mockRejectedValue({ httpCode: '404' });
const trigger = new GithubTrigger();
await expect(trigger.webhookMethods.default.create.call(mockThis)).rejects.toThrow(
NodeOperationError,
);
await expect(trigger.webhookMethods.default.create.call(mockThis)).rejects.toThrow(
/Check that the repository exists/,
);
});
});
describe('delete webhook method', () => {
let webhookData: Record<string, any>;
let mockThis: any;
beforeEach(() => {
webhookData = {
webhookId: '123456',
webhookEvents: ['push'],
webhookSecret: 'test-secret',
};
mockThis = {
getWorkflowStaticData: () => webhookData,
getNodeParameter: jest.fn().mockImplementation((name: string) => {
if (name === 'owner') return 'some-owner';
if (name === 'repository') return 'some-repo';
}),
};
});
it('should delete webhook data including secret when deletion succeeds', async () => {
jest.spyOn(GenericFunctions, 'githubApiRequest').mockResolvedValueOnce({});
const trigger = new GithubTrigger();
const result = await trigger.webhookMethods.default.delete.call(mockThis);
expect(result).toBe(true);
expect(webhookData.webhookId).toBeUndefined();
expect(webhookData.webhookEvents).toBeUndefined();
expect(webhookData.webhookSecret).toBeUndefined();
});
});
describe('webhook method', () => {
let mockThis: any;
let webhookData: Record<string, any>;
beforeEach(() => {
webhookData = {
webhookSecret: 'test-secret',
};
mockThis = {
getWorkflowStaticData: () => webhookData,
getBodyData: jest.fn().mockReturnValue({ action: 'opened' }),
getHeaderData: jest.fn().mockReturnValue({}),
getQueryData: jest.fn().mockReturnValue({}),
getResponseObject: jest.fn().mockReturnValue({
status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
end: jest.fn(),
}),
getRequestObject: jest.fn().mockReturnValue({
header: jest.fn(),
rawBody: '{}',
}),
helpers: {
returnJsonArray: jest.fn().mockImplementation((data) => data),
},
};
});
it('should reject with 401 when signature verification fails', async () => {
jest.spyOn(GithubTriggerHelpers, 'verifySignature').mockReturnValueOnce(false);
const trigger = new GithubTrigger();
const result = await trigger.webhook.call(mockThis);
expect(result).toEqual({ noWebhookResponse: true });
expect(mockThis.getResponseObject).toHaveBeenCalled();
});
it('should process webhook when signature verification succeeds', async () => {
jest.spyOn(GithubTriggerHelpers, 'verifySignature').mockReturnValueOnce(true);
const trigger = new GithubTrigger();
const result = await trigger.webhook.call(mockThis);
expect(result).toHaveProperty('workflowData');
});
it('should return OK for ping events when signature verification succeeds', async () => {
jest.spyOn(GithubTriggerHelpers, 'verifySignature').mockReturnValueOnce(true);
mockThis.getBodyData.mockReturnValue({ hook_id: '123' });
const trigger = new GithubTrigger();
const result = await trigger.webhook.call(mockThis);
expect(result).toEqual({ webhookResponse: 'OK' });
});
});
});
@@ -0,0 +1,5 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
describe('Test Github Oauth2 Credentials Expression', () => {
new NodeTestHarness().setupTests();
});
@@ -0,0 +1,103 @@
{
"name": "My workflow 255",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-300, 160],
"id": "ab490d05-67aa-4061-b01d-4478e9984c75",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "13c9caa1-e4e6-4365-8282-747dd318abce",
"name": "server",
"value": "https://github.example.com/api/v3",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [-40, 160],
"id": "562791a3-7ff5-4f29-901b-5b1061cc2da2",
"name": "Edit Fields"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "13c9caa1-e4e6-4365-8282-747dd318abce",
"name": "authUrl",
"value": "={{$json[\"server\"] === \"https://api.github.com\" ? \"https://github.com\" : $json[\"server\"].split(\"://\")[0] + \"://\" + $json[\"server\"].split(\"://\")[1].split(\"/\")[0]}}/login/oauth/authorize",
"type": "string"
},
{
"id": "02bd6a24-1375-490e-9683-7faedea36e20",
"name": "accessTokenUrl",
"value": "={{$json[\"server\"] === \"https://api.github.com\" ? \"https://github.com\" : $json[\"server\"].split(\"://\")[0] + \"://\" + $json[\"server\"].split(\"://\")[1].split(\"/\")[0]}}/login/oauth/access_token",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [200, 160],
"id": "73059a97-269e-4c1a-b034-b114f3f5e671",
"name": "Edit Fields1"
}
],
"pinData": {
"Edit Fields1": [
{
"json": {
"authUrl": "https://github.example.com/login/oauth/authorize",
"accessTokenUrl": "https://github.example.com/login/oauth/access_token"
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "2acb676c-cdfb-4f4d-9000-decf4e2fdb93",
"meta": {
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
},
"id": "s9LDuRXe2e5LF5jP",
"tags": []
}