diff options
author | Belen Pruvost <belen.pruvost@sonarsource.com> | 2021-04-28 15:04:45 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-04-29 20:03:32 +0000 |
commit | 8e115cd61790c03dfcd91183fd6424cab0b03631 (patch) | |
tree | 0340dd807f37a075762b4c98b48b5a05caf59506 /server | |
parent | 12ff4a12bc6613cd17d02c3c4b6f7de469a07ffe (diff) | |
download | sonarqube-8e115cd61790c03dfcd91183fd6424cab0b03631.tar.gz sonarqube-8e115cd61790c03dfcd91183fd6424cab0b03631.zip |
SONAR-14214 - Unassign Security Hotspot
Diffstat (limited to 'server')
12 files changed, 147 insertions, 101 deletions
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx index 902532259ba..19ba3ae0ef8 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { assignSecurityHotspot } from '../../../../api/security-hotspots'; import addGlobalSuccessMessage from '../../../../app/utils/addGlobalSuccessMessage'; import { withCurrentUser } from '../../../../components/hoc/withCurrentUser'; @@ -61,25 +61,25 @@ export class Assignee extends React.PureComponent<Props, State> { this.setState({ editing: false }); }; - handleAssign = (newAssignee?: T.UserActive) => { - if (newAssignee && newAssignee.login) { - this.setState({ loading: true }); - assignSecurityHotspot(this.props.hotspot.key, { - assignee: newAssignee.login + handleAssign = (newAssignee: T.UserActive) => { + this.setState({ loading: true }); + assignSecurityHotspot(this.props.hotspot.key, { + assignee: newAssignee?.login + }) + .then(() => { + if (this.mounted) { + this.setState({ editing: false, loading: false }); + this.props.onAssigneeChange(); + } }) - .then(() => { - if (this.mounted) { - this.setState({ editing: false, loading: false }); - this.props.onAssigneeChange(); - } - }) - .then(() => - addGlobalSuccessMessage( - translateWithParameters('hotspots.assign.success', newAssignee.name) - ) + .then(() => + addGlobalSuccessMessage( + newAssignee.login + ? translateWithParameters('hotspots.assign.success', newAssignee.name) + : translate('hotspots.assign.unassign.success') ) - .catch(() => this.setState({ loading: false })); - } + ) + .catch(() => this.setState({ loading: false })); }; render() { diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeRenderer.tsx index b9b179c0cfd..e0da8d9dcbf 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeRenderer.tsx @@ -33,7 +33,7 @@ export interface AssigneeRendererProps { assignee?: T.UserBase; loggedInUser?: T.LoggedInUser; - onAssign: (user?: T.UserActive) => void; + onAssign: (user: T.UserActive) => void; onEnterEditionMode: () => void; onExitEditionMode: () => void; } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx index bb51746a7c5..efb0ebe2caf 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx @@ -20,6 +20,7 @@ import { debounce } from 'lodash'; import * as React from 'react'; import { KeyCodes } from 'sonar-ui-common/helpers/keycodes'; +import { translate } from 'sonar-ui-common/helpers/l10n'; import { searchUsers } from '../../../../api/users'; import { isUserActive } from '../../../../helpers/users'; import AssigneeSelectionRenderer from './AssigneeSelectionRenderer'; @@ -27,17 +28,18 @@ import AssigneeSelectionRenderer from './AssigneeSelectionRenderer'; interface Props { allowCurrentUserSelection: boolean; loggedInUser: T.LoggedInUser; - onSelect: (user?: T.UserActive) => void; + onSelect: (user: T.UserActive) => void; } interface State { highlighted?: T.UserActive; loading: boolean; - open: boolean; query?: string; suggestedUsers: T.UserActive[]; } +const UNASSIGNED: T.UserActive = { login: '', name: translate('unassigned') }; + export default class AssigneeSelection extends React.PureComponent<Props, State> { mounted = false; @@ -46,8 +48,9 @@ export default class AssigneeSelection extends React.PureComponent<Props, State> this.state = { loading: false, - open: props.allowCurrentUserSelection, - suggestedUsers: props.allowCurrentUserSelection ? [props.loggedInUser] : [] + suggestedUsers: props.allowCurrentUserSelection + ? [props.loggedInUser, UNASSIGNED] + : [UNASSIGNED] }; this.handleSearch = debounce(this.handleSearch, 250); @@ -76,9 +79,8 @@ export default class AssigneeSelection extends React.PureComponent<Props, State> this.setState({ loading: false, - open: allowCurrentUserSelection, query, - suggestedUsers: allowCurrentUserSelection ? [loggedInUser] : [] + suggestedUsers: allowCurrentUserSelection ? [loggedInUser, UNASSIGNED] : [UNASSIGNED] }); }; @@ -90,8 +92,7 @@ export default class AssigneeSelection extends React.PureComponent<Props, State> this.setState({ loading: false, query, - open: true, - suggestedUsers: result.users.filter(isUserActive) as T.UserActive[] + suggestedUsers: (result.users.filter(isUserActive) as T.UserActive[]).concat(UNASSIGNED) }); } }) @@ -158,7 +159,7 @@ export default class AssigneeSelection extends React.PureComponent<Props, State> }; render() { - const { highlighted, loading, open, query, suggestedUsers } = this.state; + const { highlighted, loading, query, suggestedUsers } = this.state; return ( <AssigneeSelectionRenderer @@ -167,7 +168,6 @@ export default class AssigneeSelection extends React.PureComponent<Props, State> onKeyDown={this.handleKeyDown} onSearch={this.handleSearch} onSelect={this.props.onSelect} - open={open} query={query} suggestedUsers={suggestedUsers} /> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx index 6ff3a5a40e5..252c54cfedf 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx @@ -32,14 +32,14 @@ export interface HotspotAssigneeSelectRendererProps { loading: boolean; onKeyDown: (event: React.KeyboardEvent) => void; onSearch: (query: string) => void; - onSelect: (user: T.UserActive) => void; - open: boolean; + onSelect: (user?: T.UserActive) => void; query?: string; suggestedUsers?: T.UserActive[]; } export default function AssigneeSelectionRenderer(props: HotspotAssigneeSelectRendererProps) { - const { highlighted, loading, open, query, suggestedUsers } = props; + const { highlighted, loading, query, suggestedUsers } = props; + return ( <> <div className="display-flex-center"> @@ -50,35 +50,33 @@ export default function AssigneeSelectionRenderer(props: HotspotAssigneeSelectRe placeholder={translate('hotspots.assignee.select_user')} value={query} /> - {loading && <DeferredSpinner className="spacer-left" />} </div> - {!loading && open && ( + {!loading && ( <div className="position-relative"> <DropdownOverlay noPadding={true} placement={PopupPlacement.BottomLeft}> - {suggestedUsers && suggestedUsers.length > 0 ? ( - <ul className="hotspot-assignee-search-results"> - {suggestedUsers.map(suggestion => ( + <ul className="hotspot-assignee-search-results"> + {suggestedUsers && + suggestedUsers.map(suggestion => ( <li className={classNames('padded', { active: highlighted && highlighted.login === suggestion.login })} key={suggestion.login} onClick={() => props.onSelect(suggestion)}> - <Avatar - className="spacer-right" - hash={suggestion.avatar} - name={suggestion.name} - size={16} - /> + {suggestion.login && ( + <Avatar + className="spacer-right" + hash={suggestion.avatar} + name={suggestion.name} + size={16} + /> + )} {suggestion.name} </li> ))} - </ul> - ) : ( - <div className="padded">{translate('no_results')}</div> - )} + </ul> </DropdownOverlay> </div> )} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx index 7441b83cd2a..60d34a60ec0 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx @@ -59,10 +59,12 @@ it('should handle edition event correctly', () => { expect(wrapper.state().editing).toBe(false); }); -it('should handle assign event correctly', async () => { +it.each([ + ['assign to user', mockUser() as T.UserActive], + ['unassign', { login: '', name: 'unassigned' } as T.UserActive] +])('should handle %s event', async (_, user: T.UserActive) => { const hotspot = mockHotspot(); const onAssigneeChange = jest.fn(); - const user = mockUser() as T.UserActive; const wrapper = shallowRender({ hotspot, onAssigneeChange }); @@ -73,7 +75,7 @@ it('should handle assign event correctly', async () => { .onAssign(user); expect(wrapper.state().loading).toBe(true); - expect(assignSecurityHotspot).toHaveBeenCalledWith(hotspot.key, { assignee: user.login }); + expect(assignSecurityHotspot).toHaveBeenCalledWith(hotspot.key, { assignee: user?.login }); await waitAndUpdate(wrapper); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx index 27d15238e1f..e4eaf7a2315 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx @@ -45,7 +45,7 @@ it('should handle keydown', () => { const wrapper = shallowRender({ onSelect }); wrapper.instance().handleKeyDown(mockEvent(KeyCodes.UpArrow) as any); - expect(wrapper.state().highlighted).toBeUndefined(); + expect(wrapper.state().highlighted).toEqual({ login: '', name: 'unassigned' }); wrapper.setState({ suggestedUsers }); @@ -77,11 +77,10 @@ it('should handle search', async () => { const onSelect = jest.fn(); const wrapper = shallowRender({ onSelect }); - expect(wrapper.state().suggestedUsers.length).toBe(0); + expect(wrapper.state().suggestedUsers.length).toBe(1); wrapper.instance().handleSearch('j'); expect(searchUsers).not.toBeCalled(); - expect(wrapper.state().open).toBe(false); wrapper.instance().handleSearch('jo'); expect(wrapper.state().loading).toBe(true); @@ -90,14 +89,13 @@ it('should handle search', async () => { await waitAndUpdate(wrapper); expect(wrapper.state().loading).toBe(false); - expect(wrapper.state().open).toBe(true); - expect(wrapper.state().suggestedUsers).toHaveLength(3); + expect(wrapper.state().suggestedUsers).toHaveLength(4); jest.clearAllMocks(); wrapper.instance().handleSearch(''); expect(searchUsers).not.toBeCalled(); - expect(wrapper.state().suggestedUsers.length).toBe(0); + expect(wrapper.state().suggestedUsers.length).toBe(1); }); it('should allow current user selection', async () => { @@ -110,7 +108,7 @@ it('should allow current user selection', async () => { wrapper.instance().handleSearch('jo'); await waitAndUpdate(wrapper); - expect(wrapper.state().suggestedUsers).toHaveLength(3); + expect(wrapper.state().suggestedUsers).toHaveLength(4); wrapper.instance().handleSearch(''); expect(wrapper.state().suggestedUsers[0]).toBe(loggedInUser); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelectionRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelectionRenderer-test.tsx index b931e176e43..e0694d8ef2b 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelectionRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelectionRenderer-test.tsx @@ -27,13 +27,11 @@ import AssigneeSelectionRenderer, { it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot(); expect(shallowRender({ loading: true })).toMatchSnapshot('loading'); - expect(shallowRender({ open: true })).toMatchSnapshot('open'); const highlightedUser = mockUser({ login: 'highlighted' }) as T.UserActive; expect( shallowRender({ highlighted: highlightedUser, - open: true, suggestedUsers: [mockUser() as T.UserActive, highlightedUser] }) ).toMatchSnapshot('open with results'); @@ -43,7 +41,6 @@ it('should call onSelect when clicked', () => { const user = mockUser() as T.UserActive; const onSelect = jest.fn(); const wrapper = shallowRender({ - open: true, onSelect, suggestedUsers: [user] }); @@ -63,7 +60,6 @@ function shallowRender(props?: Partial<HotspotAssigneeSelectRendererProps>) { onKeyDown={jest.fn()} onSearch={jest.fn()} onSelect={jest.fn()} - open={false} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelection-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelection-test.tsx.snap index a0b29106d14..c814ca7fdce 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelection-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelection-test.tsx.snap @@ -6,7 +6,13 @@ exports[`should render correctly 1`] = ` onKeyDown={[Function]} onSearch={[Function]} onSelect={[MockFunction]} - open={false} - suggestedUsers={Array []} + suggestedUsers={ + Array [ + Object { + "login": "", + "name": "unassigned", + }, + ] + } /> `; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap index d8731bbac9e..ee9b3ab78e3 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap @@ -12,6 +12,18 @@ exports[`should render correctly 1`] = ` placeholder="hotspots.assignee.select_user" /> </div> + <div + className="position-relative" + > + <DropdownOverlay + noPadding={true} + placement="bottom-left" + > + <ul + className="hotspot-assignee-search-results" + /> + </DropdownOverlay> + </div> </Fragment> `; @@ -33,35 +45,6 @@ exports[`should render correctly: loading 1`] = ` </Fragment> `; -exports[`should render correctly: open 1`] = ` -<Fragment> - <div - className="display-flex-center" - > - <SearchBox - autoFocus={true} - onChange={[MockFunction]} - onKeyDown={[MockFunction]} - placeholder="hotspots.assignee.select_user" - /> - </div> - <div - className="position-relative" - > - <DropdownOverlay - noPadding={true} - placement="bottom-left" - > - <div - className="padded" - > - no_results - </div> - </DropdownOverlay> - </div> -</Fragment> -`; - exports[`should render correctly: open with results 1`] = ` <Fragment> <div diff --git a/server/sonar-web/src/main/js/types/security-hotspots.ts b/server/sonar-web/src/main/js/types/security-hotspots.ts index 34dcc490359..ba043f8f344 100644 --- a/server/sonar-web/src/main/js/types/security-hotspots.ts +++ b/server/sonar-web/src/main/js/types/security-hotspots.ts @@ -150,6 +150,6 @@ export interface HotspotSetStatusRequest { } export interface HotspotAssignRequest { - assignee: string; + assignee?: string; comment?: string; } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AssignAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AssignAction.java index 8eaeb88200f..cefdf68ff61 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AssignAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AssignAction.java @@ -20,6 +20,7 @@ package org.sonar.server.hotspot.ws; import javax.annotation.Nullable; +import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; @@ -35,6 +36,7 @@ import org.sonar.db.user.UserDto; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.ws.IssueUpdater; +import static com.google.common.base.Strings.isNullOrEmpty; import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW; import static org.sonar.server.exceptions.NotFoundException.checkFound; import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional; @@ -66,7 +68,9 @@ public class AssignAction implements HotspotsWsAction { .setSince("8.2") .setHandler(this) .setInternal(true) - .setPost(true); + .setPost(true) + .setChangelog( + new Change("8.9", "Parameter 'assignee' is no longer mandatory")); action.createParam(PARAM_HOTSPOT_KEY) .setDescription("Hotspot key") @@ -75,7 +79,6 @@ public class AssignAction implements HotspotsWsAction { action.createParam(PARAM_ASSIGNEE) .setDescription("Login of the assignee with 'Browse' project permission") - .setRequired(true) .setExampleValue("admin"); action.createParam(PARAM_COMMENT) @@ -85,7 +88,7 @@ public class AssignAction implements HotspotsWsAction { @Override public void handle(Request request, Response response) throws Exception { - String assignee = request.mandatoryParam(PARAM_ASSIGNEE); + String assignee = request.param(PARAM_ASSIGNEE); String key = request.mandatoryParam(PARAM_HOTSPOT_KEY); String comment = request.param(PARAM_COMMENT); @@ -101,7 +104,7 @@ public class AssignAction implements HotspotsWsAction { checkIfHotspotToReview(hotspotDto); hotspotWsSupport.loadAndCheckProject(dbSession, hotspotDto, UserRole.USER); - UserDto assignee = getAssignee(dbSession, login); + UserDto assignee = isNullOrEmpty(login) ? null :getAssignee(dbSession, login); IssueChangeContext context = hotspotWsSupport.newIssueChangeContext(); @@ -111,7 +114,9 @@ public class AssignAction implements HotspotsWsAction { issueFieldsSetter.addComment(defaultIssue, comment, context); } - checkAssigneeProjectPermission(dbSession, assignee, hotspotDto.getProjectUuid()); + if (assignee != null) { + checkAssigneeProjectPermission(dbSession, assignee, hotspotDto.getProjectUuid()); + } if (issueFieldsSetter.assign(defaultIssue, assignee, context)) { issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context, false); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AssignActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AssignActionTest.java index 0f749d11e0b..a952e555bb6 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AssignActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AssignActionTest.java @@ -34,6 +34,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.sonar.api.rules.RuleType; +import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; import org.sonar.api.web.UserRole; @@ -58,9 +59,11 @@ import org.sonar.server.ws.WsActionTester; import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -100,11 +103,14 @@ public class AssignActionTest { assertThat(hotspotParam.isRequired()).isTrue(); WebService.Param assigneeParam = wsDefinition.param("assignee"); assertThat(assigneeParam).isNotNull(); - assertThat(assigneeParam.isRequired()).isTrue(); + assertThat(assigneeParam.isRequired()).isFalse(); WebService.Param commentParam = wsDefinition.param("comment"); assertThat(commentParam).isNotNull(); assertThat(commentParam.isRequired()).isFalse(); assertThat(wsDefinition.since()).isEqualTo("8.2"); + assertThat(wsDefinition.changelog()) + .extracting(Change::getVersion, Change::getDescription) + .contains(tuple("8.9", "Parameter 'assignee' is no longer mandatory")); } @Test @@ -125,6 +131,23 @@ public class AssignActionTest { } @Test + public void unassign_hotspot_for_public_project() { + ComponentDto project = dbTester.components().insertPublicProject(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + UserDto assignee = insertUser(randomAlphanumeric(15)); + + IssueDto hotspot = dbTester.issues().insertHotspot(project, file, h -> h.setAssigneeUuid(assignee.getUuid())); + + UserDto userDto = insertUser(randomAlphanumeric(10)); + userSessionRule.logIn(userDto).registerComponents(project); + when(issueFieldsSetter.assign(eq(hotspot.toDefaultIssue()), isNull(), any(IssueChangeContext.class))).thenReturn(true); + + executeRequest(hotspot, null, null); + + verifyFieldSetters(null, null); + } + + @Test public void assign_hotspot_to_me_for_public_project() { ComponentDto project = dbTester.components().insertPublicProject(); ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); @@ -141,6 +164,21 @@ public class AssignActionTest { } @Test + public void unassign_hotspot_myself_for_public_project() { + ComponentDto project = dbTester.components().insertPublicProject(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + UserDto me = insertUser(randomAlphanumeric(10)); + userSessionRule.logIn(me).registerComponents(project); + IssueDto hotspot = dbTester.issues().insertHotspot(project, file, h -> h.setAssigneeUuid(me.getUuid())); + + when(issueFieldsSetter.assign(eq(hotspot.toDefaultIssue()), isNull(), any(IssueChangeContext.class))).thenReturn(true); + + executeRequest(hotspot, null, null); + + verifyFieldSetters(null, null); + } + + @Test public void assign_hotspot_to_someone_for_private_project() { ComponentDto project = dbTester.components().insertPrivateProject(); ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); @@ -157,6 +195,22 @@ public class AssignActionTest { } @Test + public void unassign_hotspot_for_private_project() { + ComponentDto project = dbTester.components().insertPrivateProject(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + UserDto assignee = insertUser(randomAlphanumeric(15)); + IssueDto hotspot = dbTester.issues().insertHotspot(project, file, h -> h.setAssigneeUuid(assignee.getUuid())); + + insertAndLoginAsUserWithProjectUserPermission(randomAlphanumeric(10), project, UserRole.USER); + + when(issueFieldsSetter.assign(eq(hotspot.toDefaultIssue()), isNull(), any(IssueChangeContext.class))).thenReturn(true); + + executeRequest(hotspot, null, null); + + verifyFieldSetters(null, null); + } + + @Test public void assign_hotspot_to_someone_for_private_project_branch() { ComponentDto project = dbTester.components().insertPrivateProject(); ComponentDto branch = dbTester.components().insertProjectBranch(project); @@ -494,8 +548,12 @@ public class AssignActionTest { } private static UserDto userMatcher(UserDto user) { - return argThat(argument -> argument.getLogin().equals(user.getLogin()) && - argument.getUuid().equals(user.getUuid())); + if (user == null) { + return isNull(); + } else { + return argThat(argument -> argument.getLogin().equals(user.getLogin()) && + argument.getUuid().equals(user.getUuid())); + } } } |