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,131 @@
import type {
IExecuteFunctions,
IHookFunctions,
IDataObject,
ILoadOptionsFunctions,
JsonObject,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
/**
* Make an API request to Github
*
*/
export async function githubApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: object,
query?: IDataObject,
option: IDataObject = {},
): Promise<any> {
const options: IRequestOptions = {
method,
headers: {
'User-Agent': 'n8n',
},
body,
qs: query,
uri: '',
json: true,
};
if (Object.keys(option).length !== 0) {
Object.assign(options, option);
}
try {
const authenticationMethod = this.getNodeParameter(
'authentication',
0,
'accessToken',
) as string;
let credentialType = '';
if (authenticationMethod === 'accessToken') {
const credentials = await this.getCredentials('githubApi');
credentialType = 'githubApi';
const baseUrl = credentials.server || 'https://api.github.com';
options.uri = `${baseUrl}${endpoint}`;
} else {
const credentials = await this.getCredentials('githubOAuth2Api');
credentialType = 'githubOAuth2Api';
const baseUrl = credentials.server || 'https://api.github.com';
options.uri = `${baseUrl}${endpoint}`;
}
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
/**
* Returns the SHA of the given file
*
* @param {(IHookFunctions | IExecuteFunctions)} this
*/
export async function getFileSha(
this: IHookFunctions | IExecuteFunctions,
owner: string,
repository: string,
filePath: string,
branch?: string,
): Promise<any> {
const query: IDataObject = {};
if (branch !== undefined) {
query.ref = branch;
}
const getEndpoint = `/repos/${owner}/${repository}/contents/${encodeURI(filePath)}`;
const responseData = await githubApiRequest.call(this, 'GET', getEndpoint, {}, query);
if (responseData.sha === undefined) {
throw new NodeOperationError(this.getNode(), 'Could not get the SHA of the file.');
}
return responseData.sha;
}
export async function githubApiRequestAllItems(
this: IHookFunctions | IExecuteFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
query.per_page = 100;
query.page = 1;
do {
responseData = await githubApiRequest.call(this, method, endpoint, body as IDataObject, query, {
resolveWithFullResponse: true,
});
query.page++;
returnData.push.apply(returnData, responseData.body as IDataObject[]);
} while (responseData.headers.link?.includes('next'));
return returnData;
}
export function isBase64(content: string) {
const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
return base64regex.test(content);
}
export function validateJSON(json: string | undefined): any {
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}
@@ -0,0 +1,40 @@
{
"node": "n8n-nodes-base.github",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/github/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.github/"
}
],
"generic": [
{
"label": "Automatically pulling and visualizing data with n8n",
"icon": "📈",
"url": "https://n8n.io/blog/automatically-pulling-and-visualizing-data-with-n8n/"
},
{
"label": "How to automatically manage contributions to open-source projects",
"icon": "🏷️",
"url": "https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/"
},
{
"label": "5 workflow automations for Mattermost that we love at n8n",
"icon": "🤖",
"url": "https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/"
},
{
"label": "How to set up a no-code CI/CD pipeline with GitHub and TravisCI",
"icon": "🎡",
"url": "https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/"
}
]
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
{
"node": "n8n-nodes-base.githubTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/github/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.githubtrigger/"
}
],
"generic": [
{
"label": "How to automatically manage contributions to open-source projects",
"icon": "🏷️",
"url": "https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/"
},
{
"label": "5 workflow automations for Mattermost that we love at n8n",
"icon": "🤖",
"url": "https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/"
},
{
"label": "How to set up a no-code CI/CD pipeline with GitHub and TravisCI",
"icon": "🎡",
"url": "https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/"
}
]
}
}
@@ -0,0 +1,659 @@
import { randomBytes } from 'crypto';
import type {
IHookFunctions,
IWebhookFunctions,
IDataObject,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
JsonObject,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeApiError, NodeOperationError } from 'n8n-workflow';
import { githubApiRequest } from './GenericFunctions';
import { verifySignature } from './GithubTriggerHelpers';
import { getRepositories, getUsers } from './SearchFunctions';
export class GithubTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Github Trigger',
name: 'githubTrigger',
icon: { light: 'file:github.svg', dark: 'file:github.dark.svg' },
group: ['trigger'],
version: 1,
subtitle:
'={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
description: 'Starts the workflow when Github events occur',
defaults: {
name: 'Github Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'githubApi',
required: true,
displayOptions: {
show: {
authentication: ['accessToken'],
},
},
},
{
name: 'githubOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName:
'Only members with owner privileges for an organization or admin privileges for a repository can set up the webhooks this node requires.',
name: 'notice',
type: 'notice',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
},
{
displayName: 'Repository Owner',
name: 'owner',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'Repository Owner',
name: 'list',
type: 'list',
placeholder: 'Select an owner...',
typeOptions: {
searchListMethod: 'getUsers',
searchable: true,
searchFilterRequired: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://github.com/n8n-io',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/(?:[^/]+)\\/([-_0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/([^/]+)\\/([-_0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid Github URL',
},
},
],
},
{
displayName: 'By Name',
name: 'name',
type: 'string',
placeholder: 'e.g. n8n-io',
validation: [
{
type: 'regex',
properties: {
regex: '[-_a-zA-Z0-9]+',
errorMessage: 'Not a valid Github Owner Name',
},
},
],
url: '=https://github.com/{{$value}}',
},
],
},
{
displayName: 'Repository Name',
name: 'repository',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'Repository Name',
name: 'list',
type: 'list',
placeholder: 'Select an Repository...',
typeOptions: {
searchListMethod: 'getRepositories',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://github.com/n8n-io/n8n',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/(?:[^/]+)\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/([^/]+)\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid Github Repository URL',
},
},
],
},
{
displayName: 'By Name',
name: 'name',
type: 'string',
placeholder: 'e.g. n8n',
validation: [
{
type: 'regex',
properties: {
regex: '[-_.0-9a-zA-Z]+',
errorMessage: 'Not a valid Github Repository Name',
},
},
],
url: '=https://github.com/{{$parameter["owner"]}}/{{$value}}',
},
],
},
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
options: [
{
name: '*',
value: '*',
description: 'Any time any event is triggered (Wildcard Event)',
},
{
name: 'Check Run',
value: 'check_run',
description:
'Triggered when a check run is created, rerequested, completed, or has a requested_action',
},
{
name: 'Check Suite',
value: 'check_suite',
description: 'Triggered when a check suite is completed, requested, or rerequested',
},
{
name: 'Commit Comment',
value: 'commit_comment',
description: 'Triggered when a commit comment is created',
},
{
name: 'Create',
value: 'create',
description: 'Represents a created repository, branch, or tag',
},
{
name: 'Delete',
value: 'delete',
description: 'Represents a deleted branch or tag',
},
{
name: 'Deploy Key',
value: 'deploy_key',
description: 'Triggered when a deploy key is added or removed from a repository',
},
{
name: 'Deployment',
value: 'deployment',
description: 'Represents a deployment',
},
{
name: 'Deployment Status',
value: 'deployment_status',
description: 'Represents a deployment status',
},
{
name: 'Fork',
value: 'fork',
description: 'Triggered when a user forks a repository',
},
{
name: 'Github App Authorization',
value: 'github_app_authorization',
description: 'Triggered when someone revokes their authorization of a GitHub App',
},
{
name: 'Gollum',
value: 'gollum',
description: 'Triggered when a Wiki page is created or updated',
},
{
name: 'Installation',
value: 'installation',
description:
'Triggered when someone installs (created), uninstalls (deleted), or accepts new permissions (new_permissions_accepted) for a GitHub App. When a GitHub App owner requests new permissions, the person who installed the GitHub App must accept the new permissions request.',
},
{
name: 'Installation Repositories',
value: 'installation_repositories',
description: 'Triggered when a repository is added or removed from an installation',
},
{
name: 'Issue Comment',
value: 'issue_comment',
description: 'Triggered when an issue comment is created, edited, or deleted',
},
{
name: 'Issues',
value: 'issues',
description:
'Triggered when an issue is opened, edited, deleted, transferred, pinned, unpinned, closed, reopened, assigned, unassigned, labeled, unlabeled, locked, unlocked, milestoned, or demilestoned',
},
{
name: 'Label',
value: 'label',
description: "Triggered when a repository's label is created, edited, or deleted",
},
{
name: 'Marketplace Purchase',
value: 'marketplace_purchase',
description:
'Triggered when someone purchases a GitHub Marketplace plan, cancels their plan, upgrades their plan (effective immediately), downgrades a plan that remains pending until the end of the billing cycle, or cancels a pending plan change',
},
{
name: 'Member',
value: 'member',
description:
'Triggered when a user accepts an invitation or is removed as a collaborator to a repository, or has their permissions changed',
},
{
name: 'Membership',
value: 'membership',
description:
'Triggered when a user is added or removed from a team. Organization hooks only.',
},
{
name: 'Meta',
value: 'meta',
description: 'Triggered when the webhook that this event is configured on is deleted',
},
{
name: 'Milestone',
value: 'milestone',
description:
'Triggered when a milestone is created, closed, opened, edited, or deleted',
},
{
name: 'Org Block',
value: 'org_block',
description:
'Triggered when an organization blocks or unblocks a user. Organization hooks only.',
},
{
name: 'Organization',
value: 'organization',
description:
'Triggered when an organization is deleted and renamed, and when a user is added, removed, or invited to an organization. Organization hooks only.',
},
{
name: 'Page Build',
value: 'page_build',
description:
'Triggered on push to a GitHub Pages enabled branch (gh-pages for project pages, master for user and organization pages)',
},
{
name: 'Project',
value: 'project',
description:
'Triggered when a project is created, updated, closed, reopened, or deleted',
},
{
name: 'Project Card',
value: 'project_card',
description:
'Triggered when a project card is created, edited, moved, converted to an issue, or deleted',
},
{
name: 'Project Column',
value: 'project_column',
description: 'Triggered when a project column is created, updated, moved, or deleted',
},
{
name: 'Public',
value: 'public',
description: 'Triggered when a private repository is open sourced',
},
{
name: 'Pull Request',
value: 'pull_request',
description:
'Triggered when a pull request is assigned, unassigned, labeled, unlabeled, opened, edited, closed, reopened, synchronize, ready_for_review, locked, unlocked, a pull request review is requested, or a review request is removed',
},
{
name: 'Pull Request Review',
value: 'pull_request_review',
description:
'Triggered when a pull request review is submitted into a non-pending state, the body is edited, or the review is dismissed',
},
{
name: 'Pull Request Review Comment',
value: 'pull_request_review_comment',
description:
"Triggered when a comment on a pull request's unified diff is created, edited, or deleted (in the Files Changed tab)",
},
{
name: 'Push',
value: 'push',
description:
'Triggered on a push to a repository branch. Branch pushes and repository tag pushes also trigger webhook push events. This is the default event.',
},
{
name: 'Release',
value: 'release',
description:
'Triggered when a release is published, unpublished, created, edited, deleted, or prereleased',
},
{
name: 'Repository',
value: 'repository',
description:
'Triggered when a repository is created, archived, unarchived, renamed, edited, transferred, made public, or made private. Organization hooks are also triggered when a repository is deleted.',
},
{
name: 'Repository Import',
value: 'repository_import',
description:
'Triggered when a successful, cancelled, or failed repository import finishes for a GitHub organization or a personal repository',
},
{
name: 'Repository Vulnerability Alert',
value: 'repository_vulnerability_alert',
description: 'Triggered when a security alert is created, dismissed, or resolved',
},
{
name: 'Security Advisory',
value: 'security_advisory',
description:
'Triggered when a new security advisory is published, updated, or withdrawn',
},
{
name: 'Star',
value: 'star',
description: 'Triggered when a star is added or removed from a repository',
},
{
name: 'Status',
value: 'status',
description: 'Triggered when the status of a Git commit changes',
},
{
name: 'Team',
value: 'team',
description:
"Triggered when an organization's team is created, deleted, edited, added_to_repository, or removed_from_repository. Organization hooks only.",
},
{
name: 'Team Add',
value: 'team_add',
description: 'Triggered when a repository is added to a team',
},
{
name: 'Watch',
value: 'watch',
description: 'Triggered when someone stars a repository',
},
],
required: true,
default: [],
description: 'The events to listen to',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Insecure SSL',
name: 'insecureSSL',
type: 'boolean',
default: false,
description:
'Whether the SSL certificate of the n8n host be verified by GitHub when delivering payloads',
},
],
},
],
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId === undefined) {
// No webhook id is set so no webhook can exist
return false;
}
// Webhook got created before so check if it still exists
const owner = this.getNodeParameter('owner', '', { extractValue: true }) as string;
const repository = this.getNodeParameter('repository', '', {
extractValue: true,
}) as string;
const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`;
try {
await githubApiRequest.call(this, 'GET', endpoint, {});
} catch (error) {
if (error.httpCode === '404') {
// Webhook does not exist
delete webhookData.webhookId;
delete webhookData.webhookEvents;
return false;
}
// Some error occurred
throw error;
}
// If it did not error then the webhook exists
return true;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
if (webhookUrl.includes('//localhost')) {
throw new NodeOperationError(
this.getNode(),
'The Webhook can not work on "localhost". Please setup n8n on a custom domain.',
);
}
const owner = this.getNodeParameter('owner', '', { extractValue: true }) as string;
const repository = this.getNodeParameter('repository', '', {
extractValue: true,
}) as string;
const events = this.getNodeParameter('events', []);
const endpoint = `/repos/${owner}/${repository}/hooks`;
const options = this.getNodeParameter('options') as { insecureSSL: boolean };
// Generate a secure random secret for webhook signature verification
const webhookSecret = randomBytes(32).toString('hex');
const body = {
name: 'web',
config: {
url: webhookUrl,
content_type: 'json',
insecure_ssl: options.insecureSSL ? '1' : '0',
secret: webhookSecret,
},
events,
active: true,
};
const webhookData = this.getWorkflowStaticData('node');
let responseData;
try {
responseData = await githubApiRequest.call(this, 'POST', endpoint, body);
} catch (error) {
if (error.httpCode === '422') {
// Webhook exists already
// Get the data of the already registered webhook
responseData = await githubApiRequest.call(this, 'GET', endpoint, body);
for (const webhook of responseData as IDataObject[]) {
if ((webhook.config! as IDataObject).url! === webhookUrl) {
// Webhook got found
if (JSON.stringify(webhook.events) === JSON.stringify(events)) {
// Webhook with same events exists already so no need to
// create it again simply save the webhook-id
webhookData.webhookId = webhook.id as string;
webhookData.webhookEvents = webhook.events as string[];
// Legacy webhook without secret on GitHub's side - not setting webhookData.webhookSecret
// so signature verification is skipped. To enable it, deactivate and reactivate the workflow.
return true;
}
}
}
throw new NodeOperationError(
this.getNode(),
'A webhook with the identical URL probably exists already. Please delete it manually on Github!',
{ level: 'warning' },
);
}
if (error.httpCode === '404') {
throw new NodeOperationError(
this.getNode(),
'Check that the repository exists and that you have permission to create the webhooks this node requires',
{ level: 'warning' },
);
}
throw error;
}
if (responseData.id === undefined || responseData.active !== true) {
// Required data is missing so was not successful
throw new NodeApiError(this.getNode(), responseData as JsonObject, {
message: 'Github webhook creation response did not contain the expected data.',
});
}
webhookData.webhookId = responseData.id as string;
webhookData.webhookEvents = responseData.events as string[];
webhookData.webhookSecret = webhookSecret;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const owner = this.getNodeParameter('owner', '', { extractValue: true }) as string;
const repository = this.getNodeParameter('repository', '', {
extractValue: true,
}) as string;
const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`;
const body = {};
try {
await githubApiRequest.call(this, 'DELETE', endpoint, body);
} catch (error) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registered anymore
delete webhookData.webhookId;
delete webhookData.webhookEvents;
delete webhookData.webhookSecret;
}
return true;
},
},
};
methods = {
listSearch: {
getUsers,
getRepositories,
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
// Verify the webhook signature before processing
if (!verifySignature.call(this)) {
const res = this.getResponseObject();
res.status(401).send('Unauthorized').end();
return {
noWebhookResponse: true,
};
}
const bodyData = this.getBodyData();
// Check if the webhook is only the ping from Github to confirm if it works
if (bodyData.hook_id !== undefined && bodyData.action === undefined) {
// Is only the ping and not an actual webhook call. So return 'OK'
// but do not start the workflow.
return {
webhookResponse: 'OK',
};
}
// Is a regular webhook call
// TODO: Add headers & requestPath
const returnData: IDataObject[] = [];
returnData.push({
body: bodyData,
headers: this.getHeaderData(),
query: this.getQueryData(),
});
return {
workflowData: [this.helpers.returnJsonArray(returnData)],
};
}
}
@@ -0,0 +1,72 @@
import { createHmac, timingSafeEqual } from 'crypto';
import type { IWebhookFunctions } from 'n8n-workflow';
/**
* Verifies the GitHub webhook signature using HMAC-SHA256.
*
* GitHub sends a signature in the `X-Hub-Signature-256` header in the format:
* `sha256=<HMAC hex digest>`
*
* This function computes the expected signature using the stored webhook secret
* and compares it with the provided signature using a constant-time comparison.
*
* @returns true if signature is valid or no secret is configured, false otherwise
*/
export function verifySignature(this: IWebhookFunctions): boolean {
// Get the secret from workflow static data (set during webhook creation)
const webhookData = this.getWorkflowStaticData('node');
const webhookSecret = webhookData.webhookSecret as string | undefined;
// If no secret is configured, skip verification (backwards compatibility)
if (!webhookSecret) {
return true;
}
const req = this.getRequestObject();
// Get the signature from GitHub's header
const signature = req.header('x-hub-signature-256');
if (!signature) {
return false;
}
// Validate signature format (must start with "sha256=")
if (!signature.startsWith('sha256=')) {
return false;
}
// Extract just the hex digest part
const providedSignature = signature.substring(7);
try {
// Get the raw request body
if (!req.rawBody) {
return false;
}
// Compute HMAC-SHA256 of the raw body using our secret
const hmac = createHmac('sha256', webhookSecret);
if (Buffer.isBuffer(req.rawBody)) {
hmac.update(req.rawBody);
} else {
const rawBodyString =
typeof req.rawBody === 'string' ? req.rawBody : JSON.stringify(req.rawBody);
hmac.update(rawBodyString);
}
const computedSignature = hmac.digest('hex');
const computedBuffer = Buffer.from(computedSignature, 'utf8');
const providedBuffer = Buffer.from(providedSignature, 'utf8');
// Buffers must be same length for timingSafeEqual
if (computedBuffer.length !== providedBuffer.length) {
return false;
}
return timingSafeEqual(computedBuffer, providedBuffer);
} catch {
return false;
}
}
@@ -0,0 +1,182 @@
import type {
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
} from 'n8n-workflow';
import { githubApiRequest } from './GenericFunctions';
type UserSearchItem = {
login: string;
html_url: string;
};
type RepositorySearchItem = {
name: string;
html_url: string;
};
type UserSearchResponse = {
items: UserSearchItem[];
total_count: number;
};
type RepositorySearchResponse = {
items: RepositorySearchItem[];
total_count: number;
};
type RefItem = {
ref: string;
};
export async function getUsers(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
let responseData: UserSearchResponse = {
items: [],
total_count: 0,
};
try {
responseData = await githubApiRequest.call(
this,
'GET',
'/search/users',
{},
{ q: filter, page, per_page },
);
} catch {
// will fail if the owner does not have any users
}
const results: INodeListSearchItems[] = responseData.items.map((item: UserSearchItem) => ({
name: item.login,
value: item.login,
url: item.html_url,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}
export async function getRepositories(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const owner = this.getCurrentNodeParameter('owner', { extractValue: true });
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
const q = `${filter ?? ''} user:${owner} fork:true`;
let responseData: RepositorySearchResponse = {
items: [],
total_count: 0,
};
try {
responseData = await githubApiRequest.call(
this,
'GET',
'/search/repositories',
{},
{ q, page, per_page },
);
} catch {
// will fail if the owner does not have any repositories
}
const results: INodeListSearchItems[] = responseData.items.map((item: RepositorySearchItem) => ({
name: item.name,
value: item.name,
url: item.html_url,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}
export async function getWorkflows(
this: ILoadOptionsFunctions,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const owner = this.getCurrentNodeParameter('owner', { extractValue: true });
const repository = this.getCurrentNodeParameter('repository', { extractValue: true });
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
const endpoint = `/repos/${owner}/${repository}/actions/workflows`;
let responseData: { workflows: Array<{ id: string; name: string }>; total_count: number } = {
workflows: [],
total_count: 0,
};
try {
responseData = await githubApiRequest.call(this, 'GET', endpoint, {}, { page, per_page });
} catch {
// will fail if the repository does not have any workflows
}
const results: INodeListSearchItems[] = responseData.workflows.map((workflow) => ({
name: workflow.name,
value: workflow.id,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}
export async function getRefs(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const owner = this.getCurrentNodeParameter('owner', { extractValue: true });
const repository = this.getCurrentNodeParameter('repository', { extractValue: true });
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
const responseData: RefItem[] = await githubApiRequest.call(
this,
'GET',
`/repos/${owner}/${repository}/git/refs`,
{},
{ page, per_page },
);
const refs: INodeListSearchItems[] = [];
for (const ref of responseData) {
const refPath = ref.ref.split('/');
const refType = refPath[1];
const refName = refPath.slice(2).join('/');
let description = '';
if (refType === 'heads') {
description = `Branch: ${refName}`;
} else if (refType === 'tags') {
description = `Tag: ${refName}`;
} else {
description = `${refType}: ${refName}`;
}
refs.push({
name: refName,
value: refName,
description,
});
}
if (filter) {
const filteredRefs = refs.filter((ref) =>
ref.name.toLowerCase().includes(filter.toLowerCase()),
);
return { results: filteredRefs };
}
const nextPaginationToken = responseData.length === per_page ? page + 1 : undefined;
return { results: refs, paginationToken: nextPaginationToken };
}
@@ -0,0 +1,148 @@
{
"type": "object",
"properties": {
"commit": {
"type": "object",
"properties": {
"author": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"committer": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"html_url": {
"type": "string"
},
"message": {
"type": "string"
},
"node_id": {
"type": "string"
},
"parents": {
"type": "array",
"items": {
"type": "object",
"properties": {
"html_url": {
"type": "string"
},
"sha": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"sha": {
"type": "string"
},
"tree": {
"type": "object",
"properties": {
"sha": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"url": {
"type": "string"
},
"verification": {
"type": "object",
"properties": {
"payload": {
"type": "null"
},
"reason": {
"type": "string"
},
"signature": {
"type": "null"
},
"verified": {
"type": "boolean"
},
"verified_at": {
"type": "null"
}
}
}
}
},
"content": {
"type": "object",
"properties": {
"_links": {
"type": "object",
"properties": {
"git": {
"type": "string"
},
"html": {
"type": "string"
},
"self": {
"type": "string"
}
}
},
"download_url": {
"type": "string"
},
"git_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"sha": {
"type": "string"
},
"size": {
"type": "integer"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,105 @@
{
"type": "object",
"properties": {
"commit": {
"type": "object",
"properties": {
"author": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"committer": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"html_url": {
"type": "string"
},
"message": {
"type": "string"
},
"node_id": {
"type": "string"
},
"parents": {
"type": "array",
"items": {
"type": "object",
"properties": {
"html_url": {
"type": "string"
},
"sha": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"sha": {
"type": "string"
},
"tree": {
"type": "object",
"properties": {
"sha": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"url": {
"type": "string"
},
"verification": {
"type": "object",
"properties": {
"payload": {
"type": "null"
},
"reason": {
"type": "string"
},
"signature": {
"type": "null"
},
"verified": {
"type": "boolean"
},
"verified_at": {
"type": "null"
}
}
}
}
},
"content": {
"type": "null"
}
},
"version": 1
}
@@ -0,0 +1,148 @@
{
"type": "object",
"properties": {
"commit": {
"type": "object",
"properties": {
"author": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"committer": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"html_url": {
"type": "string"
},
"message": {
"type": "string"
},
"node_id": {
"type": "string"
},
"parents": {
"type": "array",
"items": {
"type": "object",
"properties": {
"html_url": {
"type": "string"
},
"sha": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"sha": {
"type": "string"
},
"tree": {
"type": "object",
"properties": {
"sha": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"url": {
"type": "string"
},
"verification": {
"type": "object",
"properties": {
"payload": {
"type": "null"
},
"reason": {
"type": "string"
},
"signature": {
"type": "null"
},
"verified": {
"type": "boolean"
},
"verified_at": {
"type": "null"
}
}
}
}
},
"content": {
"type": "object",
"properties": {
"_links": {
"type": "object",
"properties": {
"git": {
"type": "string"
},
"html": {
"type": "string"
},
"self": {
"type": "string"
}
}
},
"download_url": {
"type": "string"
},
"git_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"sha": {
"type": "string"
},
"size": {
"type": "integer"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,8 @@
{
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
}
@@ -0,0 +1,44 @@
{
"type": "object",
"properties": {
"_links": {
"type": "object",
"properties": {
"git": {
"type": "string"
},
"html": {
"type": "string"
},
"self": {
"type": "string"
}
}
},
"git_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"sha": {
"type": "string"
},
"size": {
"type": "integer"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,260 @@
{
"type": "object",
"properties": {
"active_lock_reason": {
"type": "null"
},
"assignees": {
"type": "array",
"items": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"author_association": {
"type": "string"
},
"closed_at": {
"type": "null"
},
"closed_by": {
"type": "null"
},
"comments": {
"type": "integer"
},
"comments_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"events_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"default": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"labels_url": {
"type": "string"
},
"locked": {
"type": "boolean"
},
"milestone": {
"type": "null"
},
"node_id": {
"type": "string"
},
"number": {
"type": "integer"
},
"performed_via_github_app": {
"type": "null"
},
"reactions": {
"type": "object",
"properties": {
"-1": {
"type": "integer"
},
"+1": {
"type": "integer"
},
"confused": {
"type": "integer"
},
"eyes": {
"type": "integer"
},
"heart": {
"type": "integer"
},
"hooray": {
"type": "integer"
},
"laugh": {
"type": "integer"
},
"rocket": {
"type": "integer"
},
"total_count": {
"type": "integer"
},
"url": {
"type": "string"
}
}
},
"repository_url": {
"type": "string"
},
"state": {
"type": "string"
},
"state_reason": {
"type": "null"
},
"timeline_url": {
"type": "string"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"user": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,133 @@
{
"type": "object",
"properties": {
"author_association": {
"type": "string"
},
"body": {
"type": "string"
},
"created_at": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"issue_url": {
"type": "string"
},
"node_id": {
"type": "string"
},
"performed_via_github_app": {
"type": "null"
},
"reactions": {
"type": "object",
"properties": {
"-1": {
"type": "integer"
},
"+1": {
"type": "integer"
},
"confused": {
"type": "integer"
},
"eyes": {
"type": "integer"
},
"heart": {
"type": "integer"
},
"hooray": {
"type": "integer"
},
"laugh": {
"type": "integer"
},
"rocket": {
"type": "integer"
},
"total_count": {
"type": "integer"
},
"url": {
"type": "string"
}
}
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"user": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,313 @@
{
"type": "object",
"properties": {
"active_lock_reason": {
"type": "null"
},
"assignee": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
},
"assignees": {
"type": "array",
"items": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
}
},
"author_association": {
"type": "string"
},
"comments": {
"type": "integer"
},
"comments_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"events_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"default": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"labels_url": {
"type": "string"
},
"locked": {
"type": "boolean"
},
"node_id": {
"type": "string"
},
"number": {
"type": "integer"
},
"performed_via_github_app": {
"type": "null"
},
"reactions": {
"type": "object",
"properties": {
"-1": {
"type": "integer"
},
"+1": {
"type": "integer"
},
"confused": {
"type": "integer"
},
"eyes": {
"type": "integer"
},
"heart": {
"type": "integer"
},
"hooray": {
"type": "integer"
},
"laugh": {
"type": "integer"
},
"rocket": {
"type": "integer"
},
"total_count": {
"type": "integer"
},
"url": {
"type": "string"
}
}
},
"repository_url": {
"type": "string"
},
"state": {
"type": "string"
},
"timeline_url": {
"type": "string"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"user": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,366 @@
{
"type": "object",
"properties": {
"allow_forking": {
"type": "boolean"
},
"archive_url": {
"type": "string"
},
"archived": {
"type": "boolean"
},
"assignees_url": {
"type": "string"
},
"blobs_url": {
"type": "string"
},
"branches_url": {
"type": "string"
},
"clone_url": {
"type": "string"
},
"collaborators_url": {
"type": "string"
},
"comments_url": {
"type": "string"
},
"commits_url": {
"type": "string"
},
"compare_url": {
"type": "string"
},
"contents_url": {
"type": "string"
},
"contributors_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"default_branch": {
"type": "string"
},
"deployments_url": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"downloads_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"fork": {
"type": "boolean"
},
"forks": {
"type": "integer"
},
"forks_count": {
"type": "integer"
},
"forks_url": {
"type": "string"
},
"full_name": {
"type": "string"
},
"git_commits_url": {
"type": "string"
},
"git_refs_url": {
"type": "string"
},
"git_tags_url": {
"type": "string"
},
"git_url": {
"type": "string"
},
"has_discussions": {
"type": "boolean"
},
"has_downloads": {
"type": "boolean"
},
"has_issues": {
"type": "boolean"
},
"has_pages": {
"type": "boolean"
},
"has_projects": {
"type": "boolean"
},
"has_wiki": {
"type": "boolean"
},
"hooks_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_template": {
"type": "boolean"
},
"issue_comment_url": {
"type": "string"
},
"issue_events_url": {
"type": "string"
},
"issues_url": {
"type": "string"
},
"keys_url": {
"type": "string"
},
"labels_url": {
"type": "string"
},
"languages_url": {
"type": "string"
},
"merges_url": {
"type": "string"
},
"milestones_url": {
"type": "string"
},
"mirror_url": {
"type": "null"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"notifications_url": {
"type": "string"
},
"open_issues": {
"type": "integer"
},
"open_issues_count": {
"type": "integer"
},
"owner": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
},
"permissions": {
"type": "object",
"properties": {
"admin": {
"type": "boolean"
},
"maintain": {
"type": "boolean"
},
"pull": {
"type": "boolean"
},
"push": {
"type": "boolean"
},
"triage": {
"type": "boolean"
}
}
},
"private": {
"type": "boolean"
},
"pulls_url": {
"type": "string"
},
"pushed_at": {
"type": "string"
},
"releases_url": {
"type": "string"
},
"security_and_analysis": {
"type": "object",
"properties": {
"advanced_security": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
},
"dependabot_security_updates": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
},
"secret_scanning": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
},
"secret_scanning_non_provider_patterns": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
},
"secret_scanning_push_protection": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
},
"secret_scanning_validity_checks": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
}
}
},
"size": {
"type": "integer"
},
"ssh_url": {
"type": "string"
},
"stargazers_count": {
"type": "integer"
},
"stargazers_url": {
"type": "string"
},
"statuses_url": {
"type": "string"
},
"subscribers_url": {
"type": "string"
},
"subscription_url": {
"type": "string"
},
"svn_url": {
"type": "string"
},
"tags_url": {
"type": "string"
},
"teams_url": {
"type": "string"
},
"topics": {
"type": "array",
"items": {
"type": "string"
}
},
"trees_url": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"visibility": {
"type": "string"
},
"watchers": {
"type": "integer"
},
"watchers_count": {
"type": "integer"
},
"web_commit_signoff_required": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,216 @@
{
"type": "object",
"properties": {
"assets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"browser_download_url": {
"type": "string"
},
"content_type": {
"type": "string"
},
"created_at": {
"type": "string"
},
"download_count": {
"type": "integer"
},
"id": {
"type": "integer"
},
"label": {
"type": "null"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"size": {
"type": "integer"
},
"state": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"uploader": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
},
"url": {
"type": "string"
}
}
}
},
"assets_url": {
"type": "string"
},
"author": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
},
"body": {
"type": "string"
},
"created_at": {
"type": "string"
},
"draft": {
"type": "boolean"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"mentions_count": {
"type": "integer"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"prerelease": {
"type": "boolean"
},
"tag_name": {
"type": "string"
},
"target_commitish": {
"type": "string"
},
"upload_url": {
"type": "string"
},
"url": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,355 @@
{
"type": "object",
"properties": {
"allow_auto_merge": {
"type": "boolean"
},
"allow_forking": {
"type": "boolean"
},
"allow_merge_commit": {
"type": "boolean"
},
"allow_rebase_merge": {
"type": "boolean"
},
"allow_squash_merge": {
"type": "boolean"
},
"allow_update_branch": {
"type": "boolean"
},
"archive_url": {
"type": "string"
},
"archived": {
"type": "boolean"
},
"assignees_url": {
"type": "string"
},
"blobs_url": {
"type": "string"
},
"branches_url": {
"type": "string"
},
"clone_url": {
"type": "string"
},
"collaborators_url": {
"type": "string"
},
"comments_url": {
"type": "string"
},
"commits_url": {
"type": "string"
},
"compare_url": {
"type": "string"
},
"contents_url": {
"type": "string"
},
"contributors_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"default_branch": {
"type": "string"
},
"delete_branch_on_merge": {
"type": "boolean"
},
"deployments_url": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"downloads_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"fork": {
"type": "boolean"
},
"forks": {
"type": "integer"
},
"forks_count": {
"type": "integer"
},
"forks_url": {
"type": "string"
},
"full_name": {
"type": "string"
},
"git_commits_url": {
"type": "string"
},
"git_refs_url": {
"type": "string"
},
"git_tags_url": {
"type": "string"
},
"git_url": {
"type": "string"
},
"has_discussions": {
"type": "boolean"
},
"has_downloads": {
"type": "boolean"
},
"has_issues": {
"type": "boolean"
},
"has_pages": {
"type": "boolean"
},
"has_projects": {
"type": "boolean"
},
"has_wiki": {
"type": "boolean"
},
"hooks_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_template": {
"type": "boolean"
},
"issue_comment_url": {
"type": "string"
},
"issue_events_url": {
"type": "string"
},
"issues_url": {
"type": "string"
},
"keys_url": {
"type": "string"
},
"labels_url": {
"type": "string"
},
"languages_url": {
"type": "string"
},
"merge_commit_message": {
"type": "string"
},
"merge_commit_title": {
"type": "string"
},
"merges_url": {
"type": "string"
},
"milestones_url": {
"type": "string"
},
"mirror_url": {
"type": "null"
},
"name": {
"type": "string"
},
"network_count": {
"type": "integer"
},
"node_id": {
"type": "string"
},
"notifications_url": {
"type": "string"
},
"open_issues": {
"type": "integer"
},
"open_issues_count": {
"type": "integer"
},
"owner": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
},
"permissions": {
"type": "object",
"properties": {
"admin": {
"type": "boolean"
},
"maintain": {
"type": "boolean"
},
"pull": {
"type": "boolean"
},
"push": {
"type": "boolean"
},
"triage": {
"type": "boolean"
}
}
},
"private": {
"type": "boolean"
},
"pulls_url": {
"type": "string"
},
"pushed_at": {
"type": "string"
},
"releases_url": {
"type": "string"
},
"size": {
"type": "integer"
},
"squash_merge_commit_message": {
"type": "string"
},
"squash_merge_commit_title": {
"type": "string"
},
"ssh_url": {
"type": "string"
},
"stargazers_count": {
"type": "integer"
},
"stargazers_url": {
"type": "string"
},
"statuses_url": {
"type": "string"
},
"subscribers_count": {
"type": "integer"
},
"subscribers_url": {
"type": "string"
},
"subscription_url": {
"type": "string"
},
"svn_url": {
"type": "string"
},
"tags_url": {
"type": "string"
},
"teams_url": {
"type": "string"
},
"temp_clone_token": {
"type": "string"
},
"topics": {
"type": "array",
"items": {
"type": "string"
}
},
"trees_url": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"use_squash_pr_title_as_default": {
"type": "boolean"
},
"visibility": {
"type": "string"
},
"watchers": {
"type": "integer"
},
"watchers_count": {
"type": "integer"
},
"web_commit_signoff_required": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,251 @@
{
"type": "object",
"properties": {
"active_lock_reason": {
"type": "null"
},
"assignees": {
"type": "array",
"items": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
}
},
"author_association": {
"type": "string"
},
"comments": {
"type": "integer"
},
"comments_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"events_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"default": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
},
"labels_url": {
"type": "string"
},
"locked": {
"type": "boolean"
},
"node_id": {
"type": "string"
},
"number": {
"type": "integer"
},
"performed_via_github_app": {
"type": "null"
},
"reactions": {
"type": "object",
"properties": {
"-1": {
"type": "integer"
},
"+1": {
"type": "integer"
},
"confused": {
"type": "integer"
},
"eyes": {
"type": "integer"
},
"heart": {
"type": "integer"
},
"hooray": {
"type": "integer"
},
"laugh": {
"type": "integer"
},
"rocket": {
"type": "integer"
},
"total_count": {
"type": "integer"
},
"url": {
"type": "string"
}
}
},
"repository_url": {
"type": "string"
},
"state": {
"type": "string"
},
"timeline_url": {
"type": "string"
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"user": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,70 @@
{
"type": "object",
"properties": {
"_links": {
"type": "object",
"properties": {
"git": {
"type": "string"
},
"html": {
"type": "string"
},
"self": {
"type": "string"
}
}
},
"content": {
"type": "string"
},
"download_url": {
"type": "string"
},
"encoding": {
"type": "string"
},
"git_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"license": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"spdx_id": {
"type": "string"
}
}
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"sha": {
"type": "string"
},
"size": {
"type": "integer"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,313 @@
{
"type": "object",
"properties": {
"allow_forking": {
"type": "boolean"
},
"archive_url": {
"type": "string"
},
"archived": {
"type": "boolean"
},
"assignees_url": {
"type": "string"
},
"blobs_url": {
"type": "string"
},
"branches_url": {
"type": "string"
},
"clone_url": {
"type": "string"
},
"collaborators_url": {
"type": "string"
},
"comments_url": {
"type": "string"
},
"commits_url": {
"type": "string"
},
"compare_url": {
"type": "string"
},
"contents_url": {
"type": "string"
},
"contributors_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"default_branch": {
"type": "string"
},
"deployments_url": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"downloads_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"fork": {
"type": "boolean"
},
"forks": {
"type": "integer"
},
"forks_count": {
"type": "integer"
},
"forks_url": {
"type": "string"
},
"full_name": {
"type": "string"
},
"git_commits_url": {
"type": "string"
},
"git_refs_url": {
"type": "string"
},
"git_tags_url": {
"type": "string"
},
"git_url": {
"type": "string"
},
"has_discussions": {
"type": "boolean"
},
"has_downloads": {
"type": "boolean"
},
"has_issues": {
"type": "boolean"
},
"has_pages": {
"type": "boolean"
},
"has_projects": {
"type": "boolean"
},
"has_wiki": {
"type": "boolean"
},
"hooks_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_template": {
"type": "boolean"
},
"issue_comment_url": {
"type": "string"
},
"issue_events_url": {
"type": "string"
},
"issues_url": {
"type": "string"
},
"keys_url": {
"type": "string"
},
"labels_url": {
"type": "string"
},
"languages_url": {
"type": "string"
},
"merges_url": {
"type": "string"
},
"milestones_url": {
"type": "string"
},
"mirror_url": {
"type": "null"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"notifications_url": {
"type": "string"
},
"open_issues": {
"type": "integer"
},
"open_issues_count": {
"type": "integer"
},
"owner": {
"type": "object",
"properties": {
"avatar_url": {
"type": "string"
},
"events_url": {
"type": "string"
},
"followers_url": {
"type": "string"
},
"following_url": {
"type": "string"
},
"gists_url": {
"type": "string"
},
"gravatar_id": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"login": {
"type": "string"
},
"node_id": {
"type": "string"
},
"organizations_url": {
"type": "string"
},
"received_events_url": {
"type": "string"
},
"repos_url": {
"type": "string"
},
"site_admin": {
"type": "boolean"
},
"starred_url": {
"type": "string"
},
"subscriptions_url": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"user_view_type": {
"type": "string"
}
}
},
"permissions": {
"type": "object",
"properties": {
"admin": {
"type": "boolean"
},
"maintain": {
"type": "boolean"
},
"pull": {
"type": "boolean"
},
"push": {
"type": "boolean"
},
"triage": {
"type": "boolean"
}
}
},
"private": {
"type": "boolean"
},
"pulls_url": {
"type": "string"
},
"pushed_at": {
"type": "string"
},
"releases_url": {
"type": "string"
},
"size": {
"type": "integer"
},
"ssh_url": {
"type": "string"
},
"stargazers_count": {
"type": "integer"
},
"stargazers_url": {
"type": "string"
},
"statuses_url": {
"type": "string"
},
"subscribers_url": {
"type": "string"
},
"subscription_url": {
"type": "string"
},
"svn_url": {
"type": "string"
},
"tags_url": {
"type": "string"
},
"teams_url": {
"type": "string"
},
"topics": {
"type": "array",
"items": {
"type": "string"
}
},
"trees_url": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"visibility": {
"type": "string"
},
"watchers": {
"type": "integer"
},
"watchers_count": {
"type": "integer"
},
"web_commit_signoff_required": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,47 @@
{
"type": "object",
"properties": {
"total_count": {
"type": "integer"
},
"workflows": {
"type": "array",
"items": {
"type": "object",
"properties": {
"badge_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"path": {
"type": "string"
},
"state": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
},
"version": 1
}
@@ -0,0 +1,50 @@
{
"type": "object",
"properties": {
"_links": {
"type": "object",
"properties": {
"git": {
"type": "string"
},
"html": {
"type": "string"
},
"self": {
"type": "string"
}
}
},
"download_url": {
"type": "string"
},
"encoding": {
"type": "string"
},
"git_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"sha": {
"type": "string"
},
"size": {
"type": "integer"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
},
"version": 5
}
@@ -0,0 +1,44 @@
{
"type": "object",
"properties": {
"_links": {
"type": "object",
"properties": {
"git": {
"type": "string"
},
"html": {
"type": "string"
},
"self": {
"type": "string"
}
}
},
"git_url": {
"type": "string"
},
"html_url": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"sha": {
"type": "string"
},
"size": {
"type": "integer"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"status": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,36 @@
{
"type": "object",
"properties": {
"badge_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"path": {
"type": "string"
},
"state": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,47 @@
{
"type": "object",
"properties": {
"total_count": {
"type": "integer"
},
"workflows": {
"type": "array",
"items": {
"type": "object",
"properties": {
"badge_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"html_url": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"node_id": {
"type": "string"
},
"path": {
"type": "string"
},
"state": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
}
}
}
}
},
"version": 1
}
@@ -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": []
}
@@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0165 0C8.94791 0 0 9.01388 0 20.1653C0 29.0792 5.73324 36.6246 13.6868 39.2952C14.6812 39.496 15.0454 38.8613 15.0454 38.3274C15.0454 37.8599 15.0126 36.2575 15.0126 34.5879C9.4445 35.79 8.28498 32.1841 8.28498 32.1841C7.39015 29.847 6.06429 29.2463 6.06429 29.2463C4.24185 28.011 6.19704 28.011 6.19704 28.011C8.21861 28.1446 9.27938 30.081 9.27938 30.081C11.0686 33.1522 13.9518 32.2844 15.1118 31.7502C15.2773 30.4481 15.8079 29.5467 16.3713 29.046C11.9303 28.5785 7.25781 26.8425 7.25781 19.0967C7.25781 16.8932 8.05267 15.0905 9.31216 13.6884C9.11344 13.1877 8.41732 11.1174 9.51128 8.34644C9.51128 8.34644 11.2014 7.81217 15.0122 10.4164C16.6438 9.97495 18.3263 9.7504 20.0165 9.74851C21.7067 9.74851 23.4295 9.98246 25.0205 10.4164C28.8317 7.81217 30.5218 8.34644 30.5218 8.34644C31.6158 11.1174 30.9192 13.1877 30.7205 13.6884C32.0132 15.0905 32.7753 16.8932 32.7753 19.0967C32.7753 26.8425 28.1028 28.5449 23.6287 29.046C24.358 29.6802 24.9873 30.882 24.9873 32.7851C24.9873 35.4893 24.9545 37.6596 24.9545 38.327C24.9545 38.8613 25.3192 39.496 26.3132 39.2956C34.2667 36.6242 39.9999 29.0792 39.9999 20.1653C40.0327 9.01388 31.052 0 20.0165 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0165 0C8.94791 0 0 9.01388 0 20.1653C0 29.0792 5.73324 36.6246 13.6868 39.2952C14.6812 39.496 15.0454 38.8613 15.0454 38.3274C15.0454 37.8599 15.0126 36.2575 15.0126 34.5879C9.4445 35.79 8.28498 32.1841 8.28498 32.1841C7.39015 29.847 6.06429 29.2463 6.06429 29.2463C4.24185 28.011 6.19704 28.011 6.19704 28.011C8.21861 28.1446 9.27938 30.081 9.27938 30.081C11.0686 33.1522 13.9518 32.2844 15.1118 31.7502C15.2773 30.4481 15.8079 29.5467 16.3713 29.046C11.9303 28.5785 7.25781 26.8425 7.25781 19.0967C7.25781 16.8932 8.05267 15.0905 9.31216 13.6884C9.11344 13.1877 8.41732 11.1174 9.51128 8.34644C9.51128 8.34644 11.2014 7.81217 15.0122 10.4164C16.6438 9.97495 18.3263 9.7504 20.0165 9.74851C21.7067 9.74851 23.4295 9.98246 25.0205 10.4164C28.8317 7.81217 30.5218 8.34644 30.5218 8.34644C31.6158 11.1174 30.9192 13.1877 30.7205 13.6884C32.0132 15.0905 32.7753 16.8932 32.7753 19.0967C32.7753 26.8425 28.1028 28.5449 23.6287 29.046C24.358 29.6802 24.9873 30.882 24.9873 32.7851C24.9873 35.4893 24.9545 37.6596 24.9545 38.327C24.9545 38.8613 25.3192 39.496 26.3132 39.2956C34.2667 36.6242 39.9999 29.0792 39.9999 20.1653C40.0327 9.01388 31.052 0 20.0165 0Z" fill="#24292F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB