aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorBelen Pruvost <belen.pruvost@sonarsource.com>2021-04-28 15:04:45 +0200
committersonartech <sonartech@sonarsource.com>2021-04-29 20:03:32 +0000
commit8e115cd61790c03dfcd91183fd6424cab0b03631 (patch)
tree0340dd807f37a075762b4c98b48b5a05caf59506 /server
parent12ff4a12bc6613cd17d02c3c4b6f7de469a07ffe (diff)
downloadsonarqube-8e115cd61790c03dfcd91183fd6424cab0b03631.tar.gz
sonarqube-8e115cd61790c03dfcd91183fd6424cab0b03631.zip
SONAR-14214 - Unassign Security Hotspot
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/Assignee.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeRenderer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelection.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/AssigneeSelectionRenderer.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/Assignee-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelection-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/AssigneeSelectionRenderer-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelection-test.tsx.snap10
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/assignee/__tests__/__snapshots__/AssigneeSelectionRenderer-test.tsx.snap41
-rw-r--r--server/sonar-web/src/main/js/types/security-hotspots.ts2
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AssignAction.java15
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AssignActionTest.java64
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()));
+ }
}
}