You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BulkChangeActionTest.java 40KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2020 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.issue.ws;
  21. import java.util.ArrayList;
  22. import java.util.List;
  23. import java.util.stream.Collectors;
  24. import java.util.stream.IntStream;
  25. import javax.annotation.CheckForNull;
  26. import javax.annotation.Nullable;
  27. import org.junit.Before;
  28. import org.junit.Rule;
  29. import org.junit.Test;
  30. import org.junit.rules.ExpectedException;
  31. import org.mockito.ArgumentCaptor;
  32. import org.sonar.api.impl.utils.TestSystem2;
  33. import org.sonar.api.rules.RuleType;
  34. import org.sonar.api.server.ws.WebService;
  35. import org.sonar.api.utils.System2;
  36. import org.sonar.db.DbClient;
  37. import org.sonar.db.DbTester;
  38. import org.sonar.db.component.BranchType;
  39. import org.sonar.db.component.ComponentDto;
  40. import org.sonar.db.issue.IssueChangeDto;
  41. import org.sonar.db.issue.IssueDto;
  42. import org.sonar.db.rule.RuleDefinitionDto;
  43. import org.sonar.db.user.UserDto;
  44. import org.sonar.server.es.EsTester;
  45. import org.sonar.server.exceptions.UnauthorizedException;
  46. import org.sonar.server.issue.Action;
  47. import org.sonar.server.issue.IssueFieldsSetter;
  48. import org.sonar.server.issue.TestIssueChangePostProcessor;
  49. import org.sonar.server.issue.TransitionService;
  50. import org.sonar.server.issue.WebIssueStorage;
  51. import org.sonar.server.issue.index.IssueIndexer;
  52. import org.sonar.server.issue.index.IssueIteratorFactory;
  53. import org.sonar.server.issue.notification.IssuesChangesNotification;
  54. import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder;
  55. import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
  56. import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
  57. import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer;
  58. import org.sonar.server.issue.workflow.FunctionExecutor;
  59. import org.sonar.server.issue.workflow.IssueWorkflow;
  60. import org.sonar.server.notification.NotificationManager;
  61. import org.sonar.server.organization.TestDefaultOrganizationProvider;
  62. import org.sonar.server.rule.DefaultRuleFinder;
  63. import org.sonar.server.tester.UserSessionRule;
  64. import org.sonar.server.ws.TestRequest;
  65. import org.sonar.server.ws.WsActionTester;
  66. import org.sonarqube.ws.Issues.BulkChangeWsResponse;
  67. import static com.google.common.base.Preconditions.checkArgument;
  68. import static com.google.common.collect.Lists.newArrayList;
  69. import static java.util.Arrays.asList;
  70. import static java.util.Collections.singletonList;
  71. import static java.util.Objects.requireNonNull;
  72. import static java.util.Optional.ofNullable;
  73. import static org.assertj.core.api.Assertions.assertThat;
  74. import static org.assertj.core.api.Assertions.tuple;
  75. import static org.mockito.Mockito.mock;
  76. import static org.mockito.Mockito.verify;
  77. import static org.mockito.Mockito.verifyNoInteractions;
  78. import static org.mockito.Mockito.verifyZeroInteractions;
  79. import static org.sonar.api.issue.DefaultTransitions.RESOLVE_AS_REVIEWED;
  80. import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
  81. import static org.sonar.api.issue.Issue.STATUS_CLOSED;
  82. import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
  83. import static org.sonar.api.issue.Issue.STATUS_OPEN;
  84. import static org.sonar.api.rule.Severity.MAJOR;
  85. import static org.sonar.api.rule.Severity.MINOR;
  86. import static org.sonar.api.rules.RuleType.BUG;
  87. import static org.sonar.api.rules.RuleType.CODE_SMELL;
  88. import static org.sonar.api.rules.RuleType.VULNERABILITY;
  89. import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
  90. import static org.sonar.api.web.UserRole.SECURITYHOTSPOT_ADMIN;
  91. import static org.sonar.api.web.UserRole.USER;
  92. import static org.sonar.db.component.ComponentTesting.newFileDto;
  93. import static org.sonar.db.issue.IssueChangeDto.TYPE_COMMENT;
  94. import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.projectBranchOf;
  95. import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.projectOf;
  96. import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.ruleOf;
  97. import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.userOf;
  98. public class BulkChangeActionTest {
  99. private static long NOW = 2_000_000_000_000L;
  100. private System2 system2 = new TestSystem2().setNow(NOW);
  101. @Rule
  102. public ExpectedException expectedException = ExpectedException.none();
  103. @Rule
  104. public DbTester db = DbTester.create(system2);
  105. @Rule
  106. public EsTester es = EsTester.create();
  107. @Rule
  108. public UserSessionRule userSession = UserSessionRule.standalone();
  109. private DbClient dbClient = db.getDbClient();
  110. private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter();
  111. private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
  112. private WebIssueStorage issueStorage = new WebIssueStorage(system2, dbClient,
  113. new DefaultRuleFinder(dbClient, TestDefaultOrganizationProvider.from(db)),
  114. new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)));
  115. private NotificationManager notificationManager = mock(NotificationManager.class);
  116. private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor();
  117. private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer();
  118. private ArgumentCaptor<IssuesChangesNotification> issueChangeNotificationCaptor = ArgumentCaptor.forClass(IssuesChangesNotification.class);
  119. private List<Action> actions = new ArrayList<>();
  120. private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions,
  121. issueChangePostProcessor, issuesChangesSerializer));
  122. @Before
  123. public void setUp() {
  124. issueWorkflow.start();
  125. addActions();
  126. }
  127. @Test
  128. public void set_type() {
  129. UserDto user = db.users().insertUser();
  130. userSession.logIn(user);
  131. ComponentDto project = db.components().insertPrivateProject();
  132. ComponentDto file = db.components().insertComponent(newFileDto(project));
  133. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  134. RuleDefinitionDto rule = db.rules().insertIssueRule();
  135. IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i.setType(BUG)
  136. .setStatus(STATUS_OPEN).setResolution(null));
  137. BulkChangeWsResponse response = call(builder()
  138. .setIssues(singletonList(issue.getKey()))
  139. .setSetType(CODE_SMELL.name())
  140. .build());
  141. checkResponse(response, 1, 1, 0, 0);
  142. IssueDto reloaded = getIssueByKeys(issue.getKey()).get(0);
  143. assertThat(reloaded.getType()).isEqualTo(CODE_SMELL.getDbConstant());
  144. assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
  145. verifyPostProcessorCalled(file);
  146. }
  147. @Test
  148. public void set_severity() {
  149. UserDto user = db.users().insertUser();
  150. userSession.logIn(user);
  151. ComponentDto project = db.components().insertPrivateProject();
  152. ComponentDto file = db.components().insertComponent(newFileDto(project));
  153. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  154. RuleDefinitionDto rule = db.rules().insertIssueRule();
  155. IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i.setSeverity(MAJOR).setType(CODE_SMELL)
  156. .setStatus(STATUS_OPEN).setResolution(null));
  157. BulkChangeWsResponse response = call(builder()
  158. .setIssues(singletonList(issue.getKey()))
  159. .setSetSeverity(MINOR)
  160. .build());
  161. checkResponse(response, 1, 1, 0, 0);
  162. IssueDto reloaded = getIssueByKeys(issue.getKey()).get(0);
  163. assertThat(reloaded.getSeverity()).isEqualTo(MINOR);
  164. assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
  165. verifyPostProcessorCalled(file);
  166. }
  167. @Test
  168. public void add_tags() {
  169. UserDto user = db.users().insertUser();
  170. userSession.logIn(user);
  171. ComponentDto project = db.components().insertPrivateProject();
  172. ComponentDto file = db.components().insertComponent(newFileDto(project));
  173. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  174. RuleDefinitionDto rule = db.rules().insertIssueRule();
  175. IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i.setTags(asList("tag1", "tag2"))
  176. .setStatus(STATUS_OPEN).setResolution(null));
  177. BulkChangeWsResponse response = call(builder()
  178. .setIssues(singletonList(issue.getKey()))
  179. .setAddTags(singletonList("tag3"))
  180. .build());
  181. checkResponse(response, 1, 1, 0, 0);
  182. IssueDto reloaded = getIssueByKeys(issue.getKey()).get(0);
  183. assertThat(reloaded.getTags()).containsOnly("tag1", "tag2", "tag3");
  184. assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
  185. // no need to refresh measures
  186. verifyPostProcessorNotCalled();
  187. }
  188. @Test
  189. public void remove_assignee() {
  190. UserDto user = db.users().insertUser();
  191. userSession.logIn(user);
  192. ComponentDto project = db.components().insertPrivateProject();
  193. ComponentDto file = db.components().insertComponent(newFileDto(project));
  194. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  195. RuleDefinitionDto rule = db.rules().insertIssueRule();
  196. UserDto assignee = db.users().insertUser();
  197. IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i.setAssigneeUuid(assignee.getUuid())
  198. .setStatus(STATUS_OPEN).setResolution(null));
  199. BulkChangeWsResponse response = call(builder()
  200. .setIssues(singletonList(issue.getKey()))
  201. .setAssign("")
  202. .build());
  203. checkResponse(response, 1, 1, 0, 0);
  204. IssueDto reloaded = getIssueByKeys(issue.getKey()).get(0);
  205. assertThat(reloaded.getAssigneeUuid()).isNull();
  206. assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
  207. // no need to refresh measures
  208. verifyPostProcessorNotCalled();
  209. }
  210. @Test
  211. public void bulk_change_with_comment() {
  212. UserDto user = db.users().insertUser();
  213. userSession.logIn(user);
  214. ComponentDto project = db.components().insertPrivateProject();
  215. ComponentDto file = db.components().insertComponent(newFileDto(project));
  216. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  217. RuleDefinitionDto rule = db.rules().insertIssueRule();
  218. IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i.setType(BUG)
  219. .setStatus(STATUS_OPEN).setResolution(null));
  220. BulkChangeWsResponse response = call(builder()
  221. .setIssues(singletonList(issue.getKey()))
  222. .setDoTransition("confirm")
  223. .setComment("type was badly defined")
  224. .build());
  225. checkResponse(response, 1, 1, 0, 0);
  226. IssueChangeDto issueComment = dbClient.issueChangeDao().selectByTypeAndIssueKeys(db.getSession(), singletonList(issue.getKey()), TYPE_COMMENT).get(0);
  227. assertThat(issueComment.getUserUuid()).isEqualTo(user.getUuid());
  228. assertThat(issueComment.getChangeData()).isEqualTo("type was badly defined");
  229. verifyPostProcessorCalled(file);
  230. }
  231. @Test
  232. public void bulk_change_many_issues() {
  233. UserDto user = db.users().insertUser();
  234. userSession.logIn(user);
  235. ComponentDto project = db.components().insertPrivateProject();
  236. ComponentDto file = db.components().insertComponent(newFileDto(project));
  237. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  238. RuleDefinitionDto rule = db.rules().insertIssueRule();
  239. UserDto oldAssignee = db.users().insertUser();
  240. UserDto userToAssign = db.users().insertUser();
  241. db.organizations().addMember(db.getDefaultOrganization(), userToAssign);
  242. IssueDto issue1 = db.issues().insertIssue(rule, project, file,
  243. i -> i.setAssigneeUuid(oldAssignee.getUuid()).setType(BUG).setSeverity(MINOR).setStatus(STATUS_OPEN).setResolution(null));
  244. IssueDto issue2 = db.issues().insertIssue(rule, project, file,
  245. i -> i.setAssigneeUuid(userToAssign.getUuid()).setType(CODE_SMELL).setSeverity(MAJOR).setStatus(STATUS_OPEN).setResolution(null));
  246. IssueDto issue3 = db.issues().insertIssue(rule, project, file,
  247. i -> i.setAssigneeUuid(null).setType(VULNERABILITY).setSeverity(MAJOR).setStatus(STATUS_OPEN).setResolution(null));
  248. BulkChangeWsResponse response = call(builder()
  249. .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey()))
  250. .setAssign(userToAssign.getLogin())
  251. .setSetSeverity(MINOR)
  252. .setSetType(VULNERABILITY.name())
  253. .build());
  254. checkResponse(response, 3, 3, 0, 0);
  255. assertThat(getIssueByKeys(issue1.getKey(), issue2.getKey(), issue3.getKey()))
  256. .extracting(IssueDto::getKey, IssueDto::getAssigneeUuid, IssueDto::getType, IssueDto::getSeverity, IssueDto::getUpdatedAt)
  257. .containsOnly(
  258. tuple(issue1.getKey(), userToAssign.getUuid(), VULNERABILITY.getDbConstant(), MINOR, NOW),
  259. tuple(issue2.getKey(), userToAssign.getUuid(), VULNERABILITY.getDbConstant(), MINOR, NOW),
  260. tuple(issue3.getKey(), userToAssign.getUuid(), VULNERABILITY.getDbConstant(), MINOR, NOW));
  261. verifyPostProcessorCalled(file);
  262. }
  263. @Test
  264. public void send_notification() {
  265. UserDto user = db.users().insertUser();
  266. userSession.logIn(user);
  267. ComponentDto project = db.components().insertMainBranch();
  268. ComponentDto file = db.components().insertComponent(newFileDto(project));
  269. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  270. RuleDefinitionDto rule = db.rules().insertIssueRule();
  271. IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i.setType(BUG)
  272. .setStatus(STATUS_OPEN).setResolution(null));
  273. BulkChangeWsResponse response = call(builder()
  274. .setIssues(singletonList(issue.getKey()))
  275. .setDoTransition("confirm")
  276. .setSendNotifications(true)
  277. .build());
  278. checkResponse(response, 1, 1, 0, 0);
  279. verify(notificationManager).scheduleForSending(issueChangeNotificationCaptor.capture());
  280. IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotificationCaptor.getValue());
  281. assertThat(builder.getIssues()).hasSize(1);
  282. ChangedIssue changedIssue = builder.getIssues().iterator().next();
  283. assertThat(changedIssue.getKey()).isEqualTo(issue.getKey());
  284. assertThat(changedIssue.getProject().getUuid()).isEqualTo(project.uuid());
  285. assertThat(changedIssue.getProject().getKey()).isEqualTo(project.getKey());
  286. assertThat(changedIssue.getProject().getProjectName()).isEqualTo(project.name());
  287. assertThat(changedIssue.getProject().getBranchName()).isEmpty();
  288. assertThat(changedIssue.getRule().getKey()).isEqualTo(rule.getKey());
  289. assertThat(changedIssue.getRule().getName()).isEqualTo(rule.getName());
  290. assertThat(builder.getChange().getDate()).isEqualTo(NOW);
  291. assertThat(builder.getChange()).isInstanceOf(UserChange.class);
  292. UserChange userChange = (UserChange) builder.getChange();
  293. assertThat(userChange.getUser().getUuid()).isEqualTo(user.getUuid());
  294. assertThat(userChange.getUser().getLogin()).isEqualTo(user.getLogin());
  295. assertThat(userChange.getUser().getName()).contains(user.getName());
  296. }
  297. @Test
  298. public void should_ignore_hotspots() {
  299. UserDto user = db.users().insertUser();
  300. userSession.logIn(user);
  301. ComponentDto project = db.components().insertMainBranch();
  302. ComponentDto file = db.components().insertComponent(newFileDto(project));
  303. addUserProjectPermissions(user, project, USER, SECURITYHOTSPOT_ADMIN);
  304. IssueDto issue = db.issues().insertHotspot(project, file);
  305. BulkChangeWsResponse response = call(builder()
  306. .setIssues(singletonList(issue.getKey()))
  307. .setDoTransition(RESOLVE_AS_REVIEWED)
  308. .setSendNotifications(true)
  309. .build());
  310. checkResponse(response, 0, 0, 0, 0);
  311. verifyNoInteractions(notificationManager);
  312. }
  313. @Test
  314. public void send_notification_on_branch() {
  315. UserDto user = db.users().insertUser();
  316. userSession.logIn(user);
  317. ComponentDto project = db.components().insertMainBranch();
  318. ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("feature").setBranchType(BranchType.BRANCH));
  319. ComponentDto fileOnBranch = db.components().insertComponent(newFileDto(branch));
  320. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  321. RuleDefinitionDto rule = db.rules().insertIssueRule();
  322. IssueDto issue = db.issues().insertIssue(rule, branch, fileOnBranch, i -> i.setType(BUG)
  323. .setStatus(STATUS_OPEN).setResolution(null));
  324. BulkChangeWsResponse response = call(builder()
  325. .setIssues(singletonList(issue.getKey()))
  326. .setDoTransition("confirm")
  327. .setSendNotifications(true)
  328. .build());
  329. checkResponse(response, 1, 1, 0, 0);
  330. verify(notificationManager).scheduleForSending(issueChangeNotificationCaptor.capture());
  331. IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotificationCaptor.getValue());
  332. assertThat(builder.getIssues()).hasSize(1);
  333. ChangedIssue changedIssue = builder.getIssues().iterator().next();
  334. assertThat(changedIssue.getKey()).isEqualTo(issue.getKey());
  335. assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_CONFIRMED);
  336. assertThat(changedIssue.getNewResolution()).isEmpty();
  337. assertThat(changedIssue.getAssignee()).isEmpty();
  338. assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule));
  339. assertThat(changedIssue.getProject()).isEqualTo(projectBranchOf(db, branch));
  340. assertThat(builder.getChange()).isEqualTo(new UserChange(NOW, userOf(user)));
  341. verifyPostProcessorCalled(fileOnBranch);
  342. }
  343. @Test
  344. public void send_no_notification_on_PR() {
  345. verifySendNoNotification(BranchType.PULL_REQUEST);
  346. }
  347. private void verifySendNoNotification(BranchType branchType) {
  348. UserDto user = db.users().insertUser();
  349. userSession.logIn(user);
  350. ComponentDto project = db.components().insertMainBranch();
  351. ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("feature").setBranchType(branchType));
  352. ComponentDto fileOnBranch = db.components().insertComponent(newFileDto(branch));
  353. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  354. RuleDefinitionDto rule = db.rules().insertIssueRule();
  355. IssueDto issue = db.issues().insertIssue(rule, branch, fileOnBranch, i -> i.setType(BUG)
  356. .setStatus(STATUS_OPEN).setResolution(null));
  357. BulkChangeWsResponse response = call(builder()
  358. .setIssues(singletonList(issue.getKey()))
  359. .setDoTransition("confirm")
  360. .setSendNotifications(true)
  361. .build());
  362. checkResponse(response, 1, 1, 0, 0);
  363. verifyZeroInteractions(notificationManager);
  364. verifyPostProcessorCalled(fileOnBranch);
  365. }
  366. @Test
  367. public void send_notification_only_on_changed_issues() {
  368. UserDto user = db.users().insertUser();
  369. userSession.logIn(user);
  370. ComponentDto project = db.components().insertMainBranch();
  371. ComponentDto file = db.components().insertComponent(newFileDto(project));
  372. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  373. RuleDefinitionDto rule = db.rules().insertIssueRule();
  374. IssueDto issue1 = db.issues().insertIssue(rule, project, file, i -> i.setType(BUG)
  375. .setStatus(STATUS_OPEN).setResolution(null));
  376. IssueDto issue2 = db.issues().insertIssue(rule, project, file, i -> i.setType(BUG)
  377. .setStatus(STATUS_OPEN).setResolution(null));
  378. IssueDto issue3 = db.issues().insertIssue(rule, project, file, i -> i.setType(VULNERABILITY)
  379. .setStatus(STATUS_OPEN).setResolution(null));
  380. BulkChangeWsResponse response = call(builder()
  381. .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey()))
  382. .setSetType(RuleType.BUG.name())
  383. .setSendNotifications(true)
  384. .build());
  385. checkResponse(response, 3, 1, 2, 0);
  386. verify(notificationManager).scheduleForSending(issueChangeNotificationCaptor.capture());
  387. assertThat(issueChangeNotificationCaptor.getAllValues()).hasSize(1);
  388. IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotificationCaptor.getValue());
  389. assertThat(builder.getIssues()).hasSize(1);
  390. ChangedIssue changedIssue = builder.getIssues().iterator().next();
  391. assertThat(changedIssue.getKey()).isEqualTo(issue3.getKey());
  392. assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_OPEN);
  393. assertThat(changedIssue.getNewResolution()).isEmpty();
  394. assertThat(changedIssue.getAssignee()).isEmpty();
  395. assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule));
  396. assertThat(changedIssue.getProject()).isEqualTo(projectOf(project));
  397. assertThat(builder.getChange()).isEqualTo(new UserChange(NOW, userOf(user)));
  398. }
  399. @Test
  400. public void ignore_the_issues_that_do_not_match_conditions() {
  401. UserDto user = db.users().insertUser();
  402. userSession.logIn(user);
  403. ComponentDto project = db.components().insertMainBranch();
  404. ComponentDto file1 = db.components().insertComponent(newFileDto(project));
  405. ComponentDto file2 = db.components().insertComponent(newFileDto(project));
  406. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  407. RuleDefinitionDto rule = db.rules().insertIssueRule();
  408. IssueDto issue1 = db.issues().insertIssue(rule, project, file1, i -> i.setType(BUG)
  409. .setStatus(STATUS_OPEN).setResolution(null));
  410. // These 2 issues will be ignored as they are resolved, changing type is not possible
  411. IssueDto issue2 = db.issues().insertIssue(rule, project, file1, i -> i.setType(BUG)
  412. .setStatus(STATUS_CLOSED).setResolution(RESOLUTION_FIXED));
  413. IssueDto issue3 = db.issues().insertIssue(rule, project, file2, i -> i.setType(BUG)
  414. .setStatus(STATUS_CLOSED).setResolution(RESOLUTION_FIXED));
  415. BulkChangeWsResponse response = call(builder()
  416. .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey()))
  417. .setSetType(VULNERABILITY.name())
  418. .build());
  419. checkResponse(response, 3, 1, 2, 0);
  420. assertThat(getIssueByKeys(issue1.getKey(), issue2.getKey(), issue3.getKey()))
  421. .extracting(IssueDto::getKey, IssueDto::getType, IssueDto::getUpdatedAt)
  422. .containsOnly(
  423. tuple(issue1.getKey(), VULNERABILITY.getDbConstant(), NOW),
  424. tuple(issue2.getKey(), BUG.getDbConstant(), issue2.getUpdatedAt()),
  425. tuple(issue3.getKey(), BUG.getDbConstant(), issue3.getUpdatedAt()));
  426. // file2 is not refreshed
  427. verifyPostProcessorCalled(file1);
  428. }
  429. @Test
  430. public void ignore_issues_when_there_is_nothing_to_do() {
  431. UserDto user = db.users().insertUser();
  432. userSession.logIn(user);
  433. ComponentDto project = db.components().insertMainBranch();
  434. ComponentDto file1 = db.components().insertComponent(newFileDto(project));
  435. ComponentDto file2 = db.components().insertComponent(newFileDto(project));
  436. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  437. RuleDefinitionDto rule = db.rules().insertIssueRule();
  438. IssueDto issue1 = db.issues().insertIssue(rule, project, file1, i -> i.setType(BUG).setSeverity(MINOR)
  439. .setStatus(STATUS_OPEN).setResolution(null));
  440. // These 2 issues will be ignored as there's nothing to do
  441. IssueDto issue2 = db.issues().insertIssue(rule, project, file1, i -> i.setType(VULNERABILITY)
  442. .setStatus(STATUS_OPEN).setResolution(null));
  443. IssueDto issue3 = db.issues().insertIssue(rule, project, file2, i -> i.setType(VULNERABILITY)
  444. .setStatus(STATUS_OPEN).setResolution(null));
  445. BulkChangeWsResponse response = call(builder()
  446. .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey()))
  447. .setSetType(VULNERABILITY.name())
  448. .build());
  449. checkResponse(response, 3, 1, 2, 0);
  450. assertThat(getIssueByKeys(issue1.getKey(), issue2.getKey(), issue3.getKey()))
  451. .extracting(IssueDto::getKey, IssueDto::getType, IssueDto::getUpdatedAt)
  452. .containsOnly(
  453. tuple(issue1.getKey(), VULNERABILITY.getDbConstant(), NOW),
  454. tuple(issue2.getKey(), VULNERABILITY.getDbConstant(), issue2.getUpdatedAt()),
  455. tuple(issue3.getKey(), VULNERABILITY.getDbConstant(), issue3.getUpdatedAt()));
  456. // file2 is not refreshed
  457. verifyPostProcessorCalled(file1);
  458. }
  459. @Test
  460. public void add_comment_only_on_changed_issues() {
  461. UserDto user = db.users().insertUser();
  462. userSession.logIn(user);
  463. ComponentDto project = db.components().insertMainBranch();
  464. ComponentDto file1 = db.components().insertComponent(newFileDto(project));
  465. ComponentDto file2 = db.components().insertComponent(newFileDto(project));
  466. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  467. RuleDefinitionDto rule = db.rules().insertIssueRule();
  468. IssueDto issue1 = db.issues().insertIssue(rule, project, file1, i -> i.setType(BUG).setSeverity(MINOR)
  469. .setStatus(STATUS_OPEN).setResolution(null));
  470. // These 2 issues will be ignored as there's nothing to do
  471. IssueDto issue2 = db.issues().insertIssue(rule, project, file1, i -> i.setType(VULNERABILITY)
  472. .setStatus(STATUS_OPEN).setResolution(null));
  473. IssueDto issue3 = db.issues().insertIssue(rule, project, file2, i -> i.setType(VULNERABILITY)
  474. .setStatus(STATUS_OPEN).setResolution(null));
  475. BulkChangeWsResponse response = call(builder()
  476. .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey()))
  477. .setSetType(VULNERABILITY.name())
  478. .setComment("test")
  479. .build());
  480. checkResponse(response, 3, 1, 2, 0);
  481. assertThat(dbClient.issueChangeDao().selectByTypeAndIssueKeys(db.getSession(), singletonList(issue1.getKey()), TYPE_COMMENT)).hasSize(1);
  482. assertThat(dbClient.issueChangeDao().selectByTypeAndIssueKeys(db.getSession(), singletonList(issue2.getKey()), TYPE_COMMENT)).isEmpty();
  483. assertThat(dbClient.issueChangeDao().selectByTypeAndIssueKeys(db.getSession(), singletonList(issue3.getKey()), TYPE_COMMENT)).isEmpty();
  484. verifyPostProcessorCalled(file1);
  485. }
  486. @Test
  487. public void ignore_external_issue() {
  488. UserDto user = db.users().insertUser();
  489. userSession.logIn(user);
  490. ComponentDto project = db.components().insertPrivateProject();
  491. addUserProjectPermissions(user, project, USER, ISSUE_ADMIN);
  492. RuleDefinitionDto rule = db.rules().insertIssueRule();
  493. IssueDto issue = db.issues().insertIssue(rule, project, project, i -> i.setStatus(STATUS_OPEN).setResolution(null).setType(CODE_SMELL));
  494. RuleDefinitionDto externalRule = db.rules().insertIssueRule(r -> r.setIsExternal(true));
  495. IssueDto externalIssue = db.issues().insertIssue(externalRule, project, project, i -> i.setStatus(STATUS_OPEN).setResolution(null).setType(CODE_SMELL));
  496. BulkChangeWsResponse response = call(builder()
  497. .setIssues(asList(issue.getKey(), externalIssue.getKey()))
  498. .setDoTransition("confirm")
  499. .build());
  500. checkResponse(response, 2, 1, 1, 0);
  501. }
  502. @Test
  503. public void issues_on_which_user_has_not_browse_permission_are_ignored() {
  504. UserDto user = db.users().insertUser();
  505. userSession.logIn(user);
  506. ComponentDto project1 = db.components().insertPrivateProject();
  507. addUserProjectPermissions(user, project1, USER, ISSUE_ADMIN);
  508. ComponentDto project2 = db.components().insertPrivateProject();
  509. RuleDefinitionDto rule = db.rules().insertIssueRule();
  510. IssueDto authorizedIssue = db.issues().insertIssue(rule, project1, project1, i -> i.setType(BUG)
  511. .setStatus(STATUS_OPEN).setResolution(null));
  512. // User has not browse permission on these 2 issues
  513. IssueDto notAuthorizedIssue1 = db.issues().insertIssue(rule, project2, project2, i -> i.setType(BUG)
  514. .setStatus(STATUS_OPEN).setResolution(null));
  515. IssueDto notAuthorizedIssue2 = db.issues().insertIssue(rule, project2, project2, i -> i.setType(BUG)
  516. .setStatus(STATUS_OPEN).setResolution(null));
  517. BulkChangeWsResponse response = call(builder()
  518. .setIssues(asList(authorizedIssue.getKey(), notAuthorizedIssue1.getKey(), notAuthorizedIssue2.getKey()))
  519. .setSetType(VULNERABILITY.name())
  520. .build());
  521. checkResponse(response, 1, 1, 0, 0);
  522. assertThat(getIssueByKeys(authorizedIssue.getKey(), notAuthorizedIssue1.getKey(), notAuthorizedIssue2.getKey()))
  523. .extracting(IssueDto::getKey, IssueDto::getType, IssueDto::getUpdatedAt)
  524. .containsOnly(
  525. tuple(authorizedIssue.getKey(), VULNERABILITY.getDbConstant(), NOW),
  526. tuple(notAuthorizedIssue1.getKey(), BUG.getDbConstant(), notAuthorizedIssue1.getUpdatedAt()),
  527. tuple(notAuthorizedIssue2.getKey(), BUG.getDbConstant(), notAuthorizedIssue2.getUpdatedAt()));
  528. verifyPostProcessorCalled(project1);
  529. }
  530. @Test
  531. public void does_not_update_type_when_no_issue_admin_permission() {
  532. UserDto user = db.users().insertUser();
  533. userSession.logIn(user);
  534. ComponentDto project1 = db.components().insertPrivateProject();
  535. addUserProjectPermissions(user, project1, USER, ISSUE_ADMIN);
  536. ComponentDto project2 = db.components().insertPrivateProject();
  537. addUserProjectPermissions(user, project2, USER);
  538. RuleDefinitionDto rule = db.rules().insertIssueRule();
  539. IssueDto authorizedIssue1 = db.issues().insertIssue(rule, project1, project1, i -> i.setType(BUG)
  540. .setStatus(STATUS_OPEN).setResolution(null));
  541. // User has not issue admin permission on these 2 issues
  542. IssueDto notAuthorizedIssue1 = db.issues().insertIssue(rule, project2, project2, i -> i.setType(BUG)
  543. .setStatus(STATUS_OPEN).setResolution(null));
  544. IssueDto notAuthorizedIssue2 = db.issues().insertIssue(rule, project2, project2, i -> i.setType(BUG)
  545. .setStatus(STATUS_OPEN).setResolution(null));
  546. BulkChangeWsResponse response = call(builder()
  547. .setIssues(asList(authorizedIssue1.getKey(), notAuthorizedIssue1.getKey(), notAuthorizedIssue2.getKey()))
  548. .setSetType(VULNERABILITY.name())
  549. .build());
  550. checkResponse(response, 3, 1, 2, 0);
  551. assertThat(getIssueByKeys(authorizedIssue1.getKey(), notAuthorizedIssue1.getKey(), notAuthorizedIssue2.getKey()))
  552. .extracting(IssueDto::getKey, IssueDto::getType, IssueDto::getUpdatedAt)
  553. .containsOnly(
  554. tuple(authorizedIssue1.getKey(), VULNERABILITY.getDbConstant(), NOW),
  555. tuple(notAuthorizedIssue1.getKey(), BUG.getDbConstant(), notAuthorizedIssue1.getUpdatedAt()),
  556. tuple(notAuthorizedIssue2.getKey(), BUG.getDbConstant(), notAuthorizedIssue2.getUpdatedAt()));
  557. verifyPostProcessorCalled(project1);
  558. }
  559. @Test
  560. public void does_not_update_severity_when_no_issue_admin_permission() {
  561. UserDto user = db.users().insertUser();
  562. userSession.logIn(user);
  563. ComponentDto project1 = db.components().insertPrivateProject();
  564. addUserProjectPermissions(user, project1, USER, ISSUE_ADMIN);
  565. ComponentDto project2 = db.components().insertPrivateProject();
  566. addUserProjectPermissions(user, project2, USER);
  567. RuleDefinitionDto rule = db.rules().insertIssueRule();
  568. IssueDto authorizedIssue1 = db.issues().insertIssue(rule, project1, project1, i -> i.setSeverity(MAJOR)
  569. .setStatus(STATUS_OPEN).setResolution(null).setType(CODE_SMELL));
  570. // User has not issue admin permission on these 2 issues
  571. IssueDto notAuthorizedIssue1 = db.issues().insertIssue(rule, project2, project2, i -> i.setSeverity(MAJOR)
  572. .setStatus(STATUS_OPEN).setResolution(null).setType(BUG));
  573. IssueDto notAuthorizedIssue2 = db.issues().insertIssue(rule, project2, project2, i -> i.setSeverity(MAJOR)
  574. .setStatus(STATUS_OPEN).setResolution(null).setType(VULNERABILITY));
  575. BulkChangeWsResponse response = call(builder()
  576. .setIssues(asList(authorizedIssue1.getKey(), notAuthorizedIssue1.getKey(), notAuthorizedIssue2.getKey()))
  577. .setSetSeverity(MINOR)
  578. .build());
  579. checkResponse(response, 3, 1, 2, 0);
  580. assertThat(getIssueByKeys(authorizedIssue1.getKey(), notAuthorizedIssue1.getKey(), notAuthorizedIssue2.getKey()))
  581. .extracting(IssueDto::getKey, IssueDto::getSeverity, IssueDto::getUpdatedAt)
  582. .containsOnly(
  583. tuple(authorizedIssue1.getKey(), MINOR, NOW),
  584. tuple(notAuthorizedIssue1.getKey(), MAJOR, notAuthorizedIssue1.getUpdatedAt()),
  585. tuple(notAuthorizedIssue2.getKey(), MAJOR, notAuthorizedIssue2.getUpdatedAt()));
  586. verifyPostProcessorCalled(project1);
  587. }
  588. @Test
  589. public void fail_when_only_comment_action() {
  590. UserDto user = db.users().insertUser();
  591. userSession.logIn(user);
  592. ComponentDto project = db.components().insertPrivateProject();
  593. addUserProjectPermissions(user, project, USER);
  594. RuleDefinitionDto rule = db.rules().insertIssueRule();
  595. IssueDto issue = db.issues().insertIssue(rule, project, project, i -> i.setType(BUG)
  596. .setStatus(STATUS_OPEN).setResolution(null));
  597. expectedException.expectMessage("At least one action must be provided");
  598. expectedException.expect(IllegalArgumentException.class);
  599. call(builder()
  600. .setIssues(singletonList(issue.getKey()))
  601. .setComment("type was badly defined")
  602. .build());
  603. }
  604. @Test
  605. public void fail_when_number_of_issues_is_more_than_500() {
  606. userSession.logIn("john");
  607. expectedException.expectMessage("Number of issues is limited to 500");
  608. expectedException.expect(IllegalArgumentException.class);
  609. call(builder()
  610. .setIssues(IntStream.range(0, 510).mapToObj(String::valueOf).collect(Collectors.toList()))
  611. .setSetSeverity(MINOR)
  612. .build());
  613. }
  614. @Test
  615. public void fail_when_not_authenticated() {
  616. expectedException.expect(UnauthorizedException.class);
  617. call(builder().setIssues(singletonList("ABCD")).build());
  618. }
  619. @Test
  620. public void test_definition() {
  621. WebService.Action action = tester.getDef();
  622. assertThat(action.key()).isEqualTo("bulk_change");
  623. assertThat(action.isPost()).isTrue();
  624. assertThat(action.isInternal()).isFalse();
  625. assertThat(action.params()).hasSize(9);
  626. assertThat(action.responseExample()).isNotNull();
  627. }
  628. private BulkChangeWsResponse call(BulkChangeRequest bulkChangeRequest) {
  629. TestRequest request = tester.newRequest();
  630. ofNullable(bulkChangeRequest.getIssues()).ifPresent(value6 -> request.setParam("issues", String.join(",", value6)));
  631. ofNullable(bulkChangeRequest.getAssign()).ifPresent(value5 -> request.setParam("assign", value5));
  632. ofNullable(bulkChangeRequest.getSetSeverity()).ifPresent(value4 -> request.setParam("set_severity", value4));
  633. ofNullable(bulkChangeRequest.getSetType()).ifPresent(value3 -> request.setParam("set_type", value3));
  634. ofNullable(bulkChangeRequest.getDoTransition()).ifPresent(value2 -> request.setParam("do_transition", value2));
  635. ofNullable(bulkChangeRequest.getComment()).ifPresent(value1 -> request.setParam("comment", value1));
  636. ofNullable(bulkChangeRequest.getSendNotifications()).ifPresent(value -> request.setParam("sendNotifications", value != null ? value ? "true" : "false" : null));
  637. if (!bulkChangeRequest.getAddTags().isEmpty()) {
  638. request.setParam("add_tags", String.join(",", bulkChangeRequest.getAddTags()));
  639. }
  640. if (!bulkChangeRequest.getRemoveTags().isEmpty()) {
  641. request.setParam("remove_tags", String.join(",", bulkChangeRequest.getRemoveTags()));
  642. }
  643. return request.executeProtobuf(BulkChangeWsResponse.class);
  644. }
  645. private void addUserProjectPermissions(UserDto user, ComponentDto project, String... permissions) {
  646. for (String permission : permissions) {
  647. db.users().insertProjectPermissionOnUser(user, permission, project);
  648. userSession.addProjectPermission(permission, project);
  649. }
  650. }
  651. private void checkResponse(BulkChangeWsResponse response, long total, long success, long ignored, long failure) {
  652. assertThat(response)
  653. .extracting(BulkChangeWsResponse::getTotal, BulkChangeWsResponse::getSuccess, BulkChangeWsResponse::getIgnored, BulkChangeWsResponse::getFailures)
  654. .as("Total, success, ignored, failure")
  655. .containsExactly(total, success, ignored, failure);
  656. }
  657. private List<IssueDto> getIssueByKeys(String... issueKeys) {
  658. return db.getDbClient().issueDao().selectByKeys(db.getSession(), asList(issueKeys));
  659. }
  660. private void verifyPostProcessorCalled(ComponentDto... components) {
  661. assertThat(issueChangePostProcessor.calledComponents()).containsExactlyInAnyOrder(components);
  662. }
  663. private void verifyPostProcessorNotCalled() {
  664. assertThat(issueChangePostProcessor.wasCalled()).isFalse();
  665. }
  666. private void addActions() {
  667. actions.add(new org.sonar.server.issue.AssignAction(db.getDbClient(), issueFieldsSetter));
  668. actions.add(new org.sonar.server.issue.SetSeverityAction(issueFieldsSetter, userSession));
  669. actions.add(new org.sonar.server.issue.SetTypeAction(issueFieldsSetter, userSession));
  670. actions.add(new org.sonar.server.issue.TransitionAction(new TransitionService(userSession, issueWorkflow)));
  671. actions.add(new org.sonar.server.issue.AddTagsAction(issueFieldsSetter));
  672. actions.add(new org.sonar.server.issue.RemoveTagsAction(issueFieldsSetter));
  673. actions.add(new org.sonar.server.issue.CommentAction(issueFieldsSetter));
  674. }
  675. private static class BulkChangeRequest {
  676. private final List<String> issues;
  677. private final String assign;
  678. private final String setSeverity;
  679. private final String setType;
  680. private final String doTransition;
  681. private final List<String> addTags;
  682. private final List<String> removeTags;
  683. private final String comment;
  684. private final Boolean sendNotifications;
  685. private BulkChangeRequest(Builder builder) {
  686. this.issues = builder.issues;
  687. this.assign = builder.assign;
  688. this.setSeverity = builder.setSeverity;
  689. this.setType = builder.setType;
  690. this.doTransition = builder.doTransition;
  691. this.addTags = builder.addTags;
  692. this.removeTags = builder.removeTags;
  693. this.comment = builder.comment;
  694. this.sendNotifications = builder.sendNotifications;
  695. }
  696. public List<String> getIssues() {
  697. return issues;
  698. }
  699. @CheckForNull
  700. public String getAssign() {
  701. return assign;
  702. }
  703. @CheckForNull
  704. public String getSetSeverity() {
  705. return setSeverity;
  706. }
  707. @CheckForNull
  708. public String getSetType() {
  709. return setType;
  710. }
  711. @CheckForNull
  712. public String getDoTransition() {
  713. return doTransition;
  714. }
  715. public List<String> getAddTags() {
  716. return addTags;
  717. }
  718. public List<String> getRemoveTags() {
  719. return removeTags;
  720. }
  721. @CheckForNull
  722. public String getComment() {
  723. return comment;
  724. }
  725. @CheckForNull
  726. public Boolean getSendNotifications() {
  727. return sendNotifications;
  728. }
  729. }
  730. public static Builder builder() {
  731. return new Builder();
  732. }
  733. public static class Builder {
  734. private List<String> issues;
  735. private String assign;
  736. private String setSeverity;
  737. private String setType;
  738. private String doTransition;
  739. private List<String> addTags = newArrayList();
  740. private List<String> removeTags = newArrayList();
  741. private String comment;
  742. private Boolean sendNotifications;
  743. public Builder setIssues(List<String> issues) {
  744. this.issues = issues;
  745. return this;
  746. }
  747. public Builder setAssign(@Nullable String assign) {
  748. this.assign = assign;
  749. return this;
  750. }
  751. public Builder setSetSeverity(@Nullable String setSeverity) {
  752. this.setSeverity = setSeverity;
  753. return this;
  754. }
  755. public Builder setSetType(@Nullable String setType) {
  756. this.setType = setType;
  757. return this;
  758. }
  759. public Builder setDoTransition(@Nullable String doTransition) {
  760. this.doTransition = doTransition;
  761. return this;
  762. }
  763. public Builder setAddTags(List<String> addTags) {
  764. this.addTags = requireNonNull(addTags);
  765. return this;
  766. }
  767. public Builder setRemoveTags(List<String> removeTags) {
  768. this.removeTags = requireNonNull(removeTags);
  769. return this;
  770. }
  771. public Builder setComment(@Nullable String comment) {
  772. this.comment = comment;
  773. return this;
  774. }
  775. public Builder setSendNotifications(@Nullable Boolean sendNotifications) {
  776. this.sendNotifications = sendNotifications;
  777. return this;
  778. }
  779. public BulkChangeRequest build() {
  780. checkArgument(issues != null && !issues.isEmpty(), "Issue keys must be provided");
  781. return new BulkChangeRequest(this);
  782. }
  783. }
  784. }