diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index 094defd1ebc..ffa8d1eed0f 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -324,6 +324,12 @@ export class Github implements INodeType { description: 'Returns the repositories of a user', action: "Get a user's repositories", }, + { + name: 'Get Issues', + value: 'getIssues', + description: 'Returns the issues assigned to the user', + action: "Get a user's issues", + }, { name: 'Invite', value: 'invite', @@ -548,7 +554,8 @@ export class Github implements INodeType { ], displayOptions: { hide: { - operation: ['invite'], + resource: ['user'], + operation: ['invite', 'getIssues'], }, }, }, @@ -2093,6 +2100,147 @@ export class Github implements INodeType { default: 50, description: 'Max number of results to return', }, + + // ---------------------------------- + // user:getIssues + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['user'], + operation: ['getIssues'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['user'], + operation: ['getIssues'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'getUserIssuesFilters', + type: 'collection', + typeOptions: { + multipleValueButtonText: 'Add Filter', + }, + displayOptions: { + show: { + resource: ['user'], + operation: ['getIssues'], + }, + }, + default: {}, + options: [ + { + displayName: 'Mentioned', + name: 'mentioned', + type: 'string', + default: '', + description: 'Return only issues in which a specific user was mentioned', + }, + { + displayName: 'Labels', + name: 'labels', + type: 'string', + default: '', + description: + 'Return only issues with the given labels. Multiple labels can be separated by comma.', + }, + { + displayName: 'Updated Since', + name: 'since', + type: 'dateTime', + default: '', + description: 'Return only issues updated at or after this time', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + options: [ + { + name: 'All', + value: 'all', + description: 'Returns issues with any state', + }, + { + name: 'Closed', + value: 'closed', + description: 'Return issues with "closed" state', + }, + { + name: 'Open', + value: 'open', + description: 'Return issues with "open" state', + }, + ], + default: 'open', + description: 'The state to set', + }, + { + displayName: 'Sort', + name: 'sort', + type: 'options', + options: [ + { + name: 'Created', + value: 'created', + description: 'Sort by created date', + }, + { + name: 'Updated', + value: 'updated', + description: 'Sort by updated date', + }, + { + name: 'Comments', + value: 'comments', + description: 'Sort by comments', + }, + ], + default: 'created', + description: 'The order the issues should be returned in', + }, + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'asc', + description: 'Sort in ascending order', + }, + { + name: 'Descending', + value: 'desc', + description: 'Sort in descending order', + }, + ], + default: 'desc', + description: 'The sort order', + }, + ], + }, ], }; @@ -2158,6 +2306,7 @@ export class Github implements INodeType { 'repository:listPopularPaths', 'repository:listReferrers', 'user:getRepositories', + 'user:getIssues', 'release:getAll', 'review:getAll', 'organization:getRepositories', @@ -2231,7 +2380,7 @@ export class Github implements INodeType { qs = {}; let owner = ''; - if (fullOperation !== 'user:invite') { + if (fullOperation !== 'user:invite' && fullOperation !== 'user:getIssues') { // Request the parameters which almost all operations need owner = this.getNodeParameter('owner', i, '', { extractValue: true }) as string; } @@ -2239,6 +2388,7 @@ export class Github implements INodeType { let repository = ''; if ( fullOperation !== 'user:getRepositories' && + fullOperation !== 'user:getIssues' && fullOperation !== 'user:invite' && fullOperation !== 'organization:getRepositories' ) { @@ -2629,6 +2779,20 @@ export class Github implements INodeType { returnAll = this.getNodeParameter('returnAll', 0); + if (!returnAll) { + qs.per_page = this.getNodeParameter('limit', 0); + } + } else if (operation === 'getIssues') { + // ---------------------------------- + // getIssues + // ---------------------------------- + requestMethod = 'GET'; + + endpoint = '/issues'; + + qs = this.getNodeParameter('getUserIssuesFilters', i, {}) as IDataObject; + + returnAll = this.getNodeParameter('returnAll', 0); if (!returnAll) { qs.per_page = this.getNodeParameter('limit', 0); } 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 bde4d5d9780..571ab35eb13 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 @@ -594,4 +594,109 @@ describe('Test Github Node', () => { ); }); }); + + 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 'getIssues'; + 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 'getIssues'; + 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 'getIssues'; + 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' }), + }), + ); + }); + }); });