diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts index ce57fac9e5..acb19af65f 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.test.ts @@ -37,7 +37,7 @@ jest.mock('@gitbeaker/rest', () => ({ describe('gitlab:issues:create', () => { beforeEach(() => { jest.clearAllMocks(); - advanceTo(new Date(1990, 9, 24, 12, 0, 0)); // Set the desired date and time + advanceTo(new Date(1988, 5, 3, 12, 0, 0)); // Set the desired date and time }); afterEach(() => { @@ -157,4 +157,61 @@ describe('gitlab:issues:create', () => { 'https://gitlab.com/hangar18-/issues/42', ); }); + + it('should return a Gitlab issue when called with several input params', async () => { + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueType: 'incident', + title: 'Computer banks to rule the world', + description: + 'this issue should kickstart research on instruments to sight the stars', + dueDate: '1990-08-20T23:59:59Z', + token: 'myAwesomeToken', + assignees: [3, 14, 15], + labels: 'operation:mindcrime', + }, + workspacePath: 'seen2much', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 123, + 'Computer banks to rule the world', + { + issueType: 'incident', + description: + 'this issue should kickstart research on instruments to sight the stars', + assigneeIds: [3, 14, 15], + confidential: false, + epicId: undefined, + labels: 'operation:mindcrime', + createdAt: new Date().toISOString(), + dueDate: '1990-08-20T23:59:59.000Z', + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts index 408d307612..62dc94d14a 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabIssueAction.ts @@ -177,7 +177,6 @@ export const createGitlabIssueAction = (options: { ); } } - console.log('After epic test'); const mappedCreatedAt = convertDate( String(createdAt), new Date().toISOString(), @@ -200,11 +199,6 @@ export const createGitlabIssueAction = (options: { milestoneId, weight, }; - console.log('After setting issues options'); - console.log({ issueOptions }); - console.log(`Other options: ${projectId}, ${title}`); - - console.log({ token }); const response = (await api.Issues.create( projectId, @@ -212,14 +206,14 @@ export const createGitlabIssueAction = (options: { issueOptions, )) as IssueSchema; - console.log('After calling the issues endpoint'); - ctx.output('issueId', response.id); ctx.output('issueUrl', response.web_url); } catch (error: any) { if (error instanceof z.ZodError) { // Handling Zod validation errors - throw new InputError(`Validation error: ${error.message}`); + throw new InputError(`Validation error: ${error.message}`, { + validationErrors: error.errors, + }); } // Handling other errors throw new InputError(`Failed to create GitLab issue: ${error.message}`); diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts index 4de4eca786..61c61e87c3 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.test.ts @@ -19,24 +19,26 @@ import { Gitlab, GroupSchema } from '@gitbeaker/rest'; import { InputError } from '@backstage/errors'; // Mock the Gitlab client and its methods -const setupGitlabMock = () => { - jest.mock('@gitbeaker/rest', () => { - return { - Gitlab: jest.fn().mockImplementation(() => ({ - Groups: { - show: jest.fn(), - }, - Projects: { - show: jest.fn(), - }, - Epics: { - all: jest.fn(), - }, - })), - }; - }); +const mockGitlabClient = { + Groups: { + show: jest.fn(), + }, + Projects: { + show: jest.fn(), + }, + Epics: { + all: jest.fn(), + }, }; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + const mockConfig = { gitlab: [ { @@ -51,10 +53,6 @@ const mockConfig = { ], }; describe('getTopLevelParentGroup', () => { - beforeEach(() => { - setupGitlabMock(); - }); - afterEach(() => jest.resetAllMocks()); // Mocked nested groups @@ -149,7 +147,7 @@ describe('getTopLevelParentGroup', () => { it('should return the top-level parent group if the input group has a parent in the hierarchy', async () => { // Instance with token - const mockGitlabClient = new Gitlab({ + const apiClient = new Gitlab({ host: mockConfig.gitlab[0].host, token: mockConfig.gitlab[0].token!, }); @@ -166,7 +164,7 @@ describe('getTopLevelParentGroup', () => { }, ); - const action = util.getTopLevelParentGroup(mockGitlabClient, 123); + const action = util.getTopLevelParentGroup(apiClient, 123); const result = await action; expect(result).toEqual(mockTopParentGroup); @@ -174,7 +172,7 @@ describe('getTopLevelParentGroup', () => { it('should return the input group if it has no parents in the hierarchy', async () => { // Instance with token - const mockGitlabClient = new Gitlab({ + const apiClient = new Gitlab({ host: mockConfig.gitlab[0].host, token: mockConfig.gitlab[0].token!, }); @@ -191,7 +189,7 @@ describe('getTopLevelParentGroup', () => { }, ); - const action = util.getTopLevelParentGroup(mockGitlabClient, 789); + const action = util.getTopLevelParentGroup(apiClient, 789); const result = await action; expect(result).toEqual(mockTopParentGroup); @@ -201,8 +199,8 @@ describe('getTopLevelParentGroup', () => { describe('checkEpicScope', () => { afterEach(() => jest.resetAllMocks()); - it('should return true if the project and epic are found', async () => { - const mockGitlabClient = new Gitlab({ + it('should return true if the project is inside the epic scope', async () => { + const apiClient = new Gitlab({ host: mockConfig.gitlab[0].host, token: mockConfig.gitlab[0].token!, }); @@ -210,17 +208,25 @@ describe('checkEpicScope', () => { const projectId = 123; const epicId = 456; - // Mock project and top-level parent group - const mockProject = { namespace: { id: 789 } }; - const mockTopParentGroup = { id: 789, name: 'MockGroup' }; + // Mock project, top-level parent group, and epic + const mockProject = { + id: 123, + name: 'You learn', + namespace: { id: 789 }, + path_with_namespace: 'at-once/you-learn', + }; + const mockTopParentGroup = { + id: 789, + name: 'LivingTwice', + full_path: 'at-once/you-learn', + }; + const mockEpic = { id: epicId, group_id: 789 }; mockGitlabClient.Projects.show.mockResolvedValue(mockProject); mockGitlabClient.Groups.show.mockResolvedValue(mockTopParentGroup); - mockGitlabClient.Epics.all.mockResolvedValue([ - { id: epicId, group_id: 789 }, - ]); + mockGitlabClient.Epics.all.mockResolvedValue([mockEpic]); - const result = await checkEpicScope(mockGitlabClient, projectId, epicId); + const result = await util.checkEpicScope(apiClient, projectId, epicId); expect(result).toBe(true); expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); @@ -232,26 +238,116 @@ describe('checkEpicScope', () => { ); }); - it('should throw InputError if the project is not found', async () => { - const mockClient = new Gitlab(); + it('should return false if the project is not inside the epic scope', async () => { + const apiClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + + const projectId = 123; + const epicId = 45; + + // Mock project, top-level parent group, and epic + const mockProject = { + id: 123, + name: 'You learn', + namespace: { id: 32 }, + path_with_namespace: 'at-once/you-learn', + }; + const mockTopParentGroup = { + id: 32, + name: 'TheWalls', + full_path: 'you-built/within', + }; + + const mockEpic = { id: epicId, group_id: 32 }; + + mockGitlabClient.Projects.show.mockResolvedValue(mockProject); + mockGitlabClient.Groups.show.mockResolvedValue(mockTopParentGroup); + mockGitlabClient.Epics.all.mockResolvedValue([mockEpic]); + + const result = await util.checkEpicScope(apiClient, projectId, epicId); + + expect(result).toBe(false); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); + expect(mockGitlabClient.Groups.show).toHaveBeenCalledWith( + mockProject.namespace.id, + ); + expect(mockGitlabClient.Epics.all).toHaveBeenCalledWith( + mockTopParentGroup.id, + ); + }); + + it('should throw an InputError if the project is not found', async () => { + const apiClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + const projectId = 123; const epicId = 456; - // Mocking the absence of the project - mockClient.Projects.show.mockResolvedValue(null); + mockGitlabClient.Projects.show.mockResolvedValue(null); - await expect(checkEpicScope(mockClient, projectId, epicId)).rejects.toThrow( - new InputError( - `Project with id ${projectId} not found. Check your GitLab instance.`, - ), - ); - - expect(mockClient.Projects.show).toHaveBeenCalledWith(projectId); - expect(mockClient.Groups.show).not.toHaveBeenCalled(); - expect(mockClient.Epics.all).not.toHaveBeenCalled(); + await expect( + util.checkEpicScope(apiClient, projectId, epicId), + ).rejects.toThrow(InputError); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); }); - // Add more test cases as needed for different scenarios + it('should throw an InputError if the top-level parent group is not found', async () => { + const apiClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + + const projectId = 123; + const epicId = 456; + + mockGitlabClient.Projects.show.mockResolvedValue({ + id: 123, + name: 'You learn', + namespace: { id: 789 }, + path_with_namespace: 'at-once/you-learn', + }); + mockGitlabClient.Groups.show.mockResolvedValue(null); + + await expect( + util.checkEpicScope(apiClient, projectId, epicId), + ).rejects.toThrow(InputError); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); + expect(mockGitlabClient.Groups.show).toHaveBeenCalledWith(789); + }); + + it('should throw an InputError if the epic is not found', async () => { + const apiClient = new Gitlab({ + host: mockConfig.gitlab[0].host, + token: mockConfig.gitlab[0].token!, + }); + + const projectId = 123; + const epicId = 456; + + mockGitlabClient.Projects.show.mockResolvedValue({ + id: 123, + name: 'You learn', + namespace: { id: 789 }, + path_with_namespace: 'at-once/you-learn', + }); + mockGitlabClient.Groups.show.mockResolvedValue({ + id: 789, + name: 'LivingTwice', + full_path: 'at-once/you-learn', + }); + mockGitlabClient.Epics.all.mockResolvedValue([]); + + await expect( + util.checkEpicScope(apiClient, projectId, epicId), + ).rejects.toThrow(InputError); + expect(mockGitlabClient.Projects.show).toHaveBeenCalledWith(projectId); + expect(mockGitlabClient.Groups.show).toHaveBeenCalledWith(789); + expect(mockGitlabClient.Epics.all).toHaveBeenCalledWith(789); + }); }); describe('convertDate', () => { diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts index 098aa05667..a4d819d490 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/util.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -120,7 +120,6 @@ export function getClient(props: { }; gitlabOptions[tokenType] = requestToken; - console.log({ gitlabOptions }); return new Gitlab(gitlabOptions); } @@ -177,12 +176,10 @@ export async function checkEpicScope( if (!topParentGroup) { throw new InputError(`Couldn't find a suitable top-level parent group.`); } - // Get the epic const epic = (await client.Epics.all(topParentGroup.id)).find( (x: any) => x.id === epicId, ); - if (!epic) { throw new InputError( `Epic with id ${epicId} not found in the top-level parent group ${topParentGroup.name}.`, @@ -191,7 +188,6 @@ export async function checkEpicScope( const epicGroup = await client.Groups.show(epic.group_id as number); const projectNamespace: string = project.path_with_namespace as string; - return projectNamespace.startsWith(epicGroup.full_path as string); } catch (error: any) { throw new InputError(`Could not find epic scope: ${error.message}`);