From 88bca79492469d71d942a37126e98e8b9f7ad50e Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 28 Jul 2020 16:05:04 +0200 Subject: [PATCH] SONAR-13630 import from gitlab --- .../src/main/js/api/alm-integrations.ts | 11 ++ .../create/project/GitlabProjectCreate.tsx | 28 +++ .../project/GitlabProjectCreateRenderer.tsx | 5 + .../project/GitlabProjectSelectionForm.tsx | 24 ++- .../__tests__/GitlabProjectCreate-test.tsx | 52 +++++- .../GitlabProjectCreateRenderer-test.tsx | 1 + .../GitlabProjectSelectionForm-test.tsx | 40 +++++ .../GitlabProjectCreate-test.tsx.snap | 1 + .../GitlabProjectCreateRenderer-test.tsx.snap | 1 + .../GitlabProjectSelectionForm-test.tsx.snap | 159 +++++++++++++++++- .../resources/org/sonar/l10n/core.properties | 5 +- 11 files changed, 321 insertions(+), 6 deletions(-) diff --git a/server/sonar-web/src/main/js/api/alm-integrations.ts b/server/sonar-web/src/main/js/api/alm-integrations.ts index 51914405af5..e013a3721df 100644 --- a/server/sonar-web/src/main/js/api/alm-integrations.ts +++ b/server/sonar-web/src/main/js/api/alm-integrations.ts @@ -151,3 +151,14 @@ export function getGitlabProjects(data: { .then(({ repositories, paging }) => ({ projects: repositories, projectsPaging: paging })) .catch(throwGlobalError); } + +export function importGitlabProject(data: { + almSetting: string; + gitlabProjectId: string; +}): Promise<{ project: ProjectBase }> { + const { almSetting, gitlabProjectId } = data; + return postJSON('/api/alm_integrations/import_gitlab_project', { + almSetting, + gitlabProjectId + }).catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx index d7f2ad37283..87e35b9dd72 100644 --- a/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/GitlabProjectCreate.tsx @@ -22,6 +22,7 @@ import { WithRouterProps } from 'react-router'; import { checkPersonalAccessTokenIsValid, getGitlabProjects, + importGitlabProject, setAlmPersonalAccessToken } from '../../../api/alm-integrations'; import { GitlabProject } from '../../../types/alm-integration'; @@ -36,6 +37,7 @@ interface Props extends Pick { } interface State { + importingGitlabProjectId?: string; loading: boolean; loadingMore: boolean; projects?: GitlabProject[]; @@ -141,6 +143,29 @@ export default class GitlabProjectCreate extends React.PureComponent undefined); }; + handleImport = async (gitlabProjectId: string) => { + const { settings } = this.state; + + if (!settings) { + return; + } + + this.setState({ importingGitlabProjectId: gitlabProjectId }); + + const result = await importGitlabProject({ + almSetting: settings.key, + gitlabProjectId + }).catch(() => undefined); + + if (this.mounted) { + this.setState({ importingGitlabProjectId: undefined }); + + if (result) { + this.props.onProjectCreate([result.project.key]); + } + } + }; + handleLoadMore = async () => { this.setState({ loadingMore: true }); @@ -216,6 +241,7 @@ export default class GitlabProjectCreate extends React.PureComponent void; onLoadMore: () => void; onPersonalAccessTokenCreate: (pat: string) => void; onSearch: (searchQuery: string) => void; @@ -47,6 +49,7 @@ export interface GitlabProjectCreateRendererProps { export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRendererProps) { const { canAdmin, + importingGitlabProjectId, loading, loadingMore, projects, @@ -92,7 +95,9 @@ export default function GitlabProjectCreateRenderer(props: GitlabProjectCreateRe /> ) : ( void; onLoadMore: () => void; onSearch: (searchQuery: string) => void; projects?: GitlabProject[]; @@ -44,7 +48,14 @@ export interface GitlabProjectSelectionFormProps { } export default function GitlabProjectSelectionForm(props: GitlabProjectSelectionFormProps) { - const { loadingMore, projects = [], projectsPaging, searching, searchQuery } = props; + const { + importingGitlabProjectId, + loadingMore, + projects = [], + projectsPaging, + searching, + searchQuery + } = props; if (projects.length === 0 && searchQuery.length === 0 && !searching) { return ( @@ -131,7 +142,16 @@ export default function GitlabProjectSelectionForm(props: GitlabProjectSelection ) : ( -   + + + )} ))} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx index 4dffe5674f5..b7c84ffdba4 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectCreate-test.tsx @@ -24,6 +24,7 @@ import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; import { checkPersonalAccessTokenIsValid, getGitlabProjects, + importGitlabProject, setAlmPersonalAccessToken } from '../../../../api/alm-integrations'; import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations'; @@ -35,7 +36,8 @@ import GitlabProjectCreate from '../GitlabProjectCreate'; jest.mock('../../../../api/alm-integrations', () => ({ checkPersonalAccessTokenIsValid: jest.fn().mockResolvedValue(true), setAlmPersonalAccessToken: jest.fn().mockResolvedValue(null), - getGitlabProjects: jest.fn().mockRejectedValue('error') + getGitlabProjects: jest.fn().mockRejectedValue('error'), + importGitlabProject: jest.fn().mockRejectedValue('error') })); beforeEach(jest.clearAllMocks); @@ -200,6 +202,54 @@ it('should search for projects', async () => { expect(getGitlabProjects).toBeCalledWith(expect.objectContaining({ query })); }); +it('should import', async () => { + (checkPersonalAccessTokenIsValid as jest.Mock).mockResolvedValueOnce(true); + + const projects = [mockGitlabProject({ id: '1' }), mockGitlabProject({ id: '2' })]; + (getGitlabProjects as jest.Mock).mockResolvedValueOnce({ + projects, + projectsPaging: { + pageIndex: 1, + pageSize: 6, + total: 2 + } + }); + const createdProjectkey = 'imported_project_key'; + + (importGitlabProject as jest.Mock).mockResolvedValueOnce({ + project: { key: createdProjectkey } + }); + + const onProjectCreate = jest.fn(); + + const wrapper = shallowRender({ onProjectCreate }); + await waitAndUpdate(wrapper); + + wrapper.instance().handleImport(projects[1].id); + expect(wrapper.state().importingGitlabProjectId).toBe(projects[1].id); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().importingGitlabProjectId).toBeUndefined(); + expect(onProjectCreate).toBeCalledWith([createdProjectkey]); +}); + +it('should do nothing with missing settings', async () => { + const wrapper = shallowRender({ settings: [] }); + + await waitAndUpdate(wrapper); + + wrapper.instance().handleLoadMore(); + wrapper.instance().handleSearch('whatever'); + wrapper.instance().handlePersonalAccessTokenCreate('token'); + wrapper.instance().handleImport('gitlab project id'); + + expect(checkPersonalAccessTokenIsValid).not.toHaveBeenCalled(); + expect(getGitlabProjects).not.toHaveBeenCalled(); + expect(importGitlabProject).not.toHaveBeenCalled(); + expect(setAlmPersonalAccessToken).not.toHaveBeenCalled(); +}); + function shallowRender(props: Partial = {}) { return shallow( = {}) { canAdmin={false} loading={false} loadingMore={false} + onImport={jest.fn()} onLoadMore={jest.fn()} onPersonalAccessTokenCreate={jest.fn()} onSearch={jest.fn()} diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx index 2840d073908..1727f35275d 100644 --- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitlabProjectSelectionForm-test.tsx @@ -20,6 +20,9 @@ import { shallow } from 'enzyme'; import * as React from 'react'; +import { Button } from 'sonar-ui-common/components/controls/buttons'; +import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; +import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; import { mockGitlabProject } from '../../../../helpers/mocks/alm-integrations'; import GitlabProjectSelectionForm, { GitlabProjectSelectionFormProps @@ -37,6 +40,42 @@ it('should render correctly', () => { expect( shallowRender({ projects: [], projectsPaging: mockPaging(), searchQuery: 'findme' }) ).toMatchSnapshot('no projects when searching'); + + expect(shallowRender({ importingGitlabProjectId: '2' })).toMatchSnapshot('importing'); +}); + +describe('appropriate callback', () => { + const onImport = jest.fn(); + const onLoadMore = jest.fn(); + const onSearch = jest.fn(); + const wrapper = shallowRender({ onImport, onLoadMore, onSearch }); + + it('should be called when clicking to import', () => { + wrapper + .find(Button) + .first() + .simulate('click'); + + expect(onImport).toBeCalled(); + }); + + it('should be assigned to the list footer', () => { + const { loadMore } = wrapper + .find(ListFooter) + .first() + .props(); + + expect(loadMore).toBe(onLoadMore); + }); + + it('should be assigned to the search box', () => { + const { onChange } = wrapper + .find(SearchBox) + .first() + .props(); + + expect(onChange).toBe(onSearch); + }); }); function shallowRender(props: Partial = {}) { @@ -52,6 +91,7 @@ function shallowRender(props: Partial = {}) { return shallow( + +
+ + + + + + + + + + + + + + +
+ + + Awesome Project ! + + +
+ + + Company / Best Projects + + +
+ + + onboarding.create_project.gitlab.link + + + +
+ + + Awesome Project ! + + +
+ + + Company / Best Projects + + +
+ + + onboarding.create_project.gitlab.link + + + + + onboarding.create_project.repository_imported + : + + +
+ + + Already Imported + +
+
+ + +`; + exports[`should render correctly: no projects 1`] = ` -   +