diff options
author | Philippe Perrin <philippe.perrin@sonarsource.com> | 2019-10-24 11:56:14 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-10-30 20:21:08 +0100 |
commit | f8c9b9c8b3e7040907e0ded89511f86b827449ad (patch) | |
tree | 38faadc44bfd8d3b0da82fcd1ea7fe154f224afe | |
parent | 0ed5e825f6694b5f33d4234e9228ed7c6b9a236e (diff) | |
download | sonarqube-f8c9b9c8b3e7040907e0ded89511f86b827449ad.tar.gz sonarqube-f8c9b9c8b3e7040907e0ded89511f86b827449ad.zip |
SONAR-12597 Rework Quality profile Web Api calls so they don't use deprecated parameters anymore
30 files changed, 1168 insertions, 217 deletions
diff --git a/server/sonar-web/src/main/js/api/quality-profiles.ts b/server/sonar-web/src/main/js/api/quality-profiles.ts index e9f79660f51..6b6cefc529b 100644 --- a/server/sonar-web/src/main/js/api/quality-profiles.ts +++ b/server/sonar-web/src/main/js/api/quality-profiles.ts @@ -21,6 +21,7 @@ import { map } from 'lodash'; import { csvEscape } from 'sonar-ui-common/helpers/csv'; import { getJSON, post, postJSON, RequestData } from 'sonar-ui-common/helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; +import { Exporter, ProfileChangelogEvent } from '../apps/quality-profiles/types'; export interface ProfileActions { associateProjects?: boolean; @@ -73,11 +74,14 @@ export function searchQualityProfiles( return getJSON('/api/qualityprofiles/search', parameters).catch(throwGlobalError); } -export function getQualityProfile(data: { +export function getQualityProfile({ + compareToSonarWay, + profile: { key } +}: { compareToSonarWay?: boolean; - profile: string; + profile: Profile; }): Promise<any> { - return getJSON('/api/qualityprofiles/show', data); + return getJSON('/api/qualityprofiles/show', { compareToSonarWay, key }); } export function createQualityProfile(data: RequestData): Promise<any> { @@ -101,18 +105,28 @@ export function getProfileProjects( return getJSON('/api/qualityprofiles/projects', data).catch(throwGlobalError); } -export function getProfileInheritance( - profileKey: string -): Promise<{ +export function getProfileInheritance({ + language, + name: qualityProfile, + organization +}: Profile): Promise<{ ancestors: T.ProfileInheritanceDetails[]; children: T.ProfileInheritanceDetails[]; profile: T.ProfileInheritanceDetails; }> { - return getJSON('/api/qualityprofiles/inheritance', { profileKey }).catch(throwGlobalError); + return getJSON('/api/qualityprofiles/inheritance', { + language, + qualityProfile, + organization + }).catch(throwGlobalError); } -export function setDefaultProfile(profileKey: string) { - return post('/api/qualityprofiles/set_default', { profileKey }); +export function setDefaultProfile({ language, name: qualityProfile, organization }: Profile) { + return post('/api/qualityprofiles/set_default', { + language, + qualityProfile, + organization + }); } export function renameProfile(key: string, name: string) { @@ -123,17 +137,45 @@ export function copyProfile(fromKey: string, toName: string): Promise<any> { return postJSON('/api/qualityprofiles/copy', { fromKey, toName }).catch(throwGlobalError); } -export function deleteProfile(profileKey: string) { - return post('/api/qualityprofiles/delete', { profileKey }).catch(throwGlobalError); +export function deleteProfile({ language, name: qualityProfile, organization }: Profile) { + return post('/api/qualityprofiles/delete', { language, qualityProfile, organization }).catch( + throwGlobalError + ); } -export function changeProfileParent(profileKey: string, parentKey: string) { +export function changeProfileParent( + { language, name: qualityProfile, organization }: Profile, + parentProfile?: Profile +) { return post('/api/qualityprofiles/change_parent', { - profileKey, - parentKey + language, + qualityProfile, + organization, + parentQualityProfile: parentProfile ? parentProfile.name : undefined }).catch(throwGlobalError); } +export function getQualityProfileBackupUrl({ + language, + name: qualityProfile, + organization +}: Profile) { + const queryParams = Object.entries({ language, qualityProfile, organization }) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join('&'); + return `/api/qualityprofiles/backup?${queryParams}`; +} + +export function getQualityProfileExporterUrl( + { key: exporterKey }: Exporter, + { language, name: qualityProfile, organization }: Profile +) { + const queryParams = Object.entries({ exporterKey, language, qualityProfile, organization }) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join('&'); + return `/api/qualityprofiles/export?${queryParams}`; +} + export function getImporters(): Promise< Array<{ key: string; languages: Array<string>; name: string }> > { @@ -144,8 +186,25 @@ export function getExporters(): Promise<any> { return getJSON('/api/qualityprofiles/exporters').then(r => r.exporters); } -export function getProfileChangelog(data: RequestData): Promise<any> { - return getJSON('/api/qualityprofiles/changelog', data); +export function getProfileChangelog( + since: any, + to: any, + { language, name: qualityProfile, organization }: Profile, + page?: number +): Promise<{ + events: ProfileChangelogEvent[]; + p: number; + ps: number; + total: number; +}> { + return getJSON('/api/qualityprofiles/changelog', { + since, + to, + language, + qualityProfile, + organization, + p: page + }); } export interface CompareResponse { @@ -165,12 +224,28 @@ export function compareProfiles(leftKey: string, rightKey: string): Promise<Comp return getJSON('/api/qualityprofiles/compare', { leftKey, rightKey }); } -export function associateProject(key: string, project: string) { - return post('/api/qualityprofiles/add_project', { key, project }).catch(throwGlobalError); +export function associateProject( + { language, name: qualityProfile, organization }: Profile, + project: string +) { + return post('/api/qualityprofiles/add_project', { + language, + qualityProfile, + organization, + project + }).catch(throwGlobalError); } -export function dissociateProject(key: string, project: string) { - return post('/api/qualityprofiles/remove_project', { key, project }).catch(throwGlobalError); +export function dissociateProject( + { language, name: qualityProfile, organization }: Profile, + project: string +) { + return post('/api/qualityprofiles/remove_project', { + language, + qualityProfile, + organization, + project + }).catch(throwGlobalError); } export interface SearchUsersGroupsParameters { diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx index 52b84a72ebf..5800b274ddb 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/App.tsx @@ -88,26 +88,37 @@ export default class QualityProfiles extends React.PureComponent<Props, State> { handleChangeProfile = (oldKey: string, newKey: string) => { const { component } = this.props; const { allProfiles, profiles } = this.state; + const oldProfile = allProfiles && allProfiles.find(profile => profile.key === oldKey); const newProfile = allProfiles && allProfiles.find(profile => profile.key === newKey); - const request = - newProfile && newProfile.isDefault - ? dissociateProject(oldKey, component.key) - : associateProject(newKey, component.key); - - return request.then(() => { - if (this.mounted && profiles && newProfile) { - // remove old profile, add new one - const nextProfiles = [...profiles.filter(profile => profile.key !== oldKey), newProfile]; - this.setState({ profiles: nextProfiles }); - - addGlobalSuccessMessage( - translateWithParameters( - 'project_quality_profile.successfully_updated', - newProfile.languageName - ) - ); + + let request; + + if (newProfile) { + if (newProfile.isDefault && oldProfile) { + request = dissociateProject(oldProfile, component.key); + } else { + request = associateProject(newProfile, component.key); } - }); + } + + if (request) { + return request.then(() => { + if (this.mounted && profiles && newProfile) { + // remove old profile, add new one + const nextProfiles = [...profiles.filter(profile => profile.key !== oldKey), newProfile]; + this.setState({ profiles: nextProfiles }); + + addGlobalSuccessMessage( + translateWithParameters( + 'project_quality_profile.successfully_updated', + newProfile.languageName + ) + ); + } + }); + } else { + return Promise.resolve(); + } }; render() { diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx index faa3780cb55..81b97c3d481 100644 --- a/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectQualityProfiles/__tests__/App-test.tsx @@ -17,11 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -/* eslint-disable import/first */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { + associateProject, + dissociateProject, + searchQualityProfiles +} from '../../../api/quality-profiles'; +import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; +import { mockComponent, mockQualityProfile } from '../../../helpers/testMocks'; +import App from '../App'; +import Table from '../Table'; + +beforeEach(() => jest.clearAllMocks()); + jest.mock('../../../api/quality-profiles', () => ({ - associateProject: jest.fn(() => Promise.resolve()), - dissociateProject: jest.fn(() => Promise.resolve()), - searchQualityProfiles: jest.fn(() => Promise.resolve()) + associateProject: jest.fn().mockResolvedValue({}), + dissociateProject: jest.fn().mockResolvedValue({}), + searchQualityProfiles: jest.fn().mockResolvedValue({}) })); jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ @@ -32,85 +46,47 @@ jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({ default: jest.fn() })); -import { shallow } from 'enzyme'; -import * as React from 'react'; -import App from '../App'; - -const associateProject = require('../../../api/quality-profiles').associateProject as jest.Mock< - any ->; - -const dissociateProject = require('../../../api/quality-profiles').dissociateProject as jest.Mock< - any ->; - -const searchQualityProfiles = require('../../../api/quality-profiles') - .searchQualityProfiles as jest.Mock<any>; - -const addGlobalSuccessMessage = require('../../../app/utils/addGlobalSuccessMessage') - .default as jest.Mock<any>; - -const handleRequiredAuthorization = require('../../../app/utils/handleRequiredAuthorization') - .default as jest.Mock<any>; - -const component = { - analysisDate: '', - breadcrumbs: [], - configuration: { showQualityProfiles: true }, - key: 'foo', - name: 'foo', - organization: 'org', - qualifier: 'TRK', - version: '0.0.1' -}; +const component = mockComponent({ configuration: { showQualityProfiles: true } }); it('checks permissions', () => { - handleRequiredAuthorization.mockClear(); - shallow(<App component={{ ...component, configuration: undefined }} />); + shallowRender({ component: { ...component, configuration: undefined } }); expect(handleRequiredAuthorization).toBeCalled(); }); it('fetches profiles', () => { - searchQualityProfiles.mockClear(); - shallow(<App component={component} />); - expect(searchQualityProfiles.mock.calls).toHaveLength(2); - expect(searchQualityProfiles).toBeCalledWith({ organization: 'org' }); - expect(searchQualityProfiles).toBeCalledWith({ organization: 'org', project: 'foo' }); + shallowRender(); + expect(searchQualityProfiles).toHaveBeenCalledTimes(2); + expect(searchQualityProfiles).toBeCalledWith({ organization: component.organization }); + expect(searchQualityProfiles).toBeCalledWith({ + organization: component.organization, + project: component.key + }); }); it('changes profile', () => { - associateProject.mockClear(); - dissociateProject.mockClear(); - addGlobalSuccessMessage.mockClear(); - const wrapper = shallow(<App component={component} />); + const wrapper = shallowRender(); - const fooJava = randomProfile('foo-java', 'java'); - const fooJs = randomProfile('foo-js', 'js'); - const allProfiles = [ - fooJava, - randomProfile('bar-java', 'java'), - randomProfile('baz-java', 'java', true), - fooJs - ]; + const fooJava = mockQualityProfile({ key: 'foo-java', language: 'java' }); + const fooJs = mockQualityProfile({ key: 'foo-js', language: 'js' }); + const bar = mockQualityProfile({ key: 'bar-java', language: 'java' }); + const baz = mockQualityProfile({ key: 'baz-java', language: 'java', isDefault: true }); + const allProfiles = [fooJava, bar, baz, fooJs]; const profiles = [fooJava, fooJs]; wrapper.setState({ allProfiles, loading: false, profiles }); - wrapper.find('Table').prop<Function>('onChangeProfile')('foo-java', 'bar-java'); - expect(associateProject).toBeCalledWith('bar-java', 'foo'); - - wrapper.find('Table').prop<Function>('onChangeProfile')('foo-java', 'baz-java'); - expect(dissociateProject).toBeCalledWith('foo-java', 'foo'); + wrapper + .find(Table) + .props() + .onChangeProfile(fooJava.key, bar.key); + expect(associateProject).toBeCalledWith(bar, component.key); + + wrapper + .find(Table) + .props() + .onChangeProfile(fooJava.key, baz.key); + expect(dissociateProject).toBeCalledWith(fooJava, component.key); }); -function randomProfile(key: string, language: string, isDefault = false) { - return { - activeRuleCount: 17, - activeDeprecatedRuleCount: 0, - isDefault, - key, - name: key, - language, - languageName: language, - organization: 'org' - }; +function shallowRender(props: Partial<App['props']> = {}) { + return shallow<App>(<App component={component} {...props} />); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx index aa92d3750f9..533796d856f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx @@ -18,17 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { withRouter, WithRouterProps } from 'react-router'; +import { WithRouterProps } from 'react-router'; import { parseDate, toShortNotSoISOString } from 'sonar-ui-common/helpers/dates'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { getProfileChangelog } from '../../../api/quality-profiles'; +import { withRouter } from '../../../components/hoc/withRouter'; import { Profile, ProfileChangelogEvent } from '../types'; import { getProfileChangelogPath } from '../utils'; import Changelog from './Changelog'; import ChangelogEmpty from './ChangelogEmpty'; import ChangelogSearch from './ChangelogSearch'; -interface Props extends WithRouterProps { +interface Props extends Pick<WithRouterProps, 'router' | 'location'> { organization: string | null; profile: Profile; } @@ -40,7 +41,7 @@ interface State { total?: number; } -class ChangelogContainer extends React.PureComponent<Props, State> { +export class ChangelogContainer extends React.PureComponent<Props, State> { mounted = false; state: State = { loading: true }; @@ -59,27 +60,31 @@ class ChangelogContainer extends React.PureComponent<Props, State> { this.mounted = false; } - loadChangelog() { - this.setState({ loading: true }); - const { query } = this.props.location; - const data: any = { profileKey: this.props.profile.key }; - if (query.since) { - data.since = query.since; - } - if (query.to) { - data.to = query.to; + stopLoading() { + if (this.mounted) { + this.setState({ loading: false }); } + } - getProfileChangelog(data).then((r: any) => { - if (this.mounted) { - this.setState({ - events: r.events, - total: r.total, - page: r.p, - loading: false - }); - } - }); + loadChangelog() { + this.setState({ loading: true }); + const { + location: { query }, + profile + } = this.props; + + getProfileChangelog(query.since, query.to, profile) + .then((r: any) => { + if (this.mounted) { + this.setState({ + events: r.events, + total: r.total, + page: r.p, + loading: false + }); + } + }) + .catch(this.stopLoading); } loadMore(event: React.SyntheticEvent<HTMLElement>) { @@ -88,28 +93,23 @@ class ChangelogContainer extends React.PureComponent<Props, State> { if (this.state.page != null) { this.setState({ loading: true }); - const { query } = this.props.location; - const data: any = { - profileKey: this.props.profile.key, - p: this.state.page + 1 - }; - if (query.since) { - data.since = query.since; - } - if (query.to) { - data.to = query.to; - } - - getProfileChangelog(data).then((r: any) => { - if (this.mounted && this.state.events) { - this.setState({ - events: [...this.state.events, ...r.events], - total: r.total, - page: r.p, - loading: false - }); - } - }); + const { + location: { query }, + profile + } = this.props; + + getProfileChangelog(query.since, query.to, profile, this.state.page + 1) + .then((r: any) => { + if (this.mounted && this.state.events) { + this.setState({ + events: [...this.state.events, ...r.events], + total: r.total, + page: r.p, + loading: false + }); + } + }) + .catch(this.stopLoading); } } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogContainer-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogContainer-test.tsx new file mode 100644 index 00000000000..b892b56b4f8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogContainer-test.tsx @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { getProfileChangelog } from '../../../../api/quality-profiles'; +import { mockLocation, mockQualityProfile, mockRouter } from '../../../../helpers/testMocks'; +import { ChangelogContainer } from '../ChangelogContainer'; + +beforeEach(() => jest.clearAllMocks()); + +jest.mock('../../../../api/quality-profiles', () => { + const { mockQualityProfileChangelogEvent } = require.requireActual( + '../../../../helpers/testMocks' + ); + return { + getProfileChangelog: jest.fn().mockResolvedValue({ + events: [ + mockQualityProfileChangelogEvent(), + mockQualityProfileChangelogEvent(), + mockQualityProfileChangelogEvent() + ], + total: 6, + p: 1 + }) + }; +}); + +it('should render correctly without events', async () => { + (getProfileChangelog as jest.Mock).mockResolvedValueOnce({ events: [] }); + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render correctly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + +it('should load more properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().loadMore(mockEvent()); + expect(getProfileChangelog).toHaveBeenLastCalledWith(undefined, undefined, expect.anything(), 2); +}); + +function shallowRender() { + return shallow<ChangelogContainer>( + <ChangelogContainer + location={mockLocation()} + organization="TEST" + profile={mockQualityProfile()} + router={mockRouter()} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/__snapshots__/ChangelogContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/__snapshots__/ChangelogContainer-test.tsx.snap new file mode 100644 index 00000000000..788ec9aa5f6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/__snapshots__/ChangelogContainer-test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="boxed-group boxed-group-inner js-profile-changelog" +> + <header + className="spacer-bottom" + > + <ChangelogSearch + dateRange={ + Object { + "from": undefined, + "to": undefined, + } + } + onDateRangeChange={[Function]} + onReset={[Function]} + /> + </header> + <Changelog + events={ + Array [ + Object { + "action": "ACTIVATED", + "date": "2019-04-23T02:12:32+0100", + "params": Object { + "severity": "MAJOR", + }, + "ruleKey": "rule-key", + "ruleName": "rule-name", + }, + Object { + "action": "ACTIVATED", + "date": "2019-04-23T02:12:32+0100", + "params": Object { + "severity": "MAJOR", + }, + "ruleKey": "rule-key", + "ruleName": "rule-name", + }, + Object { + "action": "ACTIVATED", + "date": "2019-04-23T02:12:32+0100", + "params": Object { + "severity": "MAJOR", + }, + "ruleKey": "rule-key", + "ruleName": "rule-name", + }, + ] + } + organization="TEST" + /> + <footer + className="text-center spacer-top small" + > + <a + href="#" + onClick={[Function]} + > + show_more + </a> + </footer> +</div> +`; + +exports[`should render correctly without events 1`] = ` +<div + className="boxed-group boxed-group-inner js-profile-changelog" +> + <header + className="spacer-bottom" + > + <ChangelogSearch + dateRange={ + Object { + "from": undefined, + "to": undefined, + } + } + onDateRangeChange={[Function]} + onReset={[Function]} + /> + </header> + <ChangelogEmpty /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx index 2aaf88c3f84..72adade778f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx @@ -51,7 +51,7 @@ export default class DeleteProfileForm extends React.PureComponent<Props, State> handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { event.preventDefault(); this.setState({ loading: true }); - deleteProfile(this.props.profile.key).then(this.props.onDelete, () => { + deleteProfile(this.props.profile).then(this.props.onDelete, () => { if (this.mounted) { this.setState({ loading: false }); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx index 97d211b14c5..3fc46664230 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx @@ -77,7 +77,7 @@ export default class ExtendProfileForm extends React.PureComponent<Props, State> try { const { profile: newProfile } = await createQualityProfile(data); - await changeProfileParent(newProfile.key, parentProfile.key); + await changeProfileParent(newProfile, parentProfile); this.props.onExtend(newProfile.name); } finally { if (this.mounted) { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx index 29ba03a6321..f040f57773c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx @@ -23,7 +23,7 @@ import ActionsDropdown, { ActionsDropdownItem } from 'sonar-ui-common/components/controls/ActionsDropdown'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import { setDefaultProfile } from '../../../api/quality-profiles'; +import { getQualityProfileBackupUrl, setDefaultProfile } from '../../../api/quality-profiles'; import { Router, withRouter } from '../../../components/hoc/withRouter'; import { getRulesUrl } from '../../../helpers/urls'; import { Profile } from '../types'; @@ -119,7 +119,7 @@ export class ProfileActions extends React.PureComponent<Props, State> { }; handleSetDefaultClick = () => { - setDefaultProfile(this.props.profile.key).then(this.props.updateProfiles, () => {}); + setDefaultProfile(this.props.profile).then(this.props.updateProfiles, () => {}); }; navigateToNewProfile = (name: string) => { @@ -137,11 +137,7 @@ export class ProfileActions extends React.PureComponent<Props, State> { const { profile } = this.props; const { actions = {} } = profile; - // FIXME use org, name and lang - const backupUrl = - (window as any).baseUrl + - '/api/qualityprofiles/backup?profileKey=' + - encodeURIComponent(profile.key); + const backupUrl = `${(window as any).baseUrl}${getQualityProfileBackupUrl(profile)}`; const activateMoreUrl = getRulesUrl( { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx new file mode 100644 index 00000000000..88fb1084c4c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { deleteProfile } from '../../../../api/quality-profiles'; +import { mockEvent, mockQualityProfile } from '../../../../helpers/testMocks'; +import DeleteProfileForm from '../DeleteProfileForm'; + +beforeEach(() => jest.clearAllMocks()); + +jest.mock('../../../../api/quality-profiles', () => ({ + deleteProfile: jest.fn().mockResolvedValue({}) +})); + +it('should render correctly', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); +}); + +it('should handle form submit correctly', async () => { + const wrapper = shallowRender(); + wrapper.instance().handleFormSubmit(mockEvent()); + await waitAndUpdate(wrapper); + + expect(deleteProfile).toHaveBeenCalled(); +}); + +function shallowRender(props: Partial<DeleteProfileForm['props']> = {}) { + return shallow<DeleteProfileForm>( + <DeleteProfileForm + onClose={jest.fn()} + onDelete={jest.fn()} + profile={mockQualityProfile()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx index 868e03c1b7b..2f2e055cd45 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx @@ -53,7 +53,7 @@ it('should correctly create a new profile and extend the existing one', async () data.append('name', name); data.append('organization', organization); expect(createQualityProfile).toHaveBeenCalledWith(data); - expect(changeProfileParent).toHaveBeenCalledWith('new-profile', profile.key); + expect(changeProfileParent).toHaveBeenCalledWith({ key: 'new-profile' }, profile); }); function shallowRender(props: Partial<ExtendProfileForm['props']> = {}) { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx index 0af06aa6a84..60b6345c821 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx @@ -20,9 +20,17 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { setDefaultProfile } from '../../../../api/quality-profiles'; import { mockQualityProfile, mockRouter } from '../../../../helpers/testMocks'; import { ProfileActions } from '../ProfileActions'; +beforeEach(() => jest.clearAllMocks()); + +jest.mock('../../../../api/quality-profiles', () => ({ + ...jest.requireActual('../../../../api/quality-profiles'), + setDefaultProfile: jest.fn().mockResolvedValue({}) +})); + const PROFILE = mockQualityProfile({ activeRuleCount: 68, activeDeprecatedRuleCount: 0, @@ -105,9 +113,20 @@ it('should extend profile', async () => { expect(wrapper.find('ExtendProfileForm').exists()).toBe(false); }); +it('should delete profile properly', async () => { + const updateProfiles = jest.fn(); + + const wrapper = shallowRender({ updateProfiles }); + wrapper.instance().handleSetDefaultClick(); + await waitAndUpdate(wrapper); + + expect(setDefaultProfile).toHaveBeenCalledWith(PROFILE); + expect(updateProfiles).toHaveBeenCalled(); +}); + function shallowRender(props: Partial<ProfileActions['props']> = {}) { const router = mockRouter(); - return shallow( + return shallow<ProfileActions>( <ProfileActions organization="org" profile={PROFILE} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap new file mode 100644 index 00000000000..41855ab772c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<Modal + contentLabel="quality_profiles.delete_confirm_title" + onRequestClose={[MockFunction]} +> + <form + id="delete-profile-form" + onSubmit={[Function]} + > + <div + className="modal-head" + > + <h2> + quality_profiles.delete_confirm_title + </h2> + </div> + <div + className="modal-body" + > + <div + className="js-modal-messages" + /> + <p> + quality_profiles.are_you_sure_want_delete_profile_x.name.JavaScript + </p> + </div> + <div + className="modal-foot" + > + <SubmitButton + className="button-red" + disabled={false} + id="delete-profile-submit" + > + delete + </SubmitButton> + <ResetButtonLink + id="delete-profile-cancel" + onClick={[MockFunction]} + > + cancel + </ResetButtonLink> + </div> + </form> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap index b7c5aff79f9..d630932363d 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap @@ -22,7 +22,7 @@ exports[`renders with all permissions 1`] = ` </ActionsDropdownItem> <ActionsDropdownItem download="key.xml" - to="/api/qualityprofiles/backup?profileKey=key" + to="/api/qualityprofiles/backup?language=js&qualityProfile=name&organization=org" > <span data-test="quality-profiles__backup" @@ -103,7 +103,7 @@ exports[`renders with no permissions 1`] = ` <ActionsDropdown> <ActionsDropdownItem download="key.xml" - to="/api/qualityprofiles/backup?profileKey=key" + to="/api/qualityprofiles/backup?language=js&qualityProfile=name&organization=org" > <span data-test="quality-profiles__backup" @@ -154,7 +154,7 @@ exports[`renders with permission to edit only 1`] = ` </ActionsDropdownItem> <ActionsDropdownItem download="key.xml" - to="/api/qualityprofiles/backup?profileKey=key" + to="/api/qualityprofiles/backup?language=js&qualityProfile=name&organization=org" > <span data-test="quality-profiles__backup" diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeParentForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeParentForm.tsx index 41aa3af09f8..bb82c400ad3 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeParentForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeParentForm.tsx @@ -60,18 +60,16 @@ export default class ChangeParentForm extends React.PureComponent<Props, State> handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { event.preventDefault(); - const parent = this.state.selected; + const parent = this.props.profiles.find(p => p.key === this.state.selected); - if (parent != null) { - this.setState({ loading: true }); - changeProfileParent(this.props.profile.key, parent) - .then(this.props.onChange) - .catch(() => { - if (this.mounted) { - this.setState({ loading: false }); - } - }); - } + this.setState({ loading: true }); + changeProfileParent(this.props.profile, parent) + .then(this.props.onChange) + .catch(() => { + if (this.mounted) { + this.setState({ loading: false }); + } + }); }; render() { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx index 559a4bb9f6c..de25da3fda0 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx @@ -101,7 +101,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State }); handleSelect = (key: string) => - associateProject(this.props.profile.key, key).then(() => { + associateProject(this.props.profile, key).then(() => { if (this.mounted) { this.setState((state: State) => ({ needToReload: true, @@ -111,7 +111,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State }); handleUnselect = (key: string) => - dissociateProject(this.props.profile.key, key).then(() => { + dissociateProject(this.props.profile, key).then(() => { if (this.mounted) { this.setState((state: State) => ({ needToReload: true, diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx index f0908d704af..4f6481d8a03 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.tsx @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { stringify } from 'querystring'; import * as React from 'react'; import { translate } from 'sonar-ui-common/helpers/l10n'; +import { getQualityProfileExporterUrl } from '../../../api/quality-profiles'; import { Exporter, Profile } from '../types'; interface Props { @@ -30,18 +30,8 @@ interface Props { export default class ProfileExporters extends React.PureComponent<Props> { getExportUrl(exporter: Exporter) { - const { organization, profile } = this.props; - - const path = '/api/qualityprofiles/export'; - const parameters = { - exporterKey: exporter.key, - language: profile.language, - qualityProfile: profile.name - }; - if (organization) { - Object.assign(parameters, { organization }); - } - return (window as any).baseUrl + path + '?' + stringify(parameters); + const { profile } = this.props; + return `${(window as any).baseUrl}${getQualityProfileExporterUrl(exporter, profile)}`; } render() { @@ -62,7 +52,7 @@ export default class ProfileExporters extends React.PureComponent<Props> { className={index > 0 ? 'spacer-top' : undefined} data-key={exporter.key} key={exporter.key}> - <a href={this.getExportUrl(exporter)} target="_blank"> + <a href={this.getExportUrl(exporter)} rel="noopener noreferrer" target="_blank"> {exporter.name} </a> </li> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx index 98c4879c012..77fc90669f2 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.tsx @@ -65,7 +65,7 @@ export default class ProfileInheritance extends React.PureComponent<Props, State } loadData() { - getProfileInheritance(this.props.profile.key).then( + getProfileInheritance(this.props.profile).then( r => { if (this.mounted) { const { ancestors, children } = r; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx index 0e6c50c1482..da067067b1d 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.tsx @@ -84,7 +84,7 @@ export default class ProfileRules extends React.PureComponent<Props, State> { } return getQualityProfile({ compareToSonarWay: true, - profile: this.props.profile.key + profile: this.props.profile }); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeParentForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeParentForm-test.tsx new file mode 100644 index 00000000000..4d2ca1fcfb8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeParentForm-test.tsx @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { mockQualityProfile } from '../../../../helpers/testMocks'; +import ChangeParentForm from '../ChangeParentForm'; + +beforeEach(() => jest.clearAllMocks()); + +jest.mock('../../../../api/quality-profiles', () => ({ + changeProfileParent: jest.fn().mockResolvedValue({}) +})); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it("should handle form' submit correcty", async () => { + const onChange = jest.fn(); + + const wrapper = shallowRender({ onChange }); + wrapper.instance().handleFormSubmit(mockEvent()); + await waitAndUpdate(wrapper); + + expect(onChange).toHaveBeenCalled(); +}); + +function shallowRender(props?: Partial<ChangeParentForm['props']>) { + return shallow<ChangeParentForm>( + <ChangeParentForm + onChange={jest.fn()} + onClose={jest.fn()} + profile={mockQualityProfile()} + profiles={[ + mockQualityProfile(), + mockQualityProfile(), + mockQualityProfile(), + mockQualityProfile() + ]} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeProjectsForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeProjectsForm-test.tsx index b8928ecf7d9..fb10d9571d8 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeProjectsForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeProjectsForm-test.tsx @@ -87,7 +87,7 @@ it('should handle selection properly', async () => { wrapper.instance().handleSelect('toto'); await waitAndUpdate(wrapper); - expect(associateProject).toHaveBeenCalledWith(profile.key, 'toto'); + expect(associateProject).toHaveBeenCalledWith(profile, 'toto'); expect(wrapper.state().needToReload).toBe(true); }); @@ -96,7 +96,7 @@ it('should handle deselection properly', async () => { wrapper.instance().handleUnselect('tata'); await waitAndUpdate(wrapper); - expect(dissociateProject).toHaveBeenCalledWith(profile.key, 'tata'); + expect(dissociateProject).toHaveBeenCalledWith(profile, 'tata'); expect(wrapper.state().needToReload).toBe(true); }); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileExporters-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileExporters-test.tsx new file mode 100644 index 00000000000..d4d415a8a57 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileExporters-test.tsx @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { mockQualityProfile, mockQualityProfileExporter } from '../../../../helpers/testMocks'; +import ProfileExporters from '../ProfileExporters'; + +it('should render correctly', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); +}); + +function shallowRender(props: Partial<ProfileExporters['props']> = {}) { + const profile = mockQualityProfile(); + return shallow<ProfileExporters>( + <ProfileExporters + exporters={[mockQualityProfileExporter({ languages: [profile.language] })]} + organization="test-org" + profile={profile} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileInheritance-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileInheritance-test.tsx new file mode 100644 index 00000000000..f532c6ee1d5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfileInheritance-test.tsx @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { mockQualityProfile, mockQualityProfileInheritance } from '../../../../helpers/testMocks'; +import ProfileInheritance from '../ProfileInheritance'; + +beforeEach(() => jest.clearAllMocks()); + +jest.mock('../../../../api/quality-profiles', () => ({ + getProfileInheritance: jest.fn().mockResolvedValue({ + children: [mockQualityProfileInheritance()], + ancestors: [mockQualityProfileInheritance()] + }) +})); + +it('should render correctly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(wrapper).toMatchSnapshot(); +}); + +it('should render modal correctly', async () => { + const wrapper = shallowRender(); + wrapper.instance().handleChangeParentClick(); + await waitAndUpdate(wrapper); + + expect(wrapper).toMatchSnapshot(); +}); + +it('should handle parent change correctly', async () => { + const updateProfiles = jest.fn().mockResolvedValueOnce({}); + + const wrapper = shallowRender({ updateProfiles }); + wrapper.instance().handleParentChange(); + await waitAndUpdate(wrapper); + + expect(updateProfiles).toHaveBeenCalled(); +}); + +function shallowRender(props: Partial<ProfileInheritance['props']> = {}) { + return shallow<ProfileInheritance>( + <ProfileInheritance + organization={null} + profile={mockQualityProfile()} + profiles={[mockQualityProfile()]} + updateProfiles={jest.fn()} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ChangeParentForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ChangeParentForm-test.tsx.snap new file mode 100644 index 00000000000..81929a3bd56 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ChangeParentForm-test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<Modal + contentLabel="quality_profiles.change_parent" + onRequestClose={[MockFunction]} + size="small" +> + <form + id="change-profile-parent-form" + onSubmit={[Function]} + > + <div + className="modal-head" + > + <h2> + quality_profiles.change_parent + </h2> + </div> + <div + className="modal-body" + > + <div + className="modal-field" + > + <label + htmlFor="change-profile-parent" + > + quality_profiles.parent + + <em + className="mandatory" + > + * + </em> + </label> + <Select + clearable={false} + id="change-profile-parent" + name="parentKey" + onChange={[Function]} + options={ + Array [ + Object { + "label": "none", + "value": "", + }, + Object { + "label": "name", + "value": "key", + }, + Object { + "label": "name", + "value": "key", + }, + Object { + "label": "name", + "value": "key", + }, + Object { + "label": "name", + "value": "key", + }, + ] + } + value="" + /> + </div> + </div> + <div + className="modal-foot" + > + <SubmitButton + disabled={true} + id="change-profile-parent-submit" + > + change_verb + </SubmitButton> + <ResetButtonLink + id="change-profile-parent-cancel" + onClick={[MockFunction]} + > + cancel + </ResetButtonLink> + </div> + </form> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileExporters-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileExporters-test.tsx.snap new file mode 100644 index 00000000000..5f17cbf43d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileExporters-test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="boxed-group quality-profile-exporters" +> + <h2> + quality_profiles.exporters + </h2> + <div + className="boxed-group-inner" + > + <ul> + <li + data-key="exporter-key" + key="exporter-key" + > + <a + href="/api/qualityprofiles/export?exporterKey=exporter-key&language=js&qualityProfile=name&organization=foo" + rel="noopener noreferrer" + target="_blank" + > + exporter-name + </a> + </li> + </ul> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritance-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritance-test.tsx.snap new file mode 100644 index 00000000000..f3e83c49c72 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritance-test.tsx.snap @@ -0,0 +1,153 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="boxed-group quality-profile-inheritance" +> + <header + className="boxed-group-header" + > + <h2> + quality_profiles.profile_inheritance + </h2> + </header> + <div + className="boxed-group-inner" + > + <table + className="data zebra" + > + <tbody> + <ProfileInheritanceBox + depth={0} + key="foo" + language="js" + organization={null} + profile={ + Object { + "activeRuleCount": 4, + "isBuiltIn": false, + "key": "foo", + "name": "Foo", + "overridingRuleCount": 0, + } + } + type="ancestor" + /> + <ProfileInheritanceBox + depth={2} + key="foo" + language="js" + organization={null} + profile={ + Object { + "activeRuleCount": 4, + "isBuiltIn": false, + "key": "foo", + "name": "Foo", + "overridingRuleCount": 0, + } + } + type="child" + /> + </tbody> + </table> + </div> +</div> +`; + +exports[`should render modal correctly 1`] = ` +<div + className="boxed-group quality-profile-inheritance" +> + <header + className="boxed-group-header" + > + <h2> + quality_profiles.profile_inheritance + </h2> + </header> + <div + className="boxed-group-inner" + > + <table + className="data zebra" + > + <tbody> + <ProfileInheritanceBox + depth={0} + key="foo" + language="js" + organization={null} + profile={ + Object { + "activeRuleCount": 4, + "isBuiltIn": false, + "key": "foo", + "name": "Foo", + "overridingRuleCount": 0, + } + } + type="ancestor" + /> + <ProfileInheritanceBox + depth={2} + key="foo" + language="js" + organization={null} + profile={ + Object { + "activeRuleCount": 4, + "isBuiltIn": false, + "key": "foo", + "name": "Foo", + "overridingRuleCount": 0, + } + } + type="child" + /> + </tbody> + </table> + </div> + <ChangeParentForm + onChange={[Function]} + onClose={[Function]} + profile={ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "organization": "foo", + "projectCount": 3, + } + } + profiles={ + Array [ + Object { + "activeDeprecatedRuleCount": 2, + "activeRuleCount": 10, + "childrenCount": 0, + "depth": 1, + "isBuiltIn": false, + "isDefault": false, + "isInherited": false, + "key": "key", + "language": "js", + "languageName": "JavaScript", + "name": "name", + "organization": "foo", + "projectCount": 3, + }, + ] + } + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx index 249064bc116..ae864c25d78 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx @@ -99,9 +99,12 @@ export default class CreateProfileForm extends React.PureComponent<Props, State> try { const { profile } = await createQualityProfile(data); - if (this.state.parent) { - await changeProfileParent(profile.key, this.state.parent); + + const parentProfile = this.props.profiles.find(p => p.key === this.state.parent); + if (parentProfile) { + await changeProfileParent(profile, parentProfile); } + this.props.onCreate(profile); } finally { if (this.mounted) { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/CreateProfileForm-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/CreateProfileForm-test.tsx index d36546a0126..74fe138dae4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/CreateProfileForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/CreateProfileForm-test.tsx @@ -17,27 +17,68 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { changeProfileParent, createQualityProfile } from '../../../../api/quality-profiles'; import { mockQualityProfile } from '../../../../helpers/testMocks'; import CreateProfileForm from '../CreateProfileForm'; +beforeEach(() => jest.clearAllMocks()); + jest.mock('../../../../api/quality-profiles', () => ({ - changeProfileParent: jest.fn(), - createQualityProfile: jest.fn(), - getImporters: jest.fn().mockResolvedValue([]) + changeProfileParent: jest.fn().mockResolvedValue({}), + createQualityProfile: jest.fn().mockResolvedValue({}), + getImporters: jest.fn().mockResolvedValue([ + { + key: 'key_importer', + languages: ['lang1_importer', 'lang2_importer', 'kr'], + name: 'name_importer' + } + ]) })); -it('should render correctly', () => { - expect( - shallow( - <CreateProfileForm - languages={[{ key: 'kr', name: 'Hangeul' }]} - onClose={jest.fn()} - onCreate={jest.fn()} - organization="org" - profiles={[mockQualityProfile()]} - /> - ) - ).toMatchSnapshot(); +it('should render correctly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + +it('should handle form submit correctly', async () => { + const onCreate = jest.fn(); + + const wrapper = shallowRender({ onCreate }); + wrapper.instance().handleParentChange({ value: 'key' }); + wrapper.instance().handleFormSubmit(mockEvent({ currentTarget: undefined })); + await waitAndUpdate(wrapper); + + expect(createQualityProfile).toHaveBeenCalled(); + expect(changeProfileParent).toHaveBeenCalled(); + expect(onCreate).toHaveBeenCalled(); }); + +it('should handle form submit without parent correctly', async () => { + const onCreate = jest.fn(); + + const wrapper = shallowRender({ onCreate }); + wrapper.instance().handleFormSubmit(mockEvent({ currentTarget: undefined })); + await waitAndUpdate(wrapper); + + expect(createQualityProfile).toHaveBeenCalled(); + expect(changeProfileParent).not.toHaveBeenCalled(); + expect(onCreate).toHaveBeenCalled(); +}); + +function shallowRender(props?: Partial<CreateProfileForm['props']>) { + return shallow<CreateProfileForm>( + <CreateProfileForm + languages={[{ key: 'kr', name: 'Hangeul' }]} + onClose={jest.fn()} + onCreate={jest.fn()} + organization="org" + profiles={[mockQualityProfile()]} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/CreateProfileForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/CreateProfileForm-test.tsx.snap index e4c75d36c7e..6efd6aef32a 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/CreateProfileForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/CreateProfileForm-test.tsx.snap @@ -20,13 +20,120 @@ exports[`should render correctly 1`] = ` <div className="modal-body" > - <i - className="spinner" + <div + className="modal-field" + > + <label + htmlFor="create-profile-name" + > + name + <em + className="mandatory" + > + * + </em> + </label> + <input + autoFocus={true} + id="create-profile-name" + maxLength={100} + name="name" + onChange={[Function]} + required={true} + size={50} + type="text" + value="" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-profile-language" + > + language + <em + className="mandatory" + > + * + </em> + </label> + <Select + clearable={false} + id="create-profile-language" + name="language" + onChange={[Function]} + options={ + Array [ + Object { + "label": "Hangeul", + "value": "kr", + }, + ] + } + value="kr" + /> + </div> + <div + className="modal-field" + > + <label + htmlFor="create-profile-parent" + > + quality_profiles.parent + </label> + <Select + clearable={true} + id="create-profile-parent" + name="parentKey" + onChange={[Function]} + options={ + Array [ + Object { + "label": "none", + "value": "", + }, + ] + } + value="" + /> + </div> + <div + className="modal-field spacer-bottom js-importer" + data-key="key_importer" + key="key_importer" + > + <label + htmlFor="create-profile-form-backup-key_importer" + > + name_importer + </label> + <input + id="create-profile-form-backup-key_importer" + name="backup_key_importer" + type="file" + /> + <p + className="note" + > + quality_profiles.optional_configuration_file + </p> + </div> + <input + name="hello-ie11" + type="hidden" + value="" /> </div> <div className="modal-foot" > + <SubmitButton + disabled={false} + id="create-profile-submit" + > + create + </SubmitButton> <ResetButtonLink id="create-profile-cancel" onClick={[MockFunction]} diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index c667b17d2d1..e6b2119daf1 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -22,7 +22,7 @@ import { Location } from 'history'; import { InjectedRouter } from 'react-router'; import { createStore, Store } from 'redux'; import { DocumentationEntry } from '../apps/documentation/utils'; -import { Profile } from '../apps/quality-profiles/types'; +import { Exporter, Profile } from '../apps/quality-profiles/types'; export function mockAlmApplication(overrides: Partial<T.AlmApplication> = {}): T.AlmApplication { return { @@ -583,6 +583,28 @@ export function mockQualityProfileInheritance( }; } +export function mockQualityProfileChangelogEvent(eventOverride?: any) { + return { + action: 'ACTIVATED', + date: '2019-04-23T02:12:32+0100', + params: { + severity: 'MAJOR' + }, + ruleKey: 'rule-key', + ruleName: 'rule-name', + ...eventOverride + }; +} + +export function mockQualityProfileExporter(override?: Partial<Exporter>): Exporter { + return { + key: 'exporter-key', + name: 'exporter-name', + languages: ['first-lang', 'second-lang'], + ...override + }; +} + export function mockQualityGateProjectStatus( overrides: Partial<T.QualityGateProjectStatus> = {} ): T.QualityGateProjectStatus { |