]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3755 Add security check on issues action
authorJulien Lancelot <julien.lancelot@gmail.com>
Tue, 4 Jun 2013 17:14:49 +0000 (19:14 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Tue, 4 Jun 2013 17:14:49 +0000 (19:14 +0200)
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueQueryResult.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueQueryResult.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java
sonar-server/src/main/java/org/sonar/server/issue/ActionPlanService.java
sonar-server/src/main/java/org/sonar/server/issue/IssueService.java
sonar-server/src/test/java/org/sonar/server/issue/ActionPlanServiceTest.java
sonar-server/src/test/java/org/sonar/server/issue/IssueServiceTest.java [new file with mode: 0644]

index 7df853a07449f09202f072f48ad23796392e6488..2e6a8245dc1463040ecd4a49e738de6b6bd1493a 100644 (file)
@@ -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
index 6e579bfb2196712701f4de75f4b74512689f206a..4caa572140ae01e47f3fe12f1b43bfbd9c9bb4b5 100644 (file)
@@ -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<Issue> issues();
 
-  @CheckForNull
+  /**
+   * Return first issue in the list.
+   * It will throws IllegalArgumentException if no issue found.
+   */
   Issue first();
 
   Rule rule(Issue issue);
index 8bd1e6b322bd2484a0d404f9e387842e4d6cdf4a..1e86fd5004ac8f31d01add11d51f8c1d93152128 100644 (file)
@@ -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;
   }
index 8e8331096e107413685c548439c1fa6e061ed6c2..3edbd696c8a0906ed4bffa902e2f56f0bceade98 100644 (file)
@@ -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());
     }
   }
 
index 5a146420b9a74f9eff2a2290f9312e6c63116826..8d694dd2b2a224e174d0ab9f307208236a91b9d7 100644 (file)
  */
 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;
   }
 }
index 67f5bf86917a34a9ad35e4c9be49ad310fdfa2e7..fdf586dbd6bdfdfda74c9ce8daacfc7837f4d95a 100644 (file)
@@ -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 (file)
index 0000000..0f640f2
--- /dev/null
@@ -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.<Issue>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<Transition> transitions = newArrayList(transition);
+    when(workflow.outTransitions(issue)).thenReturn(transitions);
+
+    List<Transition> 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<IssueChangeContext> 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<IssueChangeContext> 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<IssueChangeContext> 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<IssueChangeContext> 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));
+  }
+
+}