fix(GitHub Node): Tolerate trailing slash in file path (#15517)

This commit is contained in:
Michael Kret
2025-06-17 18:21:57 +03:00
committed by GitHub
parent c64ccf74a2
commit 2f6896cc7b
6 changed files with 93 additions and 11 deletions

View File

@@ -25,6 +25,7 @@ import {
validateJSON,
} from './GenericFunctions';
import { getRefs, getRepositories, getUsers, getWorkflows } from './SearchFunctions';
import { removeTrailingSlash } from '../../utils/utilities';
import { defaultWebhookDescription } from '../Webhook/description';
export class Github implements INodeType {
@@ -2250,7 +2251,7 @@ export class Github implements INodeType {
requestMethod = 'PUT';
const filePath = this.getNodeParameter('filePath', i);
const filePath = removeTrailingSlash(this.getNodeParameter('filePath', i));
const additionalParameters = this.getNodeParameter(
'additionalParameters',
@@ -2326,7 +2327,7 @@ export class Github implements INodeType {
body.branch = (additionalParameters.branch as IDataObject).branch;
}
const filePath = this.getNodeParameter('filePath', i);
const filePath = removeTrailingSlash(this.getNodeParameter('filePath', i));
body.message = this.getNodeParameter('commitMessage', i) as string;
body.sha = await getFileSha.call(
@@ -2341,7 +2342,7 @@ export class Github implements INodeType {
} else if (operation === 'get') {
requestMethod = 'GET';
const filePath = this.getNodeParameter('filePath', i);
const filePath = removeTrailingSlash(this.getNodeParameter('filePath', i));
const additionalParameters = this.getNodeParameter(
'additionalParameters',
i,
@@ -2354,7 +2355,7 @@ export class Github implements INodeType {
endpoint = `/repos/${owner}/${repository}/contents/${encodeURIComponent(filePath)}`;
} else if (operation === 'list') {
requestMethod = 'GET';
const filePath = this.getNodeParameter('filePath', i);
const filePath = removeTrailingSlash(this.getNodeParameter('filePath', i));
endpoint = `/repos/${owner}/${repository}/contents/${encodeURIComponent(filePath)}`;
}
} else if (resource === 'issue') {

View File

@@ -2,6 +2,7 @@ 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', () => {
@@ -62,6 +63,73 @@ describe('Test Github Node', () => {
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)

View File

@@ -11,12 +11,7 @@ import type {
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
export const removeTrailingSlash = (url: string) => {
if (url.endsWith('/')) {
return url.slice(0, -1);
}
return url;
};
import { removeTrailingSlash } from '../../utils/utilities';
export async function strapiApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions,

View File

@@ -14,11 +14,11 @@ import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { entryFields, entryOperations } from './EntryDescription';
import {
getToken,
removeTrailingSlash,
strapiApiRequest,
strapiApiRequestAllItems,
validateJSON,
} from './GenericFunctions';
import { removeTrailingSlash } from '../../utils/utilities';
export class Strapi implements INodeType {
description: INodeTypeDescription = {

View File

@@ -6,6 +6,7 @@ import {
fuzzyCompare,
getResolvables,
keysToLowercase,
removeTrailingSlash,
shuffleArray,
sortItemKeysByPriorityList,
wrapData,
@@ -312,3 +313,13 @@ describe('sortItemKeysByPriorityList', () => {
expect(Object.keys(result[0].json)).toEqual(['a', 'b', 'd']);
});
});
describe('removeTrailingSlash', () => {
it('removes trailing slash', () => {
expect(removeTrailingSlash('https://example.com/')).toBe('https://example.com');
});
it('does not change a URL without trailing slash', () => {
expect(removeTrailingSlash('https://example.com')).toBe('https://example.com');
});
});

View File

@@ -472,3 +472,10 @@ export function createUtmCampaignLink(nodeType: string, instanceId?: string) {
nodeType,
)}${instanceId ? '_' + instanceId : ''}`;
}
export const removeTrailingSlash = (url: string) => {
if (url.endsWith('/')) {
return url.slice(0, -1);
}
return url;
};