diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index 699d157394..5b31d53db6 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -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') { diff --git a/packages/nodes-base/nodes/Github/__tests__/node/Github.node.test.ts b/packages/nodes-base/nodes/Github/__tests__/node/Github.node.test.ts index 7eb32dfd87..bde4d5d978 100644 --- a/packages/nodes-base/nodes/Github/__tests__/node/Github.node.test.ts +++ b/packages/nodes-base/nodes/Github/__tests__/node/Github.node.test.ts @@ -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) diff --git a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts index b687766037..3182f869e1 100644 --- a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts @@ -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, diff --git a/packages/nodes-base/nodes/Strapi/Strapi.node.ts b/packages/nodes-base/nodes/Strapi/Strapi.node.ts index 565975ef19..fea4ce72c2 100644 --- a/packages/nodes-base/nodes/Strapi/Strapi.node.ts +++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts @@ -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 = { diff --git a/packages/nodes-base/utils/__tests__/utilities.test.ts b/packages/nodes-base/utils/__tests__/utilities.test.ts index a30552f6f5..af55106344 100644 --- a/packages/nodes-base/utils/__tests__/utilities.test.ts +++ b/packages/nodes-base/utils/__tests__/utilities.test.ts @@ -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'); + }); +}); diff --git a/packages/nodes-base/utils/utilities.ts b/packages/nodes-base/utils/utilities.ts index 2b3d68e2e7..6084f9c14b 100644 --- a/packages/nodes-base/utils/utilities.ts +++ b/packages/nodes-base/utils/utilities.ts @@ -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; +};