From 9b81a46f6d32355444ad89293275e8b42a86f991 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Tue, 4 Jun 2013 19:14:49 +0200 Subject: [PATCH] SONAR-3755 Add security check on issues action --- .../core/issue/DefaultIssueQueryResult.java | 5 +- .../org/sonar/api/issue/IssueQueryResult.java | 6 +- .../api/issue/internal/DefaultIssue.java | 4 + .../sonar/server/issue/ActionPlanService.java | 5 +- .../org/sonar/server/issue/IssueService.java | 83 ++-- .../server/issue/ActionPlanServiceTest.java | 21 +- .../sonar/server/issue/IssueServiceTest.java | 429 ++++++++++++++++++ 7 files changed, 514 insertions(+), 39 deletions(-) create mode 100644 sonar-server/src/test/java/org/sonar/server/issue/IssueServiceTest.java diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueQueryResult.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueQueryResult.java index 7df853a0744..2e6a8245dc1 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueQueryResult.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueQueryResult.java @@ -104,7 +104,10 @@ public class DefaultIssueQueryResult implements IssueQueryResult { @Override public Issue first() { - return issues != null && !issues.isEmpty() ? issues.get(0) : null; + if (issues != null && !issues.isEmpty()) { + return issues.get(0); + } + throw new IllegalArgumentException("No issue"); } @Override diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueQueryResult.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueQueryResult.java index 6e579bfb219..4caa572140a 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueQueryResult.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueQueryResult.java @@ -25,6 +25,7 @@ import org.sonar.api.user.User; import org.sonar.api.utils.Paging; import javax.annotation.CheckForNull; + import java.util.Collection; import java.util.List; @@ -34,7 +35,10 @@ import java.util.List; public interface IssueQueryResult { List issues(); - @CheckForNull + /** + * Return first issue in the list. + * It will throws IllegalArgumentException if no issue found. + */ Issue first(); Rule rule(Issue issue); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java index 8bd1e6b322b..1e86fd5004a 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java @@ -110,6 +110,10 @@ public class DefaultIssue implements Issue { return this; } + /** + * The project key is not always populated, that's why it's not present is the Issue API + */ + @CheckForNull public String projectKey() { return projectKey; } diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ActionPlanService.java b/sonar-server/src/main/java/org/sonar/server/issue/ActionPlanService.java index 8e8331096e1..3edbd696c8a 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/ActionPlanService.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/ActionPlanService.java @@ -148,6 +148,7 @@ public class ActionPlanService implements ServerComponent { private ActionPlanDto findActionPlanDto(String actionPlanKey) { ActionPlanDto actionPlanDto = actionPlanDao.findByKey(actionPlanKey); if (actionPlanDto == null) { + // TODO throw 404 throw new IllegalArgumentException("Action plan " + actionPlanKey + " has not been found."); } return actionPlanDto; @@ -156,6 +157,7 @@ public class ActionPlanService implements ServerComponent { private ResourceDto findProject(String projectKey) { ResourceDto resourceDto = resourceDao.getResource(ResourceQuery.create().setKey(projectKey)); if (resourceDto == null) { + // TODO throw 404 throw new IllegalArgumentException("Project " + projectKey + " does not exists."); } return resourceDto; @@ -171,7 +173,8 @@ public class ActionPlanService implements ServerComponent { throw new IllegalStateException("User is not logged in"); } if (!authorizationDao.isAuthorizedComponentId(project.getId(), userSession.userId(), requiredRole)) { - throw new IllegalStateException("User does not have the required role to access the project: " + project.getKey()); + // TODO throw unauthorized + throw new IllegalStateException("User does not have the required role on the project: " + project.getKey()); } } diff --git a/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java b/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java index 5a146420b9a..8d694dd2b2a 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java @@ -19,8 +19,10 @@ */ package org.sonar.server.issue; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import org.sonar.api.ServerComponent; +import org.sonar.api.component.Component; import org.sonar.api.issue.Issue; import org.sonar.api.issue.IssueQuery; import org.sonar.api.issue.IssueQueryResult; @@ -28,12 +30,16 @@ import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.issue.internal.IssueChangeContext; import org.sonar.api.rules.Rule; import org.sonar.api.rules.RuleFinder; +import org.sonar.api.user.UserFinder; import org.sonar.api.web.UserRole; import org.sonar.core.issue.IssueNotifications; import org.sonar.core.issue.IssueUpdater; import org.sonar.core.issue.db.IssueStorage; import org.sonar.core.issue.workflow.IssueWorkflow; import org.sonar.core.issue.workflow.Transition; +import org.sonar.core.resource.ResourceDao; +import org.sonar.core.resource.ResourceDto; +import org.sonar.core.user.AuthorizationDao; import org.sonar.server.user.UserSession; import javax.annotation.Nullable; @@ -52,17 +58,23 @@ public class IssueService implements ServerComponent { private final IssueWorkflow workflow; private final IssueUpdater issueUpdater; private final IssueStorage issueStorage; + private final IssueNotifications issueNotifications; private final ActionPlanService actionPlanService; private final RuleFinder ruleFinder; - private final IssueNotifications issueNotifications; + private final ResourceDao resourceDao; + private final AuthorizationDao authorizationDao; + private final UserFinder userFinder; public IssueService(DefaultIssueFinder finder, IssueWorkflow workflow, IssueStorage issueStorage, IssueUpdater issueUpdater, + IssueNotifications issueNotifications, ActionPlanService actionPlanService, RuleFinder ruleFinder, - IssueNotifications issueNotifications) { + ResourceDao resourceDao, + AuthorizationDao authorizationDao, + UserFinder userFinder) { this.finder = finder; this.workflow = workflow; this.issueStorage = issueStorage; @@ -70,6 +82,9 @@ public class IssueService implements ServerComponent { this.actionPlanService = actionPlanService; this.ruleFinder = ruleFinder; this.issueNotifications = issueNotifications; + this.resourceDao = resourceDao; + this.authorizationDao = authorizationDao; + this.userFinder = userFinder; } /** @@ -92,9 +107,9 @@ public class IssueService implements ServerComponent { } public Issue doTransition(String issueKey, String transition, UserSession userSession) { - verifyLoggedIn(userSession); IssueQueryResult queryResult = loadIssue(issueKey); DefaultIssue issue = (DefaultIssue) queryResult.first(); + checkAuthorization(userSession, issue, UserRole.USER); IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login()); if (workflow.doTransition(issue, transition, context)) { issueStorage.save(issue); @@ -104,29 +119,28 @@ public class IssueService implements ServerComponent { } public Issue assign(String issueKey, @Nullable String assignee, UserSession userSession) { - verifyLoggedIn(userSession); IssueQueryResult queryResult = loadIssue(issueKey); DefaultIssue issue = (DefaultIssue) queryResult.first(); - - if (issue != null) { - // TODO check that assignee exists - IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login()); - if (issueUpdater.assign(issue, assignee, context)) { - issueStorage.save(issue); - issueNotifications.sendChanges(issue, context, queryResult); - } + checkAuthorization(userSession, issue, UserRole.USER); + if (assignee != null && userFinder.findByLogin(assignee) == null) { + throw new IllegalArgumentException("Unknown user: " + assignee); + } + IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login()); + if (issueUpdater.assign(issue, assignee, context)) { + issueStorage.save(issue); + issueNotifications.sendChanges(issue, context, queryResult); } return issue; } public Issue plan(String issueKey, @Nullable String actionPlanKey, UserSession userSession) { if (!Strings.isNullOrEmpty(actionPlanKey) && actionPlanService.findByKey(actionPlanKey, userSession) == null) { - throw new IllegalStateException("Unknown action plan: " + actionPlanKey); + throw new IllegalArgumentException("Unknown action plan: " + actionPlanKey); } - - verifyLoggedIn(userSession); IssueQueryResult queryResult = loadIssue(issueKey); DefaultIssue issue = (DefaultIssue) queryResult.first(); + checkAuthorization(userSession, issue, UserRole.USER); + IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login()); if (issueUpdater.plan(issue, actionPlanKey, context)) { issueStorage.save(issue); @@ -136,9 +150,10 @@ public class IssueService implements ServerComponent { } public Issue setSeverity(String issueKey, String severity, UserSession userSession) { - verifyLoggedIn(userSession); IssueQueryResult queryResult = loadIssue(issueKey); DefaultIssue issue = (DefaultIssue) queryResult.first(); + checkAuthorization(userSession, issue, UserRole.USER); + IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login()); if (issueUpdater.setManualSeverity(issue, severity, context)) { issueStorage.save(issue); @@ -148,7 +163,7 @@ public class IssueService implements ServerComponent { } public DefaultIssue createManualIssue(DefaultIssue issue, UserSession userSession) { - verifyLoggedIn(userSession); + checkAuthorization(userSession, issue, UserRole.USER); if (!"manual".equals(issue.ruleKey().repository())) { throw new IllegalArgumentException("Issues can be created only on rules marked as 'manual': " + issue.ruleKey()); } @@ -156,24 +171,23 @@ public class IssueService implements ServerComponent { if (rule == null) { throw new IllegalArgumentException("Unknown rule: " + issue.ruleKey()); } + Component component = resourceDao.findByKey(issue.componentKey()); + if (component == null) { + throw new IllegalArgumentException("Unknown component: " + issue.componentKey()); + } Date now = new Date(); issue.setCreationDate(now); issue.setUpdateDate(now); - - // TODO check existence of component - // TODO verify authorization - issueStorage.save(issue); return issue; } - public IssueQueryResult loadIssue(String issueKey) { - IssueQuery query = IssueQuery.builder().issueKeys(Arrays.asList(issueKey)).requiredRole(UserRole.USER).build(); - IssueQueryResult result = finder.find(query); - if (result.issues().size()!=1) { - throw new IllegalStateException("Issue not found: " + issueKey); + IssueQueryResult result = finder.find(IssueQuery.builder().issueKeys(Arrays.asList(issueKey)).requiredRole(UserRole.USER).build()); + if (result.issues().size() != 1) { + // TODO throw 404 + throw new IllegalArgumentException("Issue not found: " + issueKey); } return result; } @@ -182,10 +196,25 @@ public class IssueService implements ServerComponent { return workflow.statusKeys(); } - private void verifyLoggedIn(UserSession userSession) { + @VisibleForTesting + void checkAuthorization(UserSession userSession, Issue issue, String requiredRole) { if (!userSession.isLoggedIn()) { // must be logged throw new IllegalStateException("User is not logged in"); } + if (!authorizationDao.isAuthorizedComponentId(findProject(issue.componentKey()).getId(), userSession.userId(), requiredRole)) { + // TODO throw unauthorized + throw new IllegalStateException("User does not have the required role"); + } + } + + @VisibleForTesting + ResourceDto findProject(String componentKey) { + ResourceDto resourceDto = resourceDao.getRootProjectByComponentKey(componentKey); + if (resourceDto == null) { + // TODO throw 404 + throw new IllegalArgumentException("Component '" + componentKey + "' does not exists."); + } + return resourceDto; } } diff --git a/sonar-server/src/test/java/org/sonar/server/issue/ActionPlanServiceTest.java b/sonar-server/src/test/java/org/sonar/server/issue/ActionPlanServiceTest.java index 67f5bf86917..fdf586dbd6b 100644 --- a/sonar-server/src/test/java/org/sonar/server/issue/ActionPlanServiceTest.java +++ b/sonar-server/src/test/java/org/sonar/server/issue/ActionPlanServiceTest.java @@ -40,6 +40,7 @@ import java.util.Collection; import static com.google.common.collect.Lists.newArrayList; import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; @@ -50,7 +51,6 @@ public class ActionPlanServiceTest { private ResourceDao resourceDao = mock(ResourceDao.class); private AuthorizationDao authorizationDao = mock(AuthorizationDao.class); private UserSession userSession = mock(UserSession.class); - private ActionPlanService actionPlanService; @Before @@ -79,7 +79,8 @@ public class ActionPlanServiceTest { try { actionPlanService.create(actionPlan, userSession); - } catch (Exception e){ + fail(); + } catch (Exception e) { assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User is not logged in"); } verifyZeroInteractions(actionPlanDao); @@ -92,9 +93,10 @@ public class ActionPlanServiceTest { when(authorizationDao.isAuthorizedComponentId(anyLong(), eq(10), anyString())).thenReturn(false); try { - actionPlanService.create(actionPlan, userSession); - } catch (Exception e){ - assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User does not have the required role to access the project: org.sonar.Sample"); + actionPlanService.create(actionPlan, userSession); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User does not have the required role on the project: org.sonar.Sample"); } verify(authorizationDao).isAuthorizedComponentId(eq(1l), eq(10), eq(UserRole.ADMIN)); verifyZeroInteractions(actionPlanDao); @@ -170,8 +172,9 @@ public class ActionPlanServiceTest { try { actionPlanService.findOpenByProjectKey("org.sonar.Sample", userSession); - } catch (Exception e){ - assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User does not have the required role to access the project: org.sonar.Sample"); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User does not have the required role on the project: org.sonar.Sample"); } verify(authorizationDao).isAuthorizedComponentId(eq(1l), eq(10), eq(UserRole.USER)); verifyZeroInteractions(actionPlanDao); @@ -184,7 +187,7 @@ public class ActionPlanServiceTest { } @Test - public void should_find_action_plan_stats(){ + public void should_find_action_plan_stats() { when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(new ResourceDto().setId(1L).setKey("org.sonar.Sample")); when(actionPlanStatsDao.findByProjectId(1L)).thenReturn(newArrayList(new ActionPlanStatsDto())); @@ -193,7 +196,7 @@ public class ActionPlanServiceTest { } @Test(expected = IllegalArgumentException.class) - public void should_throw_exception_if_project_not_found_when_find_open_action_plan_stats(){ + public void should_throw_exception_if_project_not_found_when_find_open_action_plan_stats() { when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(null); actionPlanService.findActionPlanStats("org.sonar.Sample", userSession); diff --git a/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceTest.java b/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceTest.java new file mode 100644 index 00000000000..0f640f2f286 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceTest.java @@ -0,0 +1,429 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.issue; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.sonar.api.component.Component; +import org.sonar.api.issue.Issue; +import org.sonar.api.issue.IssueQuery; +import org.sonar.api.issue.IssueQueryResult; +import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.Rule; +import org.sonar.api.rules.RuleFinder; +import org.sonar.api.user.UserFinder; +import org.sonar.api.web.UserRole; +import org.sonar.core.issue.DefaultActionPlan; +import org.sonar.core.issue.IssueNotifications; +import org.sonar.core.issue.IssueUpdater; +import org.sonar.core.issue.db.IssueStorage; +import org.sonar.core.issue.workflow.IssueWorkflow; +import org.sonar.core.issue.workflow.Transition; +import org.sonar.core.resource.ResourceDao; +import org.sonar.core.resource.ResourceDto; +import org.sonar.core.user.AuthorizationDao; +import org.sonar.core.user.DefaultUser; +import org.sonar.server.user.UserSession; + +import java.util.Collections; +import java.util.List; + +import static com.google.common.collect.Lists.newArrayList; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + +public class IssueServiceTest { + + private DefaultIssueFinder finder = mock(DefaultIssueFinder.class); + private IssueWorkflow workflow = mock(IssueWorkflow.class); + private IssueUpdater issueUpdater = mock(IssueUpdater.class); + private IssueStorage issueStorage = mock(IssueStorage.class); + private IssueNotifications issueNotifications = mock(IssueNotifications.class); + private ActionPlanService actionPlanService = mock(ActionPlanService.class); + private RuleFinder ruleFinder = mock(RuleFinder.class); + private ResourceDao resourceDao = mock(ResourceDao.class); + private AuthorizationDao authorizationDao = mock(AuthorizationDao.class); + private UserFinder userFinder = mock(UserFinder.class); + private UserSession userSession = mock(UserSession.class); + private Transition transition = Transition.create("reopen", Issue.STATUS_RESOLVED, Issue.STATUS_REOPENED); + private IssueQueryResult issueQueryResult = mock(IssueQueryResult.class); + private DefaultIssue issue = new DefaultIssue().setKey("ABCD"); + private IssueService issueService; + + @Before + public void before() { + when(userSession.isLoggedIn()).thenReturn(true); + when(userSession.userId()).thenReturn(10); + when(userSession.login()).thenReturn("arthur"); + + when(authorizationDao.isAuthorizedComponentId(anyLong(), eq(10), anyString())).thenReturn(true); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + when(issueQueryResult.issues()).thenReturn(newArrayList((Issue) issue)); + when(issueQueryResult.first()).thenReturn(issue); + + issueService = new IssueService(finder, workflow, issueStorage, issueUpdater, issueNotifications, actionPlanService, ruleFinder, resourceDao, authorizationDao, userFinder); + } + + @Test + public void should_load_issue() { + IssueQueryResult result = issueService.loadIssue("ABCD"); + assertThat(result).isEqualTo(issueQueryResult); + } + + @Test + public void should_fail_to_load_issue() { + when(issueQueryResult.issues()).thenReturn(Collections.emptyList()); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + try { + issueService.loadIssue("ABCD"); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Issue not found: ABCD"); + } + } + + @Test + public void should_list_status() { + issueService.listStatus(); + verify(workflow).statusKeys(); + } + + @Test + public void should_list_transitions() { + List transitions = newArrayList(transition); + when(workflow.outTransitions(issue)).thenReturn(transitions); + + List result = issueService.listTransitions("ABCD"); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(transition); + } + + @Test + public void should_return_no_transition() { + when(issueQueryResult.first()).thenReturn(null); + when(issueQueryResult.issues()).thenReturn(newArrayList((Issue) new DefaultIssue())); + + assertThat(issueService.listTransitions("ABCD")).isEmpty(); + verifyZeroInteractions(workflow); + } + + @Test + public void should_do_transition() { + grantAccess(); + when(workflow.doTransition(eq(issue), eq(transition.key()), any(IssueChangeContext.class))).thenReturn(true); + + Issue result = issueService.doTransition("ABCD", transition.key(), userSession); + assertThat(result).isNotNull(); + + ArgumentCaptor measureCaptor = ArgumentCaptor.forClass(IssueChangeContext.class); + verify(workflow).doTransition(eq(issue), eq(transition.key()), measureCaptor.capture()); + verify(issueStorage).save(issue); + + IssueChangeContext issueChangeContext = measureCaptor.getValue(); + assertThat(issueChangeContext.login()).isEqualTo("arthur"); + assertThat(issueChangeContext.date()).isNotNull(); + + verify(issueNotifications).sendChanges(eq(issue), eq(issueChangeContext), eq(issueQueryResult)); + verify(authorizationDao).isAuthorizedComponentId(anyLong(), anyInt(), eq(UserRole.USER)); + } + + @Test + public void should_not_do_transition() { + grantAccess(); + when(workflow.doTransition(eq(issue), eq(transition.key()), any(IssueChangeContext.class))).thenReturn(false); + + Issue result = issueService.doTransition("ABCD", transition.key(), userSession); + assertThat(result).isNotNull(); + verify(workflow).doTransition(eq(issue), eq(transition.key()), any(IssueChangeContext.class)); + verifyZeroInteractions(issueStorage); + verifyZeroInteractions(issueNotifications); + } + + @Test + public void should_assign() { + grantAccess(); + String assignee = "perceval"; + + when(userFinder.findByLogin(assignee)).thenReturn(new DefaultUser()); + when(issueUpdater.assign(eq(issue), eq(assignee), any(IssueChangeContext.class))).thenReturn(true); + + Issue result = issueService.assign("ABCD", assignee, userSession); + assertThat(result).isNotNull(); + + ArgumentCaptor measureCaptor = ArgumentCaptor.forClass(IssueChangeContext.class); + verify(issueUpdater).assign(eq(issue), eq(assignee), measureCaptor.capture()); + verify(issueStorage).save(issue); + + IssueChangeContext issueChangeContext = measureCaptor.getValue(); + assertThat(issueChangeContext.login()).isEqualTo("arthur"); + assertThat(issueChangeContext.date()).isNotNull(); + + verify(issueNotifications).sendChanges(eq(issue), eq(issueChangeContext), eq(issueQueryResult)); + verify(authorizationDao).isAuthorizedComponentId(anyLong(), anyInt(), eq(UserRole.USER)); + } + + @Test + public void should_not_assign() { + grantAccess(); + String assignee = "perceval"; + + when(userFinder.findByLogin(assignee)).thenReturn(new DefaultUser()); + when(issueUpdater.assign(eq(issue), eq(assignee), any(IssueChangeContext.class))).thenReturn(false); + + Issue result = issueService.assign("ABCD", assignee, userSession); + assertThat(result).isNotNull(); + verify(issueUpdater).assign(eq(issue), eq(assignee),any(IssueChangeContext.class)); + verifyZeroInteractions(issueStorage); + verifyZeroInteractions(issueNotifications); + } + + @Test + public void should_fail_assign_if_assignee_not_found() { + grantAccess(); + String assignee = "perceval"; + + when(userFinder.findByLogin(assignee)).thenReturn(null); + + try { + issueService.assign("ABCD", assignee, userSession); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Unknown user: perceval"); + } + + verifyZeroInteractions(issueUpdater); + verifyZeroInteractions(issueStorage); + verifyZeroInteractions(issueNotifications); + } + + @Test + public void should_plan() { + grantAccess(); + String actionPlanKey = "EFGH"; + + when(actionPlanService.findByKey(actionPlanKey, userSession)).thenReturn(new DefaultActionPlan()); + when(issueUpdater.plan(eq(issue), eq(actionPlanKey), any(IssueChangeContext.class))).thenReturn(true); + + Issue result = issueService.plan("ABCD", actionPlanKey, userSession); + assertThat(result).isNotNull(); + + ArgumentCaptor measureCaptor = ArgumentCaptor.forClass(IssueChangeContext.class); + verify(issueUpdater).plan(eq(issue), eq(actionPlanKey), measureCaptor.capture()); + verify(issueStorage).save(issue); + + IssueChangeContext issueChangeContext = measureCaptor.getValue(); + assertThat(issueChangeContext.login()).isEqualTo("arthur"); + assertThat(issueChangeContext.date()).isNotNull(); + + verify(issueNotifications).sendChanges(eq(issue), eq(issueChangeContext), eq(issueQueryResult)); + verify(authorizationDao).isAuthorizedComponentId(anyLong(), anyInt(), eq(UserRole.USER)); + } + + @Test + public void should_not_plan() { + grantAccess(); + String actionPlanKey = "EFGH"; + + when(actionPlanService.findByKey(actionPlanKey, userSession)).thenReturn(new DefaultActionPlan()); + when(issueUpdater.plan(eq(issue), eq(actionPlanKey), any(IssueChangeContext.class))).thenReturn(false); + + Issue result = issueService.plan("ABCD", actionPlanKey, userSession); + assertThat(result).isNotNull(); + verify(issueUpdater).plan(eq(issue), eq(actionPlanKey), any(IssueChangeContext.class)); + verifyZeroInteractions(issueStorage); + verifyZeroInteractions(issueNotifications); + } + + @Test + public void should_fail_plan_if_action_plan_not_found() { + grantAccess(); + String actionPlanKey = "EFGH"; + + when(actionPlanService.findByKey(actionPlanKey, userSession)).thenReturn(null); + try { + issueService.plan("ABCD", actionPlanKey, userSession); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Unknown action plan: EFGH"); + } + + verifyZeroInteractions(issueUpdater); + verifyZeroInteractions(issueStorage); + verifyZeroInteractions(issueNotifications); + } + + @Test + public void should_set_severity() { + grantAccess(); + String severity = "MINOR"; + when(issueUpdater.setManualSeverity(eq(issue), eq(severity), any(IssueChangeContext.class))).thenReturn(true); + + Issue result = issueService.setSeverity("ABCD", severity, userSession); + assertThat(result).isNotNull(); + + ArgumentCaptor measureCaptor = ArgumentCaptor.forClass(IssueChangeContext.class); + verify(issueUpdater).setManualSeverity(eq(issue), eq(severity), measureCaptor.capture()); + verify(issueStorage).save(issue); + + IssueChangeContext issueChangeContext = measureCaptor.getValue(); + assertThat(issueChangeContext.login()).isEqualTo("arthur"); + assertThat(issueChangeContext.date()).isNotNull(); + + verify(issueNotifications).sendChanges(eq(issue), eq(issueChangeContext), eq(issueQueryResult)); + verify(authorizationDao).isAuthorizedComponentId(anyLong(), anyInt(), eq(UserRole.USER)); + } + + @Test + public void should_not_set_severity() { + grantAccess(); + String severity = "MINOR"; + when(issueUpdater.setManualSeverity(eq(issue), eq(severity), any(IssueChangeContext.class))).thenReturn(false); + + Issue result = issueService.setSeverity("ABCD", severity, userSession); + assertThat(result).isNotNull(); + verify(issueUpdater).setManualSeverity(eq(issue), eq(severity), any(IssueChangeContext.class)); + verifyZeroInteractions(issueStorage); + verifyZeroInteractions(issueNotifications); + } + + @Test + public void should_create_manual_issue() { + grantAccess(); + RuleKey ruleKey = RuleKey.of("manual", "manualRuleKey"); + DefaultIssue manualIssue = new DefaultIssue().setKey("GHIJ").setRuleKey(RuleKey.of("manual", "manualRuleKey")).setComponentKey("org.sonar.Sample"); + when(ruleFinder.findByKey(ruleKey)).thenReturn(Rule.create("manual", "manualRuleKey")); + when(resourceDao.findByKey("org.sonar.Sample")).thenReturn(mock(Component.class)); + + Issue result = issueService.createManualIssue(manualIssue, userSession); + assertThat(result).isNotNull(); + assertThat(result.creationDate()).isNotNull(); + assertThat(result.updateDate()).isNotNull(); + + verify(issueStorage).save(manualIssue); + verify(authorizationDao).isAuthorizedComponentId(anyLong(), anyInt(), eq(UserRole.USER)); + } + + @Test + public void should_fail_create_manual_issue_if_not_manual_rule() { + grantAccess(); + RuleKey ruleKey = RuleKey.of("squid", "s100"); + DefaultIssue manualIssue = new DefaultIssue().setKey("GHIJ").setRuleKey(ruleKey).setComponentKey("org.sonar.Sample"); + try { + issueService.createManualIssue(manualIssue, userSession); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Issues can be created only on rules marked as 'manual': squid:s100"); + } + + verifyZeroInteractions(issueStorage); + } + + @Test + public void should_fail_create_manual_issue_if_rule_not_found() { + grantAccess(); + RuleKey ruleKey = RuleKey.of("manual", "manualRuleKey"); + DefaultIssue manualIssue = new DefaultIssue().setKey("GHIJ").setRuleKey(RuleKey.of("manual", "manualRuleKey")).setComponentKey("org.sonar.Sample"); + when(ruleFinder.findByKey(ruleKey)).thenReturn(null); + try { + issueService.createManualIssue(manualIssue, userSession); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Unknown rule: manual:manualRuleKey"); + } + + verifyZeroInteractions(issueStorage); + } + + @Test + public void should_fail_create_manual_issue_if_component_not_found() { + grantAccess(); + RuleKey ruleKey = RuleKey.of("manual", "manualRuleKey"); + DefaultIssue manualIssue = new DefaultIssue().setKey("GHIJ").setRuleKey(RuleKey.of("manual", "manualRuleKey")).setComponentKey("org.sonar.Sample"); + when(ruleFinder.findByKey(ruleKey)).thenReturn(Rule.create("manual", "manualRuleKey")); + when(resourceDao.findByKey("org.sonar.Sample")).thenReturn(null); + try { + issueService.createManualIssue(manualIssue, userSession); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Unknown component: org.sonar.Sample"); + } + + verifyZeroInteractions(issueStorage); + } + + @Test + public void should_fail_if_not_logged() { + when(userSession.isLoggedIn()).thenReturn(false); + try { + issueService.checkAuthorization(userSession, issue, UserRole.USER); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User is not logged in"); + } + verifyZeroInteractions(authorizationDao); + } + + @Test + public void should_fail_if_not_having_required_role() { + grantAccess(); + when(userSession.isLoggedIn()).thenReturn(true); + when(authorizationDao.isAuthorizedComponentId(anyLong(), anyInt(), anyString())).thenReturn(false); + try { + issueService.checkAuthorization(userSession, issue, UserRole.USER); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User does not have the required role"); + } + } + + @Test + public void should_find_project() { + ResourceDto project = new ResourceDto().setKey("org.sonar.Sample").setId(1l); + when(resourceDao.getRootProjectByComponentKey(anyString())).thenReturn(project); + assertThat(issueService.findProject("org.sonar.Sample")).isEqualTo(project); + } + + @Test + public void should_fail_to_find_project() { + when(resourceDao.getRootProjectByComponentKey(anyString())).thenReturn(null); + try { + issueService.findProject("org.sonar.Sample"); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Component 'org.sonar.Sample' does not exists."); + } + } + + private void grantAccess(){ + when(resourceDao.getRootProjectByComponentKey(anyString())).thenReturn(new ResourceDto().setKey("org.sonar.Sample").setId(1l)); + } + +} -- 2.39.5