]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10085 add Quality Gate Change event API
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 16 Nov 2017 09:36:17 +0000 (10:36 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 24 Nov 2017 08:23:58 +0000 (09:23 +0100)
and use it for webhook on issue changes

26 files changed:
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDbTester.java
server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/issue/webhook/package-info.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTrigger.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java [new file with mode: 0644]

index ddd63f27043aba03e8c10e8313d6eecfe5168fb3..cb0349e386846380e0fdd354f64ade8ed570890b 100644 (file)
@@ -27,6 +27,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.organization.OrganizationDto;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
 import static java.util.Arrays.asList;
 import static org.sonar.db.component.BranchType.LONG;
@@ -280,6 +281,16 @@ public class ComponentDbTester {
     return branch;
   }
 
+  public final ComponentDto insertProjectBranch(ComponentDto project, BranchDto branchDto) {
+    // MainBranchProjectUuid will be null if it's a main branch
+    checkArgument(branchDto.getProjectUuid().equals(firstNonNull(project.getMainBranchProjectUuid(), project.projectUuid())));
+    ComponentDto branch = newProjectBranch(project, branchDto);
+    insertComponent(branch);
+    dbClient.branchDao().insert(dbSession, branchDto);
+    db.commit();
+    return branch;
+  }
+
   private static <T> T firstNonNull(@Nullable T first, T second) {
     return (first != null) ? first : second;
   }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhook.java
deleted file mode 100644 (file)
index 6d50971..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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.webhook;
-
-import com.google.common.collect.ImmutableList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import javax.annotation.Nullable;
-import org.sonar.api.rules.RuleType;
-import org.sonar.core.issue.DefaultIssue;
-import org.sonar.core.issue.IssueChangeContext;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.server.issue.ws.SearchResponseData;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-public interface IssueChangeWebhook {
-  /**
-   * Will call webhooks once for any short living branch which has at least one issue in {@link SearchResponseData} and
-   * if change described in {@link IssueChange} can alter the status of the short living branch.
-   */
-  void onChange(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context);
-
-  final class IssueChange {
-    private final RuleType ruleType;
-    private final String transitionKey;
-
-    public IssueChange(RuleType ruleType) {
-      this(ruleType, null);
-    }
-
-    public IssueChange(String transitionKey) {
-      this(null, transitionKey);
-    }
-
-    public IssueChange(@Nullable RuleType ruleType, @Nullable String transitionKey) {
-      checkArgument(ruleType != null || transitionKey != null, "At least one of ruleType and transitionKey must be non null");
-      this.ruleType = ruleType;
-      this.transitionKey = transitionKey;
-    }
-
-    public Optional<RuleType> getRuleType() {
-      return Optional.ofNullable(ruleType);
-    }
-
-    public Optional<String> getTransitionKey() {
-      return Optional.ofNullable(transitionKey);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      IssueChange that = (IssueChange) o;
-      return ruleType == that.ruleType &&
-        Objects.equals(transitionKey, that.transitionKey);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(ruleType, transitionKey);
-    }
-
-    @Override
-    public String toString() {
-      return "IssueChange{" +
-        "ruleType=" + ruleType +
-        ", transitionKey='" + transitionKey + '\'' +
-        '}';
-    }
-  }
-
-  final class IssueChangeData {
-    private final List<DefaultIssue> issues;
-    private final List<ComponentDto> components;
-
-    public IssueChangeData(List<DefaultIssue> issues, List<ComponentDto> components) {
-      this.issues = ImmutableList.copyOf(issues);
-      this.components = ImmutableList.copyOf(components);
-    }
-
-    public List<DefaultIssue> getIssues() {
-      return issues;
-    }
-
-    public List<ComponentDto> getComponents() {
-      return components;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      IssueChangeData that = (IssueChangeData) o;
-      return Objects.equals(issues, that.issues) &&
-        Objects.equals(components, that.components);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(issues, components);
-    }
-
-    @Override
-    public String toString() {
-      return "IssueChangeData{" +
-        "issues=" + issues +
-        ", components=" + components +
-        '}';
-    }
-  }
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/IssueChangeWebhookImpl.java
deleted file mode 100644 (file)
index 698dae3..0000000
+++ /dev/null
@@ -1,277 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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.webhook;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.elasticsearch.action.search.SearchResponse;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.issue.DefaultTransitions;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.api.rules.RuleType;
-import org.sonar.api.utils.System2;
-import org.sonar.core.issue.DefaultIssue;
-import org.sonar.core.issue.IssueChangeContext;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.component.AnalysisPropertyDto;
-import org.sonar.db.component.BranchDto;
-import org.sonar.db.component.BranchType;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.SnapshotDto;
-import org.sonar.db.qualitygate.QualityGateConditionDto;
-import org.sonar.server.es.Facets;
-import org.sonar.server.es.SearchOptions;
-import org.sonar.server.issue.IssueQuery;
-import org.sonar.server.issue.index.IssueIndex;
-import org.sonar.server.qualitygate.Condition;
-import org.sonar.server.qualitygate.EvaluatedCondition;
-import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
-import org.sonar.server.qualitygate.EvaluatedQualityGate;
-import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
-import org.sonar.server.rule.index.RuleIndex;
-import org.sonar.server.settings.ProjectConfigurationLoader;
-import org.sonar.server.webhook.Analysis;
-import org.sonar.server.webhook.Branch;
-import org.sonar.server.webhook.Project;
-import org.sonar.server.webhook.ProjectAnalysis;
-import org.sonar.server.webhook.WebHooks;
-import org.sonar.server.webhook.WebhookPayload;
-import org.sonar.server.webhook.WebhookPayloadFactory;
-
-import static java.lang.String.format;
-import static java.lang.String.valueOf;
-import static java.util.Collections.singletonList;
-import static org.sonar.core.util.stream.MoreCollectors.toSet;
-import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
-
-public class IssueChangeWebhookImpl implements IssueChangeWebhook {
-  private static final Set<String> MEANINGFUL_TRANSITIONS = ImmutableSet.of(
-    DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
-  private final DbClient dbClient;
-  private final WebHooks webhooks;
-  private final ProjectConfigurationLoader projectConfigurationLoader;
-  private final WebhookPayloadFactory webhookPayloadFactory;
-  private final IssueIndex issueIndex;
-  private final System2 system2;
-
-  public IssueChangeWebhookImpl(DbClient dbClient, WebHooks webhooks, ProjectConfigurationLoader projectConfigurationLoader,
-    WebhookPayloadFactory webhookPayloadFactory, IssueIndex issueIndex, System2 system2) {
-    this.dbClient = dbClient;
-    this.webhooks = webhooks;
-    this.projectConfigurationLoader = projectConfigurationLoader;
-    this.webhookPayloadFactory = webhookPayloadFactory;
-    this.issueIndex = issueIndex;
-    this.system2 = system2;
-  }
-
-  @Override
-  public void onChange(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context) {
-    if (isEmpty(issueChangeData) || !isUserChangeContext(context) || !isRelevant(issueChange)) {
-      return;
-    }
-
-    callWebHook(issueChangeData);
-  }
-
-  private static boolean isRelevant(IssueChange issueChange) {
-    return issueChange.getTransitionKey().map(IssueChangeWebhookImpl::isMeaningfulTransition).orElse(true);
-  }
-
-  private static boolean isEmpty(IssueChangeData issueChangeData) {
-    return issueChangeData.getIssues().isEmpty();
-  }
-
-  private static boolean isUserChangeContext(IssueChangeContext context) {
-    return context.login() != null;
-  }
-
-  private static boolean isMeaningfulTransition(String transitionKey) {
-    return MEANINGFUL_TRANSITIONS.contains(transitionKey);
-  }
-
-  private void callWebHook(IssueChangeData issueChangeData) {
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      Map<String, ComponentDto> branchesByUuid = getBranchComponents(dbSession, issueChangeData);
-      if (branchesByUuid.isEmpty()) {
-        return;
-      }
-
-      Set<String> branchProjectUuids = branchesByUuid.values().stream()
-        .map(ComponentDto::uuid)
-        .collect(toSet(branchesByUuid.size()));
-      Set<BranchDto> shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids)
-        .stream()
-        .filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT)
-        .collect(toSet(branchesByUuid.size()));
-      if (shortBranches.isEmpty()) {
-        return;
-      }
-
-      Map<String, Configuration> configurationByUuid = projectConfigurationLoader.loadProjectConfigurations(dbSession,
-        shortBranches.stream().map(shortBranch -> branchesByUuid.get(shortBranch.getUuid())).collect(Collectors.toSet()));
-      Set<BranchDto> branchesWithWebhooks = shortBranches.stream()
-        .filter(shortBranch -> webhooks.isEnabled(configurationByUuid.get(shortBranch.getUuid())))
-        .collect(toSet());
-      if (branchesWithWebhooks.isEmpty()) {
-        return;
-      }
-
-      Map<String, SnapshotDto> analysisByProjectUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(
-        dbSession,
-        branchesWithWebhooks.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size())))
-        .stream()
-        .collect(uniqueIndex(SnapshotDto::getComponentUuid));
-      branchesWithWebhooks
-        .forEach(shortBranch -> {
-          ComponentDto branch = branchesByUuid.get(shortBranch.getUuid());
-          SnapshotDto analysis = analysisByProjectUuid.get(shortBranch.getUuid());
-          if (branch != null && analysis != null) {
-            webhooks.sendProjectAnalysisUpdate(
-              configurationByUuid.get(shortBranch.getUuid()),
-              new WebHooks.Analysis(shortBranch.getUuid(), analysis.getUuid(), null),
-              () -> buildWebHookPayload(dbSession, branch, shortBranch, analysis));
-          }
-        });
-    }
-  }
-
-  private WebhookPayload buildWebHookPayload(DbSession dbSession, ComponentDto branch, BranchDto shortBranch, SnapshotDto analysis) {
-    Map<String, String> analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid())
-      .stream()
-      .collect(Collectors.toMap(AnalysisPropertyDto::getKey, AnalysisPropertyDto::getValue));
-    ProjectAnalysis projectAnalysis = new ProjectAnalysis(
-      new Project(branch.getMainBranchProjectUuid(), branch.getKey(), branch.name()),
-      null,
-      new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
-      new Branch(false, shortBranch.getKey(), Branch.Type.SHORT),
-      createQualityGate(branch, issueIndex),
-      null,
-      analysisProperties);
-    return webhookPayloadFactory.create(projectAnalysis);
-  }
-
-  private EvaluatedQualityGate createQualityGate(ComponentDto branch, IssueIndex issueIndex) {
-    SearchResponse searchResponse = issueIndex.search(IssueQuery.builder()
-      .projectUuids(singletonList(branch.getMainBranchProjectUuid()))
-      .branchUuid(branch.uuid())
-      .mainBranch(false)
-      .resolved(false)
-      .checkAuthorization(false)
-      .build(),
-      new SearchOptions().addFacets(RuleIndex.FACET_TYPES));
-    LinkedHashMap<String, Long> typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone())
-      .get(RuleIndex.FACET_TYPES);
-
-    EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder();
-    Set<Condition> conditions = ShortLivingBranchQualityGate.CONDITIONS.stream()
-      .map(c -> {
-        long measure = getMeasure(typeFacet, c);
-        EvaluationStatus status = measure > 0 ? EvaluationStatus.ERROR : EvaluationStatus.OK;
-        Condition condition = new Condition(c.getMetricKey(), toOperator(c), c.getErrorThreshold(), c.getWarnThreshold(), c.isOnLeak());
-        builder.addCondition(condition, status, valueOf(measure));
-        return condition;
-      })
-      .collect(toSet(ShortLivingBranchQualityGate.CONDITIONS.size()));
-    builder
-      .setQualityGate(
-        new org.sonar.server.qualitygate.QualityGate(
-          valueOf(ShortLivingBranchQualityGate.ID),
-          ShortLivingBranchQualityGate.NAME,
-          conditions))
-      .setStatus(qgStatusFrom(builder.getEvaluatedConditions()));
-
-    return builder.build();
-  }
-
-  private static Condition.Operator toOperator(ShortLivingBranchQualityGate.Condition c) {
-    String operator = c.getOperator();
-    switch (operator) {
-      case QualityGateConditionDto.OPERATOR_GREATER_THAN:
-        return Condition.Operator.GREATER_THAN;
-      case QualityGateConditionDto.OPERATOR_LESS_THAN:
-        return Condition.Operator.LESS_THAN;
-      case QualityGateConditionDto.OPERATOR_EQUALS:
-        return Condition.Operator.EQUALS;
-      case QualityGateConditionDto.OPERATOR_NOT_EQUALS:
-        return Condition.Operator.NOT_EQUALS;
-      default:
-        throw new IllegalArgumentException(format("Unsupported Condition operator '%s'", operator));
-    }
-  }
-
-  private static EvaluatedQualityGate.Status qgStatusFrom(Set<EvaluatedCondition> conditions) {
-    if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluationStatus.ERROR)) {
-      return EvaluatedQualityGate.Status.ERROR;
-    }
-    return EvaluatedQualityGate.Status.OK;
-  }
-
-  private static long getMeasure(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) {
-    String metricKey = c.getMetricKey();
-    switch (metricKey) {
-      case CoreMetrics.BUGS_KEY:
-        return getValueForRuleType(typeFacet, RuleType.BUG);
-      case CoreMetrics.VULNERABILITIES_KEY:
-        return getValueForRuleType(typeFacet, RuleType.VULNERABILITY);
-      case CoreMetrics.CODE_SMELLS_KEY:
-        return getValueForRuleType(typeFacet, RuleType.CODE_SMELL);
-      default:
-        throw new IllegalArgumentException(format("Unsupported metric key '%s' in hardcoded quality gate", metricKey));
-    }
-  }
-
-  private static long getValueForRuleType(Map<String, Long> facet, RuleType ruleType) {
-    Long res = facet.get(ruleType.name());
-    if (res == null) {
-      return 0L;
-    }
-    return res;
-  }
-
-  private Map<String, ComponentDto> getBranchComponents(DbSession dbSession, IssueChangeData issueChangeData) {
-    Set<String> projectUuids = issueChangeData.getIssues().stream()
-      .map(DefaultIssue::projectUuid)
-      .collect(toSet());
-    Set<String> missingProjectUuids = ImmutableSet.copyOf(Sets.difference(
-      projectUuids,
-      issueChangeData.getComponents()
-        .stream()
-        .map(ComponentDto::uuid)
-        .collect(Collectors.toSet())));
-    if (missingProjectUuids.isEmpty()) {
-      return issueChangeData.getComponents()
-        .stream()
-        .filter(c -> projectUuids.contains(c.uuid()))
-        .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
-        .collect(uniqueIndex(ComponentDto::uuid));
-    }
-    return Stream.concat(
-      issueChangeData.getComponents().stream().filter(c -> projectUuids.contains(c.uuid())),
-      dbClient.componentDao().selectByUuids(dbSession, missingProjectUuids).stream())
-      .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
-      .collect(uniqueIndex(ComponentDto::uuid));
-  }
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/issue/webhook/package-info.java
deleted file mode 100644 (file)
index 9f828ad..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.server.issue.webhook;
-
-import javax.annotation.ParametersAreNonnullByDefault;
index 60ad453ba28adf4eff611d35b0d601901dfc269b..032cc02cd50d27fed41d16aff6922a87db36c141 100644 (file)
@@ -59,8 +59,8 @@ import org.sonar.server.issue.RemoveTagsAction;
 import org.sonar.server.issue.SetTypeAction;
 import org.sonar.server.issue.TransitionAction;
 import org.sonar.server.issue.notification.IssueChangeNotification;
-import org.sonar.server.issue.webhook.IssueChangeWebhook;
 import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Issues;
 
@@ -109,17 +109,17 @@ public class BulkChangeAction implements IssuesWsAction {
   private final IssueStorage issueStorage;
   private final NotificationManager notificationService;
   private final List<Action> actions;
-  private final IssueChangeWebhook issueChangeWebhook;
+  private final IssueChangeTrigger issueChangeTrigger;
 
   public BulkChangeAction(System2 system2, UserSession userSession, DbClient dbClient, IssueStorage issueStorage, NotificationManager notificationService, List<Action> actions,
-    IssueChangeWebhook issueChangeWebhook) {
+    IssueChangeTrigger issueChangeTrigger) {
     this.system2 = system2;
     this.userSession = userSession;
     this.dbClient = dbClient;
     this.issueStorage = issueStorage;
     this.notificationService = notificationService;
     this.actions = actions;
-    this.issueChangeWebhook = issueChangeWebhook;
+    this.issueChangeTrigger = issueChangeTrigger;
   }
 
   @Override
@@ -206,8 +206,8 @@ public class BulkChangeAction implements IssuesWsAction {
       issueStorage.save(items);
       items.forEach(sendNotification(issueChangeContext, bulkChangeData));
       buildWebhookIssueChange(bulkChangeData.propertiesByActions)
-        .ifPresent(issueChange -> issueChangeWebhook.onChange(
-          new IssueChangeWebhook.IssueChangeData(
+        .ifPresent(issueChange -> issueChangeTrigger.onChange(
+          new IssueChangeTrigger.IssueChangeData(
             bulkChangeData.issues.stream().filter(i -> result.success.contains(i.key())).collect(MoreCollectors.toList()),
             copyOf(bulkChangeData.componentsByUuid.values())),
           issueChange,
@@ -216,7 +216,7 @@ public class BulkChangeAction implements IssuesWsAction {
     };
   }
 
-  private static Optional<IssueChangeWebhook.IssueChange> buildWebhookIssueChange(Map<String, Map<String, Object>> propertiesByActions) {
+  private static Optional<IssueChangeTrigger.IssueChange> buildWebhookIssueChange(Map<String, Map<String, Object>> propertiesByActions) {
     RuleType ruleType = Optional.ofNullable(propertiesByActions.get(SetTypeAction.SET_TYPE_KEY))
       .map(t -> (String) t.get(SetTypeAction.TYPE_PARAMETER))
       .map(RuleType::valueOf)
@@ -227,7 +227,7 @@ public class BulkChangeAction implements IssuesWsAction {
     if (ruleType == null && transitionKey == null) {
       return Optional.empty();
     }
-    return Optional.of(new IssueChangeWebhook.IssueChange(ruleType, transitionKey));
+    return Optional.of(new IssueChangeTrigger.IssueChange(ruleType, transitionKey));
   }
 
   private static Predicate<DefaultIssue> bulkChange(IssueChangeContext issueChangeContext, BulkChangeData bulkChangeData, BulkChangeResult result) {
index 201ca3d98e529f4010f68202659e94be49194076..43a0d0056a774eb12c3f392ecc5a4edd4ddd1a5d 100644 (file)
@@ -37,7 +37,7 @@ import org.sonar.db.issue.IssueDto;
 import org.sonar.server.issue.IssueFinder;
 import org.sonar.server.issue.IssueUpdater;
 import org.sonar.server.issue.TransitionService;
-import org.sonar.server.issue.webhook.IssueChangeWebhook;
+import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger;
 import org.sonar.server.user.UserSession;
 
 import static com.google.common.collect.ImmutableList.copyOf;
@@ -54,10 +54,10 @@ public class DoTransitionAction implements IssuesWsAction {
   private final TransitionService transitionService;
   private final OperationResponseWriter responseWriter;
   private final System2 system2;
-  private final IssueChangeWebhook issueChangeWebhook;
+  private final IssueChangeTrigger issueChangeTrigger;
 
   public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService,
-    OperationResponseWriter responseWriter, System2 system2, IssueChangeWebhook issueChangeWebhook) {
+    OperationResponseWriter responseWriter, System2 system2, IssueChangeTrigger issueChangeTrigger) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.issueFinder = issueFinder;
@@ -65,7 +65,7 @@ public class DoTransitionAction implements IssuesWsAction {
     this.transitionService = transitionService;
     this.responseWriter = responseWriter;
     this.system2 = system2;
-    this.issueChangeWebhook = issueChangeWebhook;
+    this.issueChangeTrigger = issueChangeTrigger;
   }
 
   @Override
@@ -108,11 +108,11 @@ public class DoTransitionAction implements IssuesWsAction {
     transitionService.checkTransitionPermission(transitionKey, defaultIssue);
     if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
       SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null);
-      issueChangeWebhook.onChange(
-        new IssueChangeWebhook.IssueChangeData(
+      issueChangeTrigger.onChange(
+        new IssueChangeTrigger.IssueChangeData(
           searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(searchResponseData.getIssues().size())),
           copyOf(searchResponseData.getComponents())),
-        new IssueChangeWebhook.IssueChange(transitionKey),
+        new IssueChangeTrigger.IssueChange(transitionKey),
         context);
       return searchResponseData;
     }
index 6deec524da244a39af70613bf635a550e9512d9f..2c750380e4daeda0f339fab5c2232aa9b6cb873a 100644 (file)
@@ -26,10 +26,12 @@ import org.sonar.server.issue.IssueQueryFactory;
 import org.sonar.server.issue.IssueUpdater;
 import org.sonar.server.issue.ServerIssueStorage;
 import org.sonar.server.issue.TransitionService;
-import org.sonar.server.issue.webhook.IssueChangeWebhookImpl;
 import org.sonar.server.issue.workflow.FunctionExecutor;
 import org.sonar.server.issue.workflow.IssueWorkflow;
+import org.sonar.server.qualitygate.changeevent.IssueChangeTriggerImpl;
+import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl;
 import org.sonar.server.settings.ProjectConfigurationLoaderImpl;
+import org.sonar.server.webhook.WebhookQGChangeEventListener;
 import org.sonar.server.ws.WsResponseCommonFormat;
 
 public class IssueWsModule extends Module {
@@ -65,6 +67,8 @@ public class IssueWsModule extends Module {
       ChangelogAction.class,
       BulkChangeAction.class,
       ProjectConfigurationLoaderImpl.class,
-      IssueChangeWebhookImpl.class);
+      IssueChangeTriggerImpl.class,
+      WebhookQGChangeEventListener.class,
+      QGChangeEventListenersImpl.class);
   }
 }
index 6554cdac0c85eadf24f6d3a141f644ada70e6945..e30f64e749a73aae2c069386a8e09267e8666e8c 100644 (file)
@@ -37,7 +37,7 @@ import org.sonar.db.issue.IssueDto;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.IssueFinder;
 import org.sonar.server.issue.IssueUpdater;
-import org.sonar.server.issue.webhook.IssueChangeWebhook;
+import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger;
 import org.sonar.server.user.UserSession;
 
 import static com.google.common.collect.ImmutableList.copyOf;
@@ -55,10 +55,10 @@ public class SetTypeAction implements IssuesWsAction {
   private final IssueUpdater issueUpdater;
   private final OperationResponseWriter responseWriter;
   private final System2 system2;
-  private final IssueChangeWebhook issueChangeWebhook;
+  private final IssueChangeTrigger issueChangeTrigger;
 
   public SetTypeAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater,
-    OperationResponseWriter responseWriter, System2 system2, IssueChangeWebhook issueChangeWebhook) {
+    OperationResponseWriter responseWriter, System2 system2, IssueChangeTrigger issueChangeTrigger) {
     this.userSession = userSession;
     this.dbClient = dbClient;
     this.issueFinder = issueFinder;
@@ -66,7 +66,7 @@ public class SetTypeAction implements IssuesWsAction {
     this.issueUpdater = issueUpdater;
     this.responseWriter = responseWriter;
     this.system2 = system2;
-    this.issueChangeWebhook = issueChangeWebhook;
+    this.issueChangeTrigger = issueChangeTrigger;
   }
 
   @Override
@@ -116,11 +116,11 @@ public class SetTypeAction implements IssuesWsAction {
     IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
     if (issueFieldsSetter.setType(issue, ruleType, context)) {
       SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null);
-      issueChangeWebhook.onChange(
-        new IssueChangeWebhook.IssueChangeData(
+      issueChangeTrigger.onChange(
+        new IssueChangeTrigger.IssueChangeData(
           searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(searchResponseData.getIssues().size())),
           copyOf(searchResponseData.getComponents())),
-        new IssueChangeWebhook.IssueChange(ruleType),
+        new IssueChangeTrigger.IssueChange(ruleType),
         context);
       return searchResponseData;
     }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTrigger.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTrigger.java
new file mode 100644 (file)
index 0000000..4fe0b86
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.api.rules.RuleType;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.issue.ws.SearchResponseData;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public interface IssueChangeTrigger {
+  /**
+   * Will call webhooks once for any short living branch which has at least one issue in {@link SearchResponseData} and
+   * if change described in {@link IssueChange} can alter the status of the short living branch.
+   */
+  void onChange(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context);
+
+  final class IssueChange {
+    private final RuleType ruleType;
+    private final String transitionKey;
+
+    public IssueChange(RuleType ruleType) {
+      this(ruleType, null);
+    }
+
+    public IssueChange(String transitionKey) {
+      this(null, transitionKey);
+    }
+
+    public IssueChange(@Nullable RuleType ruleType, @Nullable String transitionKey) {
+      checkArgument(ruleType != null || transitionKey != null, "At least one of ruleType and transitionKey must be non null");
+      this.ruleType = ruleType;
+      this.transitionKey = transitionKey;
+    }
+
+    public Optional<RuleType> getRuleType() {
+      return Optional.ofNullable(ruleType);
+    }
+
+    public Optional<String> getTransitionKey() {
+      return Optional.ofNullable(transitionKey);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      IssueChange that = (IssueChange) o;
+      return ruleType == that.ruleType &&
+        Objects.equals(transitionKey, that.transitionKey);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(ruleType, transitionKey);
+    }
+
+    @Override
+    public String toString() {
+      return "IssueChange{" +
+        "ruleType=" + ruleType +
+        ", transitionKey='" + transitionKey + '\'' +
+        '}';
+    }
+  }
+
+  final class IssueChangeData {
+    private final List<DefaultIssue> issues;
+    private final List<ComponentDto> components;
+
+    public IssueChangeData(List<DefaultIssue> issues, List<ComponentDto> components) {
+      this.issues = ImmutableList.copyOf(issues);
+      this.components = ImmutableList.copyOf(components);
+    }
+
+    public List<DefaultIssue> getIssues() {
+      return issues;
+    }
+
+    public List<ComponentDto> getComponents() {
+      return components;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      IssueChangeData that = (IssueChangeData) o;
+      return Objects.equals(issues, that.issues) &&
+        Objects.equals(components, that.components);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(issues, components);
+    }
+
+    @Override
+    public String toString() {
+      return "IssueChangeData{" +
+        "issues=" + issues +
+        ", components=" + components +
+        '}';
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java
new file mode 100644 (file)
index 0000000..25af687
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.issue.DefaultTransitions;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.server.settings.ProjectConfigurationLoader;
+
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class IssueChangeTriggerImpl implements IssueChangeTrigger {
+  private static final Set<String> MEANINGFUL_TRANSITIONS = ImmutableSet.of(
+    DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
+  private final DbClient dbClient;
+  private final ProjectConfigurationLoader projectConfigurationLoader;
+  private final QGChangeEventListeners qgEventListeners;
+
+  public IssueChangeTriggerImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader, QGChangeEventListeners qgEventListeners) {
+    this.dbClient = dbClient;
+    this.projectConfigurationLoader = projectConfigurationLoader;
+    this.qgEventListeners = qgEventListeners;
+  }
+
+  @Override
+  public void onChange(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context) {
+    if (isEmpty(issueChangeData) || !isUserChangeContext(context) || !isRelevant(issueChange) || qgEventListeners.isEmpty()) {
+      return;
+    }
+
+    callWebHook(issueChangeData);
+  }
+
+  private static boolean isRelevant(IssueChange issueChange) {
+    return issueChange.getTransitionKey().map(IssueChangeTriggerImpl::isMeaningfulTransition).orElse(true);
+  }
+
+  private static boolean isEmpty(IssueChangeData issueChangeData) {
+    return issueChangeData.getIssues().isEmpty();
+  }
+
+  private static boolean isUserChangeContext(IssueChangeContext context) {
+    return context.login() != null;
+  }
+
+  private static boolean isMeaningfulTransition(String transitionKey) {
+    return MEANINGFUL_TRANSITIONS.contains(transitionKey);
+  }
+
+  private void callWebHook(IssueChangeData issueChangeData) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      Map<String, ComponentDto> branchesByUuid = getBranchComponents(dbSession, issueChangeData);
+      if (branchesByUuid.isEmpty()) {
+        return;
+      }
+
+      Set<String> branchProjectUuids = branchesByUuid.values().stream()
+        .map(ComponentDto::uuid)
+        .collect(toSet(branchesByUuid.size()));
+      Set<BranchDto> shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids)
+        .stream()
+        .filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT)
+        .collect(toSet(branchesByUuid.size()));
+      if (shortBranches.isEmpty()) {
+        return;
+      }
+
+      Map<String, Configuration> configurationByUuid = projectConfigurationLoader.loadProjectConfigurations(dbSession,
+        shortBranches.stream().map(shortBranch -> branchesByUuid.get(shortBranch.getUuid())).collect(Collectors.toSet()));
+      Set<String> shortBranchesComponentUuids = shortBranches.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size()));
+      Map<String, SnapshotDto> analysisByProjectUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(
+        dbSession,
+        shortBranchesComponentUuids)
+        .stream()
+        .collect(uniqueIndex(SnapshotDto::getComponentUuid));
+
+      List<QGChangeEvent> qgChangeEvents = shortBranches
+        .stream()
+        .map(shortBranch -> {
+          ComponentDto branch = branchesByUuid.get(shortBranch.getUuid());
+          SnapshotDto analysis = analysisByProjectUuid.get(shortBranch.getUuid());
+          if (branch != null && analysis != null) {
+            Configuration configuration = configurationByUuid.get(shortBranch.getUuid());
+
+            return new QGChangeEvent(branch, shortBranch, analysis, configuration);
+          }
+          return null;
+        })
+        .filter(Objects::nonNull)
+        .collect(MoreCollectors.toList(shortBranches.size()));
+      qgEventListeners.broadcast(Trigger.ISSUE_CHANGE, qgChangeEvents);
+    }
+  }
+
+  private Map<String, ComponentDto> getBranchComponents(DbSession dbSession, IssueChangeData issueChangeData) {
+    Set<String> projectUuids = issueChangeData.getIssues().stream()
+      .map(DefaultIssue::projectUuid)
+      .collect(toSet());
+    Set<String> missingProjectUuids = ImmutableSet.copyOf(Sets.difference(
+      projectUuids,
+      issueChangeData.getComponents()
+        .stream()
+        .map(ComponentDto::uuid)
+        .collect(Collectors.toSet())));
+    if (missingProjectUuids.isEmpty()) {
+      return issueChangeData.getComponents()
+        .stream()
+        .filter(c -> projectUuids.contains(c.uuid()))
+        .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
+        .collect(uniqueIndex(ComponentDto::uuid));
+    }
+    return Stream.concat(
+      issueChangeData.getComponents().stream().filter(c -> projectUuids.contains(c.uuid())),
+      dbClient.componentDao().selectByUuids(dbSession, missingProjectUuids).stream())
+      .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
+      .collect(uniqueIndex(ComponentDto::uuid));
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java
new file mode 100644 (file)
index 0000000..a3d82c6
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.config.Configuration;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+
+public class QGChangeEvent {
+  private final ComponentDto project;
+  private final BranchDto branch;
+  private final SnapshotDto analysis;
+  private final Configuration projectConfiguration;
+
+  public QGChangeEvent(ComponentDto project, BranchDto branch, SnapshotDto analysis, Configuration projectConfiguration) {
+    this.branch = branch;
+    this.project = project;
+    this.analysis = analysis;
+    this.projectConfiguration = projectConfiguration;
+  }
+
+  public BranchDto getBranch() {
+    return branch;
+  }
+
+  public ComponentDto getProject() {
+    return project;
+  }
+
+  public SnapshotDto getAnalysis() {
+    return analysis;
+  }
+
+  public Configuration getProjectConfiguration() {
+    return projectConfiguration;
+  }
+
+  @Override
+  public String toString() {
+    return "QGChangeEvent{" +
+      "branch=" + toString(branch) +
+      ", project=" + toString(project) +
+      ", analysis=" + toString(analysis) +
+      ", projectConfiguration=" + projectConfiguration +
+      '}';
+  }
+
+  @CheckForNull
+  private static String toString(@Nullable BranchDto shortBranch) {
+    if (shortBranch == null) {
+      return null;
+    }
+    return shortBranch.getBranchType() + ":" + shortBranch.getUuid() + ":" + shortBranch.getProjectUuid() + ":" + shortBranch.getMergeBranchUuid();
+  }
+
+  @CheckForNull
+  private static String toString(@Nullable ComponentDto shortBranchComponent) {
+    if (shortBranchComponent == null) {
+      return null;
+    }
+    return shortBranchComponent.uuid() + ":" + shortBranchComponent.getKey();
+  }
+
+  @CheckForNull
+  private static String toString(@Nullable SnapshotDto latestAnalysis) {
+    if (latestAnalysis == null) {
+      return null;
+    }
+    return latestAnalysis.getUuid() + ":" + latestAnalysis.getCreatedAt();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java
new file mode 100644 (file)
index 0000000..8927602
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+import java.util.Collection;
+
+public interface QGChangeEventListener {
+  void onChanges(Trigger trigger, Collection<QGChangeEvent> changeEvents);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
new file mode 100644 (file)
index 0000000..50065d9
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+import java.util.Collection;
+
+public interface QGChangeEventListeners {
+  boolean isEmpty();
+
+  void broadcast(Trigger trigger, Collection<QGChangeEvent> changeEvents);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
new file mode 100644 (file)
index 0000000..1c1d3d8
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+import java.util.Arrays;
+import java.util.Collection;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+
+/**
+ * Broadcast a given collection of {@link QGChangeEvent} for a specific trigger to all the registered
+ * {@link QGChangeEventListener} in Pico.
+ *
+ * This class ensures that an {@link Exception} occurring calling one of the {@link QGChangeEventListener} doesn't
+ * prevent from calling the others.
+ */
+public class QGChangeEventListenersImpl implements QGChangeEventListeners {
+  private static final Logger LOG = Loggers.get(QGChangeEventListenersImpl.class);
+
+  private final QGChangeEventListener[] listeners;
+
+  /**
+   * Used by Pico when there is no QGChangeEventListener instance in container.
+   */
+  public QGChangeEventListenersImpl() {
+    this.listeners = new QGChangeEventListener[0];
+  }
+
+  public QGChangeEventListenersImpl(QGChangeEventListener[] listeners) {
+    this.listeners = listeners;
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return listeners.length == 0;
+  }
+
+  @Override
+  public void broadcast(Trigger trigger, Collection<QGChangeEvent> changeEvents) {
+    try {
+      Arrays.stream(listeners).forEach(listener -> broadcastTo(trigger, changeEvents, listener));
+    } catch (Error e) {
+      LOG.warn(format("Broadcasting to listeners failed for %s events", changeEvents.size()), e);
+    }
+  }
+
+  private void broadcastTo(Trigger trigger, Collection<QGChangeEvent> changeEvents, QGChangeEventListener listener) {
+    try {
+      LOG.debug("calling onChange() on listener {} for events {}...", listener.getClass().getName(), changeEvents);
+      listener.onChanges(trigger, changeEvents);
+    } catch (Exception e) {
+      LOG.warn(format("onChange() call failed on listener %s for events %s", listener.getClass().getName(), changeEvents), e);
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/Trigger.java
new file mode 100644 (file)
index 0000000..6a57448
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+public enum Trigger {
+  ISSUE_CHANGE
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/package-info.java
new file mode 100644 (file)
index 0000000..ac361c6
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.qualitygate.changeevent;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java
new file mode 100644 (file)
index 0000000..4f3c6ba
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.webhook;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.elasticsearch.action.search.SearchResponse;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.AnalysisPropertyDto;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.qualitygate.QualityGateConditionDto;
+import org.sonar.server.es.Facets;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.issue.IssueQuery;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.qualitygate.Condition;
+import org.sonar.server.qualitygate.EvaluatedCondition;
+import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
+import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
+import org.sonar.server.qualitygate.changeevent.QGChangeEventListener;
+import org.sonar.server.qualitygate.changeevent.Trigger;
+import org.sonar.server.rule.index.RuleIndex;
+import org.sonar.server.settings.ProjectConfigurationLoader;
+import org.sonar.server.webhook.Analysis;
+import org.sonar.server.webhook.Branch;
+import org.sonar.server.webhook.Project;
+import org.sonar.server.webhook.ProjectAnalysis;
+import org.sonar.server.webhook.WebHooks;
+import org.sonar.server.webhook.WebhookPayload;
+import org.sonar.server.webhook.WebhookPayloadFactory;
+
+import static java.lang.String.format;
+import static java.lang.String.valueOf;
+import static java.util.Collections.singletonList;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+
+public class WebhookQGChangeEventListener implements QGChangeEventListener {
+  private final WebHooks webhooks;
+  private final WebhookPayloadFactory webhookPayloadFactory;
+  private final IssueIndex issueIndex;
+  private final DbClient dbClient;
+  private final System2 system2;
+
+  public WebhookQGChangeEventListener(WebHooks webhooks, WebhookPayloadFactory webhookPayloadFactory, IssueIndex issueIndex, DbClient dbClient, System2 system2) {
+    this.webhooks = webhooks;
+    this.webhookPayloadFactory = webhookPayloadFactory;
+    this.issueIndex = issueIndex;
+    this.dbClient = dbClient;
+    this.system2 = system2;
+  }
+
+  @Override
+  public void onChanges(Trigger trigger, Collection<QGChangeEvent> changeEvents) {
+    if (changeEvents.isEmpty()) {
+      return;
+    }
+
+    List<QGChangeEvent> branchesWithWebhooks = changeEvents.stream()
+      .filter(changeEvent -> webhooks.isEnabled(changeEvent.getProjectConfiguration()))
+      .collect(MoreCollectors.toList());
+    if (branchesWithWebhooks.isEmpty()) {
+      return;
+    }
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      branchesWithWebhooks.forEach(event -> callWebhook(dbSession, event));
+    }
+  }
+
+  private void callWebhook(DbSession dbSession, QGChangeEvent event) {
+    webhooks.sendProjectAnalysisUpdate(
+      event.getProjectConfiguration(),
+      new WebHooks.Analysis(event.getBranch().getUuid(), event.getAnalysis().getUuid(), null),
+      () -> buildWebHookPayload(dbSession, event.getProject(), event.getBranch(), event.getAnalysis()));
+  }
+
+  private WebhookPayload buildWebHookPayload(DbSession dbSession, ComponentDto branch, BranchDto shortBranch, SnapshotDto analysis) {
+    Map<String, String> analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid())
+      .stream()
+      .collect(Collectors.toMap(AnalysisPropertyDto::getKey, AnalysisPropertyDto::getValue));
+    ProjectAnalysis projectAnalysis = new ProjectAnalysis(
+      new Project(branch.getMainBranchProjectUuid(), branch.getKey(), branch.name()),
+      null,
+      new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
+      new Branch(false, shortBranch.getKey(), Branch.Type.SHORT),
+      createQualityGate(branch, issueIndex),
+      null,
+      analysisProperties);
+    return webhookPayloadFactory.create(projectAnalysis);
+  }
+
+  private EvaluatedQualityGate createQualityGate(ComponentDto branch, IssueIndex issueIndex) {
+    SearchResponse searchResponse = issueIndex.search(IssueQuery.builder()
+      .projectUuids(singletonList(branch.getMainBranchProjectUuid()))
+      .branchUuid(branch.uuid())
+      .mainBranch(false)
+      .resolved(false)
+      .checkAuthorization(false)
+      .build(),
+      new SearchOptions().addFacets(RuleIndex.FACET_TYPES));
+    LinkedHashMap<String, Long> typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone())
+      .get(RuleIndex.FACET_TYPES);
+
+    EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder();
+    Set<Condition> conditions = ShortLivingBranchQualityGate.CONDITIONS.stream()
+      .map(c -> {
+        long measure = getMeasure(typeFacet, c);
+        EvaluationStatus status = measure > 0 ? EvaluationStatus.ERROR : EvaluationStatus.OK;
+        Condition condition = new Condition(c.getMetricKey(), toOperator(c), c.getErrorThreshold(), c.getWarnThreshold(), c.isOnLeak());
+        builder.addCondition(condition, status, valueOf(measure));
+        return condition;
+      })
+      .collect(toSet(ShortLivingBranchQualityGate.CONDITIONS.size()));
+    builder
+      .setQualityGate(
+        new org.sonar.server.qualitygate.QualityGate(
+          valueOf(ShortLivingBranchQualityGate.ID),
+          ShortLivingBranchQualityGate.NAME,
+          conditions))
+      .setStatus(qgStatusFrom(builder.getEvaluatedConditions()));
+
+    return builder.build();
+  }
+
+  private static Condition.Operator toOperator(ShortLivingBranchQualityGate.Condition c) {
+    String operator = c.getOperator();
+    switch (operator) {
+      case QualityGateConditionDto.OPERATOR_GREATER_THAN:
+        return Condition.Operator.GREATER_THAN;
+      case QualityGateConditionDto.OPERATOR_LESS_THAN:
+        return Condition.Operator.LESS_THAN;
+      case QualityGateConditionDto.OPERATOR_EQUALS:
+        return Condition.Operator.EQUALS;
+      case QualityGateConditionDto.OPERATOR_NOT_EQUALS:
+        return Condition.Operator.NOT_EQUALS;
+      default:
+        throw new IllegalArgumentException(format("Unsupported Condition operator '%s'", operator));
+    }
+  }
+
+  private static EvaluatedQualityGate.Status qgStatusFrom(Set<EvaluatedCondition> conditions) {
+    if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluationStatus.ERROR)) {
+      return EvaluatedQualityGate.Status.ERROR;
+    }
+    return EvaluatedQualityGate.Status.OK;
+  }
+
+  private static long getMeasure(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) {
+    String metricKey = c.getMetricKey();
+    switch (metricKey) {
+      case CoreMetrics.BUGS_KEY:
+        return getValueForRuleType(typeFacet, RuleType.BUG);
+      case CoreMetrics.VULNERABILITIES_KEY:
+        return getValueForRuleType(typeFacet, RuleType.VULNERABILITY);
+      case CoreMetrics.CODE_SMELLS_KEY:
+        return getValueForRuleType(typeFacet, RuleType.CODE_SMELL);
+      default:
+        throw new IllegalArgumentException(format("Unsupported metric key '%s' in hardcoded quality gate", metricKey));
+    }
+  }
+
+  private static long getValueForRuleType(Map<String, Long> facet, RuleType ruleType) {
+    Long res = facet.get(ruleType.name());
+    if (res == null) {
+      return 0L;
+    }
+    return res;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookImplTest.java
deleted file mode 100644 (file)
index 94c25b6..0000000
+++ /dev/null
@@ -1,835 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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.webhook;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.tngtech.java.junit.dataprovider.DataProvider;
-import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Random;
-import java.util.Set;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-import javax.annotation.Nullable;
-import org.assertj.core.groups.Tuple;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.issue.DefaultTransitions;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.rules.RuleType;
-import org.sonar.api.utils.System2;
-import org.sonar.core.issue.IssueChangeContext;
-import org.sonar.core.util.UuidFactoryFast;
-import org.sonar.core.util.stream.MoreCollectors;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.DbTester;
-import org.sonar.db.component.AnalysisPropertyDto;
-import org.sonar.db.component.BranchDao;
-import org.sonar.db.component.BranchType;
-import org.sonar.db.component.ComponentDao;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.ComponentTesting;
-import org.sonar.db.component.SnapshotDao;
-import org.sonar.db.component.SnapshotDto;
-import org.sonar.db.issue.IssueDto;
-import org.sonar.db.organization.OrganizationDto;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleTesting;
-import org.sonar.server.es.EsTester;
-import org.sonar.server.issue.index.IssueIndex;
-import org.sonar.server.issue.index.IssueIndexDefinition;
-import org.sonar.server.issue.index.IssueIndexer;
-import org.sonar.server.issue.index.IssueIteratorFactory;
-import org.sonar.server.issue.webhook.IssueChangeWebhook.IssueChange;
-import org.sonar.server.permission.index.AuthorizationTypeSupport;
-import org.sonar.server.qualitygate.Condition;
-import org.sonar.server.qualitygate.EvaluatedCondition;
-import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
-import org.sonar.server.qualitygate.EvaluatedQualityGate;
-import org.sonar.server.qualitygate.QualityGate;
-import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
-import org.sonar.server.settings.ProjectConfigurationLoader;
-import org.sonar.server.tester.UserSessionRule;
-import org.sonar.server.webhook.Analysis;
-import org.sonar.server.webhook.Branch;
-import org.sonar.server.webhook.Project;
-import org.sonar.server.webhook.ProjectAnalysis;
-import org.sonar.server.webhook.WebHooks;
-import org.sonar.server.webhook.WebhookPayload;
-import org.sonar.server.webhook.WebhookPayloadFactory;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static java.lang.String.valueOf;
-import static java.util.Arrays.asList;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singleton;
-import static java.util.Collections.singletonList;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyCollectionOf;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.same;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
-import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY;
-import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
-import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
-import static org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN;
-
-@RunWith(DataProviderRunner.class)
-public class IssueChangeWebhookImplTest {
-  private static final List<String> OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED);
-  private static final List<String> NON_OPEN_STATUSES = Issue.STATUSES.stream().filter(OPEN_STATUSES::contains).collect(MoreCollectors.toList());
-
-  @Rule
-  public DbTester dbTester = DbTester.create(System2.INSTANCE);
-  @Rule
-  public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));
-  @Rule
-  public UserSessionRule userSessionRule = UserSessionRule.standalone();
-
-  private DbClient dbClient = dbTester.getDbClient();
-
-  private Random random = new Random();
-  private String randomResolution = Issue.RESOLUTIONS.get(random.nextInt(Issue.RESOLUTIONS.size()));
-  private String randomOpenStatus = OPEN_STATUSES.get(random.nextInt(OPEN_STATUSES.size()));
-  private String randomNonOpenStatus = NON_OPEN_STATUSES.get(random.nextInt(NON_OPEN_STATUSES.size()));
-  private RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)];
-
-  private IssueChangeContext scanChangeContext = IssueChangeContext.createScan(new Date());
-  private IssueChangeContext userChangeContext = IssueChangeContext.createUser(new Date(), "userLogin");
-  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient()));
-  private WebHooks webHooks = mock(WebHooks.class);
-  private WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class);
-  private IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule));
-  private DbClient spiedOnDbClient = spy(dbClient);
-  private ProjectConfigurationLoader projectConfigurationLoader = mock(ProjectConfigurationLoader.class);
-  private IssueChangeWebhookImpl underTest = new IssueChangeWebhookImpl(spiedOnDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, issueIndex, System2.INSTANCE);
-  private DbClient mockedDbClient = mock(DbClient.class);
-  private IssueIndex spiedOnIssueIndex = spy(issueIndex);
-  private IssueChangeWebhookImpl mockedUnderTest = new IssueChangeWebhookImpl(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex,
-    System2.INSTANCE);
-
-  @Test
-  public void on_type_change_has_no_effect_if_SearchResponseData_has_no_issue() {
-    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType), userChangeContext);
-
-    verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex);
-  }
-
-  @Test
-  public void on_type_change_has_no_effect_if_scan_IssueChangeContext() {
-    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType), scanChangeContext);
-
-    verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex);
-  }
-
-  @Test
-  public void on_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() {
-    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomAlphanumeric(12)), userChangeContext);
-
-    verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex);
-  }
-
-  @Test
-  public void onTransition_has_no_effect_if_transition_key_is_empty() {
-    on_transition_changeHasNoEffectForTransitionKey("");
-  }
-
-  @Test
-  public void onTransition_has_no_effect_if_transition_key_is_random() {
-    on_transition_changeHasNoEffectForTransitionKey(randomAlphanumeric(99));
-  }
-
-  @Test
-  public void on_transition_change_has_no_effect_if_transition_key_is_ignored_default_transition_key() {
-    Set<String> supportedDefaultTransitionKeys = ImmutableSet.of(
-      DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
-    DefaultTransitions.ALL.stream()
-      .filter(s -> !supportedDefaultTransitionKeys.contains(s))
-      .forEach(this::on_transition_changeHasNoEffectForTransitionKey);
-  }
-
-  private void on_transition_changeHasNoEffectForTransitionKey(@Nullable String transitionKey) {
-    reset(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory);
-
-    mockedUnderTest.onChange(issueChangeData(newIssueDto(null)), new IssueChange(transitionKey), userChangeContext);
-
-    verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex);
-  }
-
-  @Test
-  public void on_transition_change_has_no_effect_if_scan_IssueChangeContext() {
-    mockedUnderTest.onChange(issueChangeData(newIssueDto(null)), new IssueChange(randomAlphanumeric(12)), scanChangeContext);
-
-    verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex);
-  }
-
-  @Test
-  public void on_type_and_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() {
-    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), userChangeContext);
-
-    verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex);
-  }
-
-  @Test
-  public void on_type_and_transition_change_has_no_effect_if_scan_IssueChangeContext() {
-    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), scanChangeContext);
-
-    verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex);
-  }
-
-  @Test
-  public void on_type_and_transition_has_no_effect_if_transition_key_is_empty() {
-    on_type_and_transition_changeHasNoEffectForTransitionKey("");
-  }
-
-  @Test
-  public void on_type_and_transition_has_no_effect_if_transition_key_is_random() {
-    on_type_and_transition_changeHasNoEffectForTransitionKey(randomAlphanumeric(66));
-  }
-
-  @Test
-  public void on_type_and_transition_has_no_effect_if_transition_key_is_ignored_default_transition_key() {
-    Set<String> supportedDefaultTransitionKeys = ImmutableSet.of(
-      DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
-    DefaultTransitions.ALL.stream()
-      .filter(s -> !supportedDefaultTransitionKeys.contains(s))
-      .forEach(this::on_type_and_transition_changeHasNoEffectForTransitionKey);
-  }
-
-  private void on_type_and_transition_changeHasNoEffectForTransitionKey(@Nullable String transitionKey) {
-    reset(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory);
-
-    mockedUnderTest.onChange(issueChangeData(newIssueDto(null)), new IssueChange(randomRuleType, transitionKey), userChangeContext);
-
-    verifyZeroInteractions(mockedDbClient, webHooks, projectConfigurationLoader, webhookPayloadFactory, spiedOnIssueIndex);
-  }
-
-  @Test
-  @UseDataProvider("validIssueChanges")
-  public void call_webhook_for_short_living_branch_of_issue(IssueChange issueChange) {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPublicProject(organization);
-    ComponentDto branch = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.SHORT)
-      .setKey("foo"));
-    SnapshotDto analysis = insertAnalysisTask(branch);
-    Configuration configuration = mockLoadProjectConfiguration(branch);
-    mockWebhookEnabled(configuration);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    Map<String, String> properties = new HashMap<>();
-    properties.put("sonar.analysis.test1", randomAlphanumeric(50));
-    properties.put("sonar.analysis.test2", randomAlphanumeric(5000));
-    insertPropertiesFor(analysis.getUuid(), properties);
-
-    underTest.onChange(issueChangeData(newIssueDto(branch)), issueChange, userChangeContext);
-
-    ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
-    Condition condition1 = new Condition(BUGS_KEY, GREATER_THAN, "0", null, false);
-    Condition condition2 = new Condition(VULNERABILITIES_KEY, GREATER_THAN, "0", null, false);
-    Condition condition3 = new Condition(CODE_SMELLS_KEY, GREATER_THAN, "0", null, false);
-    assertThat(projectAnalysis).isEqualTo(
-      new ProjectAnalysis(
-        new Project(project.uuid(), project.getKey(), project.name()),
-        null,
-        new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
-        new Branch(false, "foo", Branch.Type.SHORT),
-        EvaluatedQualityGate.newBuilder()
-          .setQualityGate(new QualityGate(
-            valueOf(ShortLivingBranchQualityGate.ID),
-            ShortLivingBranchQualityGate.NAME,
-            ImmutableSet.of(condition1, condition2, condition3)))
-          .setStatus(EvaluatedQualityGate.Status.OK)
-          .addCondition(condition1, EvaluationStatus.OK, "0")
-          .addCondition(condition2, EvaluationStatus.OK, "0")
-          .addCondition(condition3, EvaluationStatus.OK, "0")
-          .build(),
-        null,
-        properties));
-  }
-
-  @Test
-  @UseDataProvider("validIssueChanges")
-  public void do_not_retrieve_analysis_nor_call_webhook_if_webhook_are_disabled_for_short_branch(IssueChange issueChange) {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPublicProject(organization);
-    ComponentDto branch1 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.SHORT)
-      .setKey("foo"));
-    ComponentDto branch2 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.SHORT)
-      .setKey("bar"));
-    SnapshotDto analysis2 = insertAnalysisTask(branch2);
-    Configuration configuration1 = mock(Configuration.class);
-    Configuration configuration2 = mock(Configuration.class);
-    mockLoadProjectConfigurations(
-      branch1, configuration1,
-      branch2, configuration2);
-    mockWebhookDisabled(configuration1);
-    mockWebhookEnabled(configuration2);
-    mockPayloadSupplierConsumedByWebhooks();
-    ImmutableList<IssueDto> issueDtos = ImmutableList.of(newIssueDto(branch1), newIssueDto(branch2));
-
-    SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao());
-    when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
-    underTest.onChange(issueChangeData(issueDtos), issueChange, userChangeContext);
-
-    verifyWebhookCalled(branch2, configuration2, analysis2);
-    verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(singleton(branch2.uuid())));
-  }
-
-  @Test
-  @UseDataProvider("validIssueChanges")
-  public void do_not_load_project_configuration_nor_analysis_nor_call_webhook_if_there_are_no_short_branch(IssueChange issueChange) {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPublicProject(organization);
-    ComponentDto longBranch1 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.LONG)
-      .setKey("foo"));
-    ComponentDto longBranch2 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.LONG)
-      .setKey("bar"));
-    ImmutableList<IssueDto> issueDtos = ImmutableList.of(newIssueDto(project), newIssueDto(longBranch1), newIssueDto(longBranch2));
-
-    SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao());
-    when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
-    underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext);
-
-    verifyZeroInteractions(projectConfigurationLoader, webHooks);
-    verify(snapshotDaoSpy, times(0)).selectLastAnalysesByRootComponentUuids(any(DbSession.class), anyCollectionOf(String.class));
-  }
-
-  @Test
-  @UseDataProvider("validIssueChanges")
-  public void do_not_load_analysis_nor_call_webhook_if_there_no_short_branch_with_enabled_webhook(IssueChange issueChange) {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPublicProject(organization);
-    ComponentDto branch1 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.SHORT)
-      .setKey("foo"));
-    ComponentDto branch2 = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.SHORT)
-      .setKey("bar"));
-    Configuration configuration1 = mock(Configuration.class);
-    Configuration configuration2 = mock(Configuration.class);
-    mockLoadProjectConfigurations(
-      branch1, configuration1,
-      branch2, configuration2);
-    mockWebhookDisabled(configuration1, configuration2);
-    mockPayloadSupplierConsumedByWebhooks();
-    ImmutableList<IssueDto> issueDtos = ImmutableList.of(newIssueDto(branch1), newIssueDto(branch2));
-
-    SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao());
-    when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
-    underTest.onChange(issueChangeData(issueDtos), issueChange, userChangeContext);
-
-    verify(webHooks).isEnabled(configuration1);
-    verify(webHooks).isEnabled(configuration2);
-    verify(webHooks, times(0)).sendProjectAnalysisUpdate(any(), any(), any());
-    verify(snapshotDaoSpy, times(0)).selectLastAnalysesByRootComponentUuids(any(DbSession.class), anyCollectionOf(String.class));
-  }
-
-  @Test
-  public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto branch = insertPrivateBranch(organization, BranchType.SHORT);
-    SnapshotDto analysis = insertAnalysisTask(branch);
-    Configuration configuration = mockLoadProjectConfiguration(branch);
-    mockWebhookEnabled(configuration);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    underTest.onChange(issueChangeData(newIssueDto(branch)), new IssueChange(randomRuleType), userChangeContext);
-
-    ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
-    EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
-    assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.OK);
-    assertThat(qualityGate.getEvaluatedConditions())
-      .extracting(EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
-      .containsOnly(Tuple.tuple(EvaluationStatus.OK, Optional.of("0")));
-  }
-
-  @Test
-  public void computes_QG_error_if_there_is_one_unresolved_bug_issue_in_index_ignoring_permissions() {
-    int unresolvedIssues = 1 + random.nextInt(10);
-
-    computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
-      unresolvedIssues,
-      RuleType.BUG,
-      Tuple.tuple(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
-      Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")),
-      Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.OK, Optional.of("0")));
-  }
-
-  @Test
-  public void computes_QG_error_if_there_is_one_unresolved_vulnerability_issue_in_index_ignoring_permissions() {
-    int unresolvedIssues = 1 + random.nextInt(10);
-
-    computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
-      unresolvedIssues,
-      RuleType.VULNERABILITY,
-      Tuple.tuple(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")),
-      Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
-      Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.OK, Optional.of("0")));
-  }
-
-  @Test
-  public void computes_QG_error_if_there_is_one_unresolved_codeSmell_issue_in_index_ignoring_permissions() {
-    int unresolvedIssues = 1 + random.nextInt(10);
-
-    computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
-      unresolvedIssues,
-      RuleType.CODE_SMELL,
-      Tuple.tuple(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")),
-      Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")),
-      Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))));
-  }
-
-  private void computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
-    int unresolvedIssues, RuleType ruleType, Tuple... expectedQGConditions) {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto branch = insertPrivateBranch(organization, BranchType.SHORT);
-    SnapshotDto analysis = insertAnalysisTask(branch);
-    IntStream.range(0, unresolvedIssues).forEach(i -> insertIssue(branch, ruleType, randomOpenStatus, null));
-    IntStream.range(0, random.nextInt(10)).forEach(i -> insertIssue(branch, ruleType, randomNonOpenStatus, randomResolution));
-    indexIssues(branch);
-    Configuration configuration = mockLoadProjectConfiguration(branch);
-    mockWebhookEnabled(configuration);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    underTest.onChange(issueChangeData(newIssueDto(branch)), new IssueChange(randomRuleType), userChangeContext);
-
-    ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
-    EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
-    assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
-    assertThat(qualityGate.getEvaluatedConditions())
-      .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
-      .containsOnly(expectedQGConditions);
-  }
-
-  @Test
-  public void computes_QG_error_with_all_failing_conditions() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto branch = insertPrivateBranch(organization, BranchType.SHORT);
-    SnapshotDto analysis = insertAnalysisTask(branch);
-    int unresolvedBugs = 1 + random.nextInt(10);
-    int unresolvedVulnerabilities = 1 + random.nextInt(10);
-    int unresolvedCodeSmells = 1 + random.nextInt(10);
-    IntStream.range(0, unresolvedBugs).forEach(i -> insertIssue(branch, RuleType.BUG, randomOpenStatus, null));
-    IntStream.range(0, unresolvedVulnerabilities).forEach(i -> insertIssue(branch, RuleType.VULNERABILITY, randomOpenStatus, null));
-    IntStream.range(0, unresolvedCodeSmells).forEach(i -> insertIssue(branch, RuleType.CODE_SMELL, randomOpenStatus, null));
-    indexIssues(branch);
-    Configuration configuration = mockLoadProjectConfiguration(branch);
-    mockWebhookEnabled(configuration);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    underTest.onChange(issueChangeData(newIssueDto(branch)), new IssueChange(randomRuleType), userChangeContext);
-
-    ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
-    EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
-    assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
-    assertThat(qualityGate.getEvaluatedConditions())
-      .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
-      .containsOnly(
-        Tuple.tuple(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedBugs))),
-        Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedVulnerabilities))),
-        Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedCodeSmells))));
-  }
-
-  @Test
-  public void call_webhook_only_once_per_short_branch_with_at_least_one_issue_in_SearchResponseData() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto branch1 = insertPrivateBranch(organization, BranchType.SHORT);
-    ComponentDto branch2 = insertPrivateBranch(organization, BranchType.SHORT);
-    ComponentDto branch3 = insertPrivateBranch(organization, BranchType.SHORT);
-    SnapshotDto analysis1 = insertAnalysisTask(branch1);
-    SnapshotDto analysis2 = insertAnalysisTask(branch2);
-    SnapshotDto analysis3 = insertAnalysisTask(branch3);
-    int issuesBranch1 = 2 + random.nextInt(10);
-    int issuesBranch2 = 2 + random.nextInt(10);
-    int issuesBranch3 = 2 + random.nextInt(10);
-    List<IssueDto> issueDtos = Stream.of(
-      IntStream.range(0, issuesBranch1).mapToObj(i -> newIssueDto(branch1)),
-      IntStream.range(0, issuesBranch2).mapToObj(i -> newIssueDto(branch2)),
-      IntStream.range(0, issuesBranch3).mapToObj(i -> newIssueDto(branch3)))
-      .flatMap(s -> s)
-      .collect(MoreCollectors.toList());
-    Configuration configuration1 = mock(Configuration.class);
-    Configuration configuration2 = mock(Configuration.class);
-    Configuration configuration3 = mock(Configuration.class);
-    mockLoadProjectConfigurations(branch1, configuration1,
-      branch2, configuration2,
-      branch3, configuration3);
-    mockWebhookEnabled(configuration1, configuration2, configuration3);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    ComponentDao componentDaoSpy = spy(dbClient.componentDao());
-    BranchDao branchDaoSpy = spy(dbClient.branchDao());
-    SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao());
-    when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
-    when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy);
-    when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
-    underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext);
-
-    verifyWebhookCalled(branch1, configuration1, analysis1);
-    verifyWebhookCalled(branch2, configuration2, analysis2);
-    verifyWebhookCalled(branch3, configuration3, analysis3);
-    extractPayloadFactoryArguments(3);
-
-    Set<String> uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid());
-    verify(componentDaoSpy).selectByUuids(any(DbSession.class), eq(uuids));
-    verify(branchDaoSpy).selectByUuids(any(DbSession.class), eq(uuids));
-    verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(uuids));
-    verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy);
-  }
-
-  @Test
-  public void call_webhood_only_for_short_branches() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto shortBranch = insertPrivateBranch(organization, BranchType.SHORT);
-    ComponentDto longBranch = insertPrivateBranch(organization, BranchType.LONG);
-    SnapshotDto analysis1 = insertAnalysisTask(shortBranch);
-    SnapshotDto analysis2 = insertAnalysisTask(longBranch);
-    Configuration configuration = mockLoadProjectConfiguration(shortBranch);
-    mockWebhookEnabled(configuration);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    ComponentDao componentDaoSpy = spy(dbClient.componentDao());
-    BranchDao branchDaoSpy = spy(dbClient.branchDao());
-    SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao());
-    when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
-    when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy);
-    when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
-    underTest.onChange(
-      issueChangeData(asList(newIssueDto(shortBranch), newIssueDto(longBranch))),
-      new IssueChange(randomRuleType),
-      userChangeContext);
-
-    verifyWebhookCalledAndExtractPayloadFactoryArgument(shortBranch, configuration, analysis1);
-
-    Set<String> uuids = ImmutableSet.of(shortBranch.uuid(), longBranch.uuid());
-    verify(componentDaoSpy).selectByUuids(any(DbSession.class), eq(uuids));
-    verify(branchDaoSpy).selectByUuids(any(DbSession.class), eq(uuids));
-    verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(ImmutableSet.of(shortBranch.uuid())));
-    verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy);
-  }
-
-  @Test
-  public void do_not_load_componentDto_from_DB_if_all_are_in_inputData() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto branch1 = insertPrivateBranch(organization, BranchType.SHORT);
-    ComponentDto branch2 = insertPrivateBranch(organization, BranchType.SHORT);
-    ComponentDto branch3 = insertPrivateBranch(organization, BranchType.SHORT);
-    SnapshotDto analysis1 = insertAnalysisTask(branch1);
-    SnapshotDto analysis2 = insertAnalysisTask(branch2);
-    SnapshotDto analysis3 = insertAnalysisTask(branch3);
-    List<IssueDto> issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3));
-    Configuration configuration1 = mock(Configuration.class);
-    Configuration configuration2 = mock(Configuration.class);
-    Configuration configuration3 = mock(Configuration.class);
-    mockLoadProjectConfigurations(
-      branch1, configuration1,
-      branch2, configuration2,
-      branch3, configuration3);
-    mockWebhookEnabled(configuration1, configuration2, configuration3);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    ComponentDao componentDaoSpy = spy(dbClient.componentDao());
-    when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
-    underTest.onChange(
-      issueChangeData(issueDtos, branch1, branch2, branch3),
-      new IssueChange(randomRuleType),
-      userChangeContext);
-
-    verifyWebhookCalled(branch1, configuration1, analysis1);
-    verifyWebhookCalled(branch2, configuration2, analysis2);
-    verifyWebhookCalled(branch3, configuration3, analysis3);
-
-    verify(componentDaoSpy, times(0)).selectByUuids(any(DbSession.class), anyCollectionOf(String.class));
-    verifyNoMoreInteractions(componentDaoSpy);
-  }
-
-  @Test
-  public void call_db_only_for_componentDto_not_in_inputData() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto branch1 = insertPrivateBranch(organization, BranchType.SHORT);
-    ComponentDto branch2 = insertPrivateBranch(organization, BranchType.SHORT);
-    ComponentDto branch3 = insertPrivateBranch(organization, BranchType.SHORT);
-    SnapshotDto analysis1 = insertAnalysisTask(branch1);
-    SnapshotDto analysis2 = insertAnalysisTask(branch2);
-    SnapshotDto analysis3 = insertAnalysisTask(branch3);
-    List<IssueDto> issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3));
-    Configuration configuration1 = mock(Configuration.class);
-    Configuration configuration2 = mock(Configuration.class);
-    Configuration configuration3 = mock(Configuration.class);
-    mockLoadProjectConfigurations(
-      branch1, configuration1,
-      branch2, configuration2,
-      branch3, configuration3);
-    mockWebhookEnabled(configuration1, configuration2, configuration3);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    ComponentDao componentDaoSpy = spy(dbClient.componentDao());
-    BranchDao branchDaoSpy = spy(dbClient.branchDao());
-    SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao());
-    when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
-    when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy);
-    when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
-    underTest.onChange(
-      issueChangeData(issueDtos, branch1, branch3),
-      new IssueChange(randomRuleType),
-      userChangeContext);
-
-    verifyWebhookCalled(branch1, configuration1, analysis1);
-    verifyWebhookCalled(branch2, configuration2, analysis2);
-    verifyWebhookCalled(branch3, configuration3, analysis3);
-    extractPayloadFactoryArguments(3);
-
-    Set<String> uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid());
-    verify(componentDaoSpy).selectByUuids(any(DbSession.class), eq(ImmutableSet.of(branch2.uuid())));
-    verify(branchDaoSpy).selectByUuids(any(DbSession.class), eq(uuids));
-    verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(uuids));
-    verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy);
-  }
-
-  @Test
-  public void supports_issues_on_files_and_filter_on_short_branches_asap_when_calling_db() {
-    OrganizationDto organization = dbTester.organizations().insert();
-    ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    ComponentDto projectFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(project));
-    ComponentDto shortBranch = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.SHORT)
-      .setKey("foo"));
-    ComponentDto longBranch = dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(BranchType.LONG)
-      .setKey("bar"));
-    ComponentDto shortBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(shortBranch));
-    ComponentDto longBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(longBranch));
-    SnapshotDto analysis1 = insertAnalysisTask(project);
-    SnapshotDto analysis2 = insertAnalysisTask(shortBranch);
-    SnapshotDto analysis3 = insertAnalysisTask(longBranch);
-    List<IssueDto> issueDtos = asList(
-      newIssueDto(projectFile, project),
-      newIssueDto(shortBranchFile, shortBranch),
-      newIssueDto(longBranchFile, longBranch));
-    Configuration configuration = mockLoadProjectConfiguration(shortBranch);
-    mockWebhookEnabled(configuration);
-    mockPayloadSupplierConsumedByWebhooks();
-
-    ComponentDao componentDaoSpy = spy(dbClient.componentDao());
-    BranchDao branchDaoSpy = spy(dbClient.branchDao());
-    SnapshotDao snapshotDaoSpy = spy(dbClient.snapshotDao());
-    when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
-    when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy);
-    when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
-    underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext);
-
-    verifyWebhookCalledAndExtractPayloadFactoryArgument(shortBranch, configuration, analysis2);
-
-    Set<String> uuids = ImmutableSet.of(project.uuid(), shortBranch.uuid(), longBranch.uuid());
-    verify(componentDaoSpy).selectByUuids(any(DbSession.class), eq(uuids));
-    verify(branchDaoSpy).selectByUuids(any(DbSession.class), eq(ImmutableSet.of(shortBranch.uuid(), longBranch.uuid())));
-    verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(any(DbSession.class), eq(ImmutableSet.of(shortBranch.uuid())));
-    verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy);
-  }
-
-  private void mockWebhookEnabled(Configuration... configurations) {
-    for (Configuration configuration : configurations) {
-      when(webHooks.isEnabled(configuration)).thenReturn(true);
-    }
-  }
-
-  private void mockWebhookDisabled(Configuration... configurations) {
-    for (Configuration configuration : configurations) {
-      when(webHooks.isEnabled(configuration)).thenReturn(false);
-    }
-  }
-
-  private Configuration mockLoadProjectConfiguration(ComponentDto shortBranch) {
-    Configuration configuration = mock(Configuration.class);
-    when(projectConfigurationLoader.loadProjectConfigurations(any(DbSession.class), eq(singleton(shortBranch))))
-      .thenReturn(ImmutableMap.of(shortBranch.uuid(), configuration));
-    return configuration;
-  }
-
-  private void mockLoadProjectConfigurations(Object... branchesAndConfiguration) {
-    checkArgument(branchesAndConfiguration.length % 2 == 0);
-    Set<ComponentDto> components = new HashSet<>();
-    Map<String, Configuration> result = new HashMap<>();
-    for (int i = 0; i < branchesAndConfiguration.length; i++) {
-      ComponentDto component = (ComponentDto) branchesAndConfiguration[i++];
-      Configuration configuration = (Configuration) branchesAndConfiguration[i];
-      components.add(component);
-      result.put(component.uuid(), configuration);
-    }
-    when(projectConfigurationLoader.loadProjectConfigurations(any(DbSession.class), eq(components)))
-      .thenReturn(result);
-  }
-
-  private void mockPayloadSupplierConsumedByWebhooks() {
-    doAnswer(invocationOnMock -> {
-      Supplier<WebhookPayload> supplier = (Supplier<WebhookPayload>) invocationOnMock.getArguments()[2];
-      supplier.get();
-      return null;
-    }).when(webHooks).sendProjectAnalysisUpdate(any(Configuration.class), any(), any());
-  }
-
-  private void insertIssue(ComponentDto branch, RuleType ruleType, String status, @Nullable String resolution) {
-    RuleDefinitionDto rule = RuleTesting.newRule();
-    dbTester.rules().insert(rule);
-    dbTester.commit();
-    dbTester.issues().insert(rule, branch, branch, i -> i.setType(ruleType).setStatus(status).setResolution(resolution));
-    dbTester.commit();
-  }
-
-  private ComponentDto insertPrivateBranch(OrganizationDto organization, BranchType branchType) {
-    ComponentDto project = dbTester.components().insertPrivateProject(organization);
-    return dbTester.components().insertProjectBranch(project, branchDto -> branchDto
-      .setBranchType(branchType)
-      .setKey("foo"));
-  }
-
-  private void insertPropertiesFor(String snapshotUuid, Map<String, String> properties) {
-    List<AnalysisPropertyDto> analysisProperties = properties.entrySet().stream()
-      .map(entry -> new AnalysisPropertyDto()
-        .setUuid(UuidFactoryFast.getInstance().create())
-        .setSnapshotUuid(snapshotUuid)
-        .setKey(entry.getKey())
-        .setValue(entry.getValue()))
-      .collect(toArrayList(properties.size()));
-    dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties);
-    dbTester.getSession().commit();
-  }
-
-  private SnapshotDto insertAnalysisTask(ComponentDto branch) {
-    return dbTester.components().insertSnapshot(branch);
-  }
-
-  private ProjectAnalysis verifyWebhookCalledAndExtractPayloadFactoryArgument(ComponentDto branch, Configuration configuration, SnapshotDto analysis) {
-    verifyWebhookCalled(branch, configuration, analysis);
-
-    return extractPayloadFactoryArguments(1).iterator().next();
-  }
-
-  private void verifyWebhookCalled(ComponentDto branch, Configuration branchConfiguration, SnapshotDto analysis) {
-    verify(webHooks).isEnabled(branchConfiguration);
-    ArgumentCaptor<Supplier> supplierCaptor = ArgumentCaptor.forClass(Supplier.class);
-    verify(webHooks).sendProjectAnalysisUpdate(
-      same(branchConfiguration),
-      eq(new WebHooks.Analysis(branch.uuid(), analysis.getUuid(), null)),
-      supplierCaptor.capture());
-  }
-
-  private List<ProjectAnalysis> extractPayloadFactoryArguments(int time) {
-    ArgumentCaptor<ProjectAnalysis> projectAnalysisCaptor = ArgumentCaptor.forClass(ProjectAnalysis.class);
-    verify(webhookPayloadFactory, times(time)).create(projectAnalysisCaptor.capture());
-    return projectAnalysisCaptor.getAllValues();
-  }
-
-  private void indexIssues(ComponentDto branch) {
-    issueIndexer.indexOnAnalysis(branch.uuid());
-  }
-
-  @DataProvider
-  public static Object[][] validIssueChanges() {
-    return new Object[][] {
-      {new IssueChange(RuleType.BUG)},
-      {new IssueChange(RuleType.VULNERABILITY)},
-      {new IssueChange(RuleType.CODE_SMELL)},
-      {new IssueChange(DefaultTransitions.RESOLVE)},
-      {new IssueChange(RuleType.BUG, DefaultTransitions.RESOLVE)},
-      {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.RESOLVE)},
-      {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.RESOLVE)},
-      {new IssueChange(DefaultTransitions.FALSE_POSITIVE)},
-      {new IssueChange(RuleType.BUG, DefaultTransitions.FALSE_POSITIVE)},
-      {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.FALSE_POSITIVE)},
-      {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.FALSE_POSITIVE)},
-      {new IssueChange(DefaultTransitions.WONT_FIX)},
-      {new IssueChange(RuleType.BUG, DefaultTransitions.WONT_FIX)},
-      {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.WONT_FIX)},
-      {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.WONT_FIX)},
-      {new IssueChange(DefaultTransitions.REOPEN)},
-      {new IssueChange(RuleType.BUG, DefaultTransitions.REOPEN)},
-      {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.REOPEN)},
-      {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.REOPEN)}
-    };
-  }
-
-  private IssueChangeWebhook.IssueChangeData issueChangeData() {
-    return new IssueChangeWebhook.IssueChangeData(emptyList(), emptyList());
-  }
-
-  private IssueChangeWebhook.IssueChangeData issueChangeData(IssueDto issueDto) {
-    return new IssueChangeWebhook.IssueChangeData(singletonList(issueDto.toDefaultIssue()), emptyList());
-  }
-
-  private IssueChangeWebhook.IssueChangeData issueChangeData(Collection<IssueDto> issueDtos, ComponentDto... components) {
-    return new IssueChangeWebhook.IssueChangeData(
-      issueDtos.stream().map(IssueDto::toDefaultIssue).collect(Collectors.toList()),
-      Arrays.stream(components).collect(Collectors.toList()));
-  }
-
-  private IssueDto newIssueDto(@Nullable ComponentDto project) {
-    return newIssueDto(project, project);
-  }
-
-  private IssueDto newIssueDto(@Nullable ComponentDto component, @Nullable ComponentDto project) {
-    RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)];
-    String randomStatus = Issue.STATUSES.get(random.nextInt(Issue.STATUSES.size()));
-    IssueDto res = new IssueDto()
-      .setType(randomRuleType)
-      .setStatus(randomStatus)
-      .setRuleKey(randomAlphanumeric(3), randomAlphanumeric(4));
-    if (component != null) {
-      res.setComponent(component);
-    }
-    if (project != null) {
-      res.setProject(project);
-    }
-    return res;
-  }
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/webhook/IssueChangeWebhookTest.java
deleted file mode 100644 (file)
index b88f25a..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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.webhook;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.api.rules.RuleType;
-
-import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class IssueChangeWebhookTest {
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-
-  @Test
-  public void IssueChange_constructor_throws_IAE_if_both_args_are_null() {
-    expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("At least one of ruleType and transitionKey must be non null");
-
-    new IssueChangeWebhook.IssueChange(null, null);
-  }
-
-  @Test
-  public void verify_IssueChange_getters() {
-    IssueChangeWebhook.IssueChange transitionKeyOnly = new IssueChangeWebhookImpl.IssueChange("foo");
-    assertThat(transitionKeyOnly.getTransitionKey()).contains("foo");
-    assertThat(transitionKeyOnly.getRuleType()).isEmpty();
-    IssueChangeWebhook.IssueChange ruleTypeOnly = new IssueChangeWebhookImpl.IssueChange(RuleType.BUG);
-    assertThat(ruleTypeOnly.getTransitionKey()).isEmpty();
-    assertThat(ruleTypeOnly.getRuleType()).contains(RuleType.BUG);
-    IssueChangeWebhook.IssueChange transitionKeyAndRuleType = new IssueChangeWebhookImpl.IssueChange(RuleType.VULNERABILITY, "bar");
-    assertThat(transitionKeyAndRuleType.getTransitionKey()).contains("bar");
-    assertThat(transitionKeyAndRuleType.getRuleType()).contains(RuleType.VULNERABILITY);
-  }
-
-  @Test
-  public void verify_IssueChange_equality() {
-    IssueChangeWebhook.IssueChange underTest = new IssueChangeWebhook.IssueChange(RuleType.BUG);
-
-    assertThat(underTest).isEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG));
-    assertThat(underTest).isEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG, null));
-
-    assertThat(underTest).isNotEqualTo(null);
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG, randomAlphanumeric(10)));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10)));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10)));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(randomAlphanumeric(10)));
-
-    String transitionKey = randomAlphanumeric(10);
-    underTest = new IssueChangeWebhook.IssueChange(transitionKey);
-
-    assertThat(underTest).isEqualTo(new IssueChangeWebhook.IssueChange(transitionKey));
-    assertThat(underTest).isEqualTo(new IssueChangeWebhook.IssueChange(null, transitionKey));
-
-    assertThat(underTest).isNotEqualTo(null);
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG, transitionKey));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.BUG, randomAlphanumeric(10)));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10)));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.CODE_SMELL, transitionKey));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10)));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(RuleType.VULNERABILITY, transitionKey));
-    assertThat(underTest).isNotEqualTo(new IssueChangeWebhook.IssueChange(randomAlphanumeric(9)));
-  }
-}
index 6efc13e9f2b01866be6ec8b3ac2e0b0df53fd39e..a88c1f70004eee4f5116a6689be2a80de6c1d1b3 100644 (file)
@@ -56,12 +56,12 @@ import org.sonar.server.issue.index.IssueIndexDefinition;
 import org.sonar.server.issue.index.IssueIndexer;
 import org.sonar.server.issue.index.IssueIteratorFactory;
 import org.sonar.server.issue.notification.IssueChangeNotification;
-import org.sonar.server.issue.webhook.IssueChangeWebhook;
 import org.sonar.server.issue.workflow.FunctionExecutor;
 import org.sonar.server.issue.workflow.IssueWorkflow;
 import org.sonar.server.notification.NotificationManager;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger;
 import org.sonar.server.rule.DefaultRuleFinder;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
@@ -117,7 +117,7 @@ public class BulkChangeActionTest {
   private IssueStorage issueStorage = new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient,
     new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)));
   private NotificationManager notificationManager = mock(NotificationManager.class);
-  private IssueChangeWebhook issueChangeWebhook = mock(IssueChangeWebhook.class);
+  private IssueChangeTrigger issueChangeTrigger = mock(IssueChangeTrigger.class);
   private List<Action> actions = new ArrayList<>();
 
   private RuleDto rule;
@@ -126,7 +126,7 @@ public class BulkChangeActionTest {
   private ComponentDto file;
   private UserDto user;
 
-  private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, issueChangeWebhook));
+  private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, issueChangeTrigger));
 
   @Before
   public void setUp() throws Exception {
@@ -173,7 +173,7 @@ public class BulkChangeActionTest {
     assertThat(reloaded.getSeverity()).isEqualTo(MINOR);
     assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
 
-    verifyZeroInteractions(issueChangeWebhook);
+    verifyZeroInteractions(issueChangeTrigger);
   }
 
   @Test
@@ -191,7 +191,7 @@ public class BulkChangeActionTest {
     assertThat(reloaded.getTags()).containsOnly("tag1", "tag2", "tag3");
     assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
 
-    verifyZeroInteractions(issueChangeWebhook);
+    verifyZeroInteractions(issueChangeTrigger);
   }
 
   @Test
@@ -209,7 +209,7 @@ public class BulkChangeActionTest {
     assertThat(reloaded.getAssignee()).isNull();
     assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
 
-    verifyZeroInteractions(issueChangeWebhook);
+    verifyZeroInteractions(issueChangeTrigger);
   }
 
   @Test
@@ -530,12 +530,12 @@ public class BulkChangeActionTest {
   private void verifyIssueChangeWebhookCalled(@Nullable RuleType expectedRuleType, @Nullable String transitionKey,
     String[] componentUUids,
     IssueDto... issueDtos) {
-    ArgumentCaptor<IssueChangeWebhook.IssueChangeData> issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeWebhook.IssueChangeData.class);
-    verify(issueChangeWebhook).onChange(
+    ArgumentCaptor<IssueChangeTrigger.IssueChangeData> issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeTrigger.IssueChangeData.class);
+    verify(issueChangeTrigger).onChange(
       issueChangeDataCaptor.capture(),
-      eq(new IssueChangeWebhook.IssueChange(expectedRuleType, transitionKey)),
+      eq(new IssueChangeTrigger.IssueChange(expectedRuleType, transitionKey)),
       eq(IssueChangeContext.createUser(new Date(NOW), userSession.getLogin())));
-    IssueChangeWebhook.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue();
+    IssueChangeTrigger.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue();
     assertThat(issueChangeData.getIssues())
       .extracting(DefaultIssue::key)
       .containsOnly(Arrays.stream(issueDtos).map(IssueDto::getKey).toArray(String[]::new));
index 3384f42a4ec63b871d6b65c2c4535fbad9b279ee..69a4815f72f6554cbe8c6470fbc9c7dad2aab600 100644 (file)
@@ -53,12 +53,12 @@ import org.sonar.server.issue.TransitionService;
 import org.sonar.server.issue.index.IssueIndexDefinition;
 import org.sonar.server.issue.index.IssueIndexer;
 import org.sonar.server.issue.index.IssueIteratorFactory;
-import org.sonar.server.issue.webhook.IssueChangeWebhook;
 import org.sonar.server.issue.workflow.FunctionExecutor;
 import org.sonar.server.issue.workflow.IssueWorkflow;
 import org.sonar.server.notification.NotificationManager;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger;
 import org.sonar.server.rule.DefaultRuleFinder;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
@@ -113,10 +113,10 @@ public class DoTransitionActionTest {
   private ComponentDto project;
   private ComponentDto file;
   private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
-  private IssueChangeWebhook issueChangeWebhook = mock(IssueChangeWebhook.class);
+  private IssueChangeTrigger issueChangeTrigger = mock(IssueChangeTrigger.class);
 
   private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2,
-    issueChangeWebhook);
+    issueChangeTrigger);
   private WsActionTester tester = new WsActionTester(underTest);
 
   @Before
@@ -140,12 +140,12 @@ public class DoTransitionActionTest {
     IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get();
     assertThat(issueReloaded.getStatus()).isEqualTo(STATUS_CONFIRMED);
 
-    ArgumentCaptor<IssueChangeWebhook.IssueChangeData> issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeWebhook.IssueChangeData.class);
-    verify(issueChangeWebhook).onChange(
+    ArgumentCaptor<IssueChangeTrigger.IssueChangeData> issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeTrigger.IssueChangeData.class);
+    verify(issueChangeTrigger).onChange(
       issueChangeDataCaptor.capture(),
-      eq(new IssueChangeWebhook.IssueChange(null, "confirm")),
+      eq(new IssueChangeTrigger.IssueChange(null, "confirm")),
       eq(IssueChangeContext.createUser(new Date(now), userSession.getLogin())));
-    IssueChangeWebhook.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue();
+    IssueChangeTrigger.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue();
     assertThat(issueChangeData.getIssues())
       .extracting(DefaultIssue::key)
       .containsOnly(issueDto.getKey());
index 7ec66707d9b09bc32d22f54ee0333ad0807be99c..e4a4bdea80ba029a3a8af6ba0ee2b3adac676804 100644 (file)
@@ -30,7 +30,7 @@ public class IssueWsModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new IssueWsModule().configure(container);
-    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 30);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 32);
   }
 }
 
index ed9320e78842213d043e0fda019d503621a511f5..56b46375d012e2a47c9f8a4ca0ec2c64c7d38a3d 100644 (file)
@@ -51,10 +51,10 @@ import org.sonar.server.issue.ServerIssueStorage;
 import org.sonar.server.issue.index.IssueIndexDefinition;
 import org.sonar.server.issue.index.IssueIndexer;
 import org.sonar.server.issue.index.IssueIteratorFactory;
-import org.sonar.server.issue.webhook.IssueChangeWebhook;
 import org.sonar.server.notification.NotificationManager;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger;
 import org.sonar.server.rule.DefaultRuleFinder;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
@@ -97,11 +97,11 @@ public class SetTypeActionTest {
   private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class);
 
   private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient));
-  private IssueChangeWebhook issueChangeWebhook = mock(IssueChangeWebhook.class);
+  private IssueChangeTrigger issueChangeTrigger = mock(IssueChangeTrigger.class);
   private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(),
     new IssueUpdater(dbClient,
       new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class)),
-    responseWriter, system2, issueChangeWebhook));
+    responseWriter, system2, issueChangeTrigger));
 
   @Test
   public void set_type() throws Exception {
@@ -117,12 +117,12 @@ public class SetTypeActionTest {
     IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get();
     assertThat(issueReloaded.getType()).isEqualTo(BUG.getDbConstant());
 
-    ArgumentCaptor<IssueChangeWebhook.IssueChangeData> issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeWebhook.IssueChangeData.class);
-    verify(issueChangeWebhook).onChange(
+    ArgumentCaptor<IssueChangeTrigger.IssueChangeData> issueChangeDataCaptor = ArgumentCaptor.forClass(IssueChangeTrigger.IssueChangeData.class);
+    verify(issueChangeTrigger).onChange(
       issueChangeDataCaptor.capture(),
-      eq(new IssueChangeWebhook.IssueChange(BUG, null)),
+      eq(new IssueChangeTrigger.IssueChange(BUG, null)),
       eq(IssueChangeContext.createUser(new Date(now), userSession.getLogin())));
-    IssueChangeWebhook.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue();
+    IssueChangeTrigger.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue();
     assertThat(issueChangeData.getIssues())
       .extracting(DefaultIssue::key)
       .containsOnly(issueDto.getKey());
diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java
new file mode 100644 (file)
index 0000000..d0d48fb
--- /dev/null
@@ -0,0 +1,606 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.issue.DefaultTransitions;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.AnalysisPropertyDto;
+import org.sonar.db.component.BranchDao;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDao;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.component.SnapshotDao;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger.IssueChange;
+import org.sonar.server.settings.ProjectConfigurationLoader;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.webhook.WebhookPayloadFactory;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static java.util.Collections.singletonList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.Mockito.mock;
+import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
+import static org.sonar.db.component.ComponentTesting.newBranchDto;
+
+@RunWith(DataProviderRunner.class)
+public class IssueChangeTriggerImplTest {
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+  @Rule
+  public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+  private DbClient dbClient = dbTester.getDbClient();
+
+  private Random random = new Random();
+  private RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)];
+
+  private IssueChangeContext scanChangeContext = IssueChangeContext.createScan(new Date());
+  private IssueChangeContext userChangeContext = IssueChangeContext.createUser(new Date(), "userLogin");
+  private WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class);
+  private DbClient spiedOnDbClient = Mockito.spy(dbClient);
+  private ProjectConfigurationLoader projectConfigurationLoader = mock(ProjectConfigurationLoader.class);
+  private QGChangeEventListeners qgChangeEventListeners = mock(QGChangeEventListeners.class);
+  private IssueChangeTriggerImpl underTest = new IssueChangeTriggerImpl(spiedOnDbClient, projectConfigurationLoader, qgChangeEventListeners);
+  private DbClient mockedDbClient = mock(DbClient.class);
+  private IssueChangeTriggerImpl mockedUnderTest = new IssueChangeTriggerImpl(mockedDbClient, projectConfigurationLoader, qgChangeEventListeners);
+
+  @Test
+  public void on_type_change_has_no_effect_if_SearchResponseData_has_no_issue() {
+    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType), userChangeContext);
+
+    Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+  }
+
+  @Test
+  public void on_type_change_has_no_effect_if_scan_IssueChangeContext() {
+    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType), scanChangeContext);
+
+    Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+  }
+
+  @Test
+  public void on_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() {
+    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomAlphanumeric(12)), userChangeContext);
+
+    Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+  }
+
+  @Test
+  public void onTransition_has_no_effect_if_transition_key_is_empty() {
+    on_transition_changeHasNoEffectForTransitionKey("");
+  }
+
+  @Test
+  public void onTransition_has_no_effect_if_transition_key_is_random() {
+    on_transition_changeHasNoEffectForTransitionKey(randomAlphanumeric(99));
+  }
+
+  @Test
+  public void on_transition_change_has_no_effect_if_transition_key_is_ignored_default_transition_key() {
+    Set<String> supportedDefaultTransitionKeys = ImmutableSet.of(
+      DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
+    DefaultTransitions.ALL.stream()
+      .filter(s -> !supportedDefaultTransitionKeys.contains(s))
+      .forEach(this::on_transition_changeHasNoEffectForTransitionKey);
+  }
+
+  private void on_transition_changeHasNoEffectForTransitionKey(@Nullable String transitionKey) {
+    Mockito.reset(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+
+    mockedUnderTest.onChange(issueChangeData(newIssueDto()), new IssueChange(transitionKey), userChangeContext);
+
+    Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+  }
+
+  @Test
+  public void on_transition_change_has_no_effect_if_scan_IssueChangeContext() {
+    mockedUnderTest.onChange(issueChangeData(newIssueDto()), new IssueChange(randomAlphanumeric(12)), scanChangeContext);
+
+    Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+  }
+
+  @Test
+  public void on_type_and_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() {
+    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), userChangeContext);
+
+    Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+  }
+
+  @Test
+  public void on_type_and_transition_change_has_no_effect_if_scan_IssueChangeContext() {
+    mockedUnderTest.onChange(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), scanChangeContext);
+
+    Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+  }
+
+  @Test
+  public void on_type_and_transition_has_no_effect_if_transition_key_is_empty() {
+    on_type_and_transition_changeHasNoEffectForTransitionKey("");
+  }
+
+  @Test
+  public void on_type_and_transition_has_no_effect_if_transition_key_is_random() {
+    on_type_and_transition_changeHasNoEffectForTransitionKey(randomAlphanumeric(66));
+  }
+
+  @Test
+  public void on_type_and_transition_has_no_effect_if_transition_key_is_ignored_default_transition_key() {
+    Set<String> supportedDefaultTransitionKeys = ImmutableSet.of(
+      DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
+    DefaultTransitions.ALL.stream()
+      .filter(s -> !supportedDefaultTransitionKeys.contains(s))
+      .forEach(this::on_type_and_transition_changeHasNoEffectForTransitionKey);
+  }
+
+  private void on_type_and_transition_changeHasNoEffectForTransitionKey(@Nullable String transitionKey) {
+    Mockito.reset(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+
+    mockedUnderTest.onChange(issueChangeData(newIssueDto()), new IssueChange(randomRuleType, transitionKey), userChangeContext);
+
+    Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory);
+  }
+
+  @Test
+  @UseDataProvider("validIssueChanges")
+  public void broadcast_to_QGEventListeners_for_short_living_branch_of_issue(IssueChange issueChange) {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPublicProject(organization);
+    ComponentAndBranch branch = insertProjectBranch(project, BranchType.SHORT, "foo");
+    SnapshotDto analysis = insertAnalysisTask(branch);
+    Configuration configuration = mockLoadProjectConfiguration(branch);
+
+    Map<String, String> properties = new HashMap<>();
+    properties.put("sonar.analysis.test1", randomAlphanumeric(50));
+    properties.put("sonar.analysis.test2", randomAlphanumeric(5000));
+    insertPropertiesFor(analysis.getUuid(), properties);
+
+    underTest.onChange(issueChangeData(newIssueDto(branch)), issueChange, userChangeContext);
+
+    Collection<QGChangeEvent> events = verifyListenersBroadcastedTo();
+    assertThat(events).hasSize(1);
+    QGChangeEvent event = events.iterator().next();
+    assertThat(event.getProject()).isEqualTo(branch.component);
+    assertThat(event.getBranch()).isEqualTo(branch.branch);
+    assertThat(event.getAnalysis()).isEqualTo(analysis);
+    assertThat(event.getProjectConfiguration()).isSameAs(configuration);
+  }
+
+  @Test
+  public void do_not_load_project_configuration_nor_analysis_nor_call_webhook_if_there_are_no_short_branch() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPublicProject(organization);
+    ComponentAndBranch longBranch1 = insertProjectBranch(project, BranchType.LONG, "foo");
+    ComponentAndBranch longBranch2 = insertProjectBranch(project, BranchType.LONG, "bar");
+    ImmutableList<IssueDto> issueDtos = ImmutableList.of(newIssueDto(project), newIssueDto(longBranch1), newIssueDto(longBranch2));
+
+    SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao());
+    Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
+    underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext);
+
+    Mockito.verifyZeroInteractions(projectConfigurationLoader);
+    Mockito.verify(snapshotDaoSpy, Mockito.times(0)).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.anyCollectionOf(String.class));
+  }
+
+  @Test
+  public void creates_single_QGChangeEvent_per_short_branch_with_at_least_one_issue_in_SearchResponseData() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT);
+    ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT);
+    ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT);
+    SnapshotDto analysis1 = insertAnalysisTask(branch1);
+    SnapshotDto analysis2 = insertAnalysisTask(branch2);
+    SnapshotDto analysis3 = insertAnalysisTask(branch3);
+    int issuesBranch1 = 2 + random.nextInt(10);
+    int issuesBranch2 = 2 + random.nextInt(10);
+    int issuesBranch3 = 2 + random.nextInt(10);
+    List<IssueDto> issueDtos = Stream.of(
+      IntStream.range(0, issuesBranch1).mapToObj(i -> newIssueDto(branch1.component)),
+      IntStream.range(0, issuesBranch2).mapToObj(i -> newIssueDto(branch2.component)),
+      IntStream.range(0, issuesBranch3).mapToObj(i -> newIssueDto(branch3.component)))
+      .flatMap(s -> s)
+      .collect(MoreCollectors.toList());
+    Configuration configuration1 = mock(Configuration.class);
+    Configuration configuration2 = mock(Configuration.class);
+    Configuration configuration3 = mock(Configuration.class);
+    mockLoadProjectConfigurations(
+      branch1.component, configuration1,
+      branch2.component, configuration2,
+      branch3.component, configuration3);
+
+    ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao());
+    BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao());
+    SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao());
+    Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
+    Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy);
+    Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
+    underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext);
+
+    Collection<QGChangeEvent> qgChangeEvents = verifyListenersBroadcastedTo();
+    assertThat(qgChangeEvents)
+      .hasSize(3)
+      .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis)
+      .containsOnly(
+        tuple(branch1.branch, configuration1, analysis1),
+        tuple(branch2.branch, configuration2, analysis2),
+        tuple(branch3.branch, configuration3, analysis3));
+
+    // verifyWebhookCalled(branch1, configuration1, analysis1);
+    // verifyWebhookCalled(branch2, configuration2, analysis2);
+    // verifyWebhookCalled(branch3, configuration3, analysis3);
+    // extractPayloadFactoryArguments(3);
+
+    Set<String> uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid());
+    Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids));
+    Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids));
+    Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(uuids));
+    Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy);
+  }
+
+  @Test
+  public void create_QGChangeEvent_only_for_short_branches() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch shortBranch = insertPrivateBranch(organization, BranchType.SHORT);
+    ComponentAndBranch longBranch = insertPrivateBranch(organization, BranchType.LONG);
+    SnapshotDto analysis1 = insertAnalysisTask(shortBranch);
+    SnapshotDto analysis2 = insertAnalysisTask(longBranch);
+    Configuration configuration = mockLoadProjectConfiguration(shortBranch);
+
+    ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao());
+    BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao());
+    SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao());
+    Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
+    Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy);
+    Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
+    underTest.onChange(
+      issueChangeData(asList(newIssueDto(shortBranch), newIssueDto(longBranch))),
+      new IssueChange(randomRuleType),
+      userChangeContext);
+
+    Collection<QGChangeEvent> qgChangeEvents = verifyListenersBroadcastedTo();
+    assertThat(qgChangeEvents)
+      .hasSize(1)
+      .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis)
+      .containsOnly(tuple(shortBranch.branch, configuration, analysis1));
+
+    Set<String> uuids = ImmutableSet.of(shortBranch.uuid(), longBranch.uuid());
+    Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids));
+    Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids));
+    Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid())));
+    Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy);
+  }
+
+  @Test
+  public void do_not_load_componentDto_from_DB_if_all_are_in_inputData() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT);
+    ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT);
+    ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT);
+    SnapshotDto analysis1 = insertAnalysisTask(branch1);
+    SnapshotDto analysis2 = insertAnalysisTask(branch2);
+    SnapshotDto analysis3 = insertAnalysisTask(branch3);
+    List<IssueDto> issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3));
+    Configuration configuration1 = mock(Configuration.class);
+    Configuration configuration2 = mock(Configuration.class);
+    Configuration configuration3 = mock(Configuration.class);
+    mockLoadProjectConfigurations(
+      branch1.component, configuration1,
+      branch2.component, configuration2,
+      branch3.component, configuration3);
+
+    ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao());
+    Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
+    underTest.onChange(
+      issueChangeData(issueDtos, branch1, branch2, branch3),
+      new IssueChange(randomRuleType),
+      userChangeContext);
+
+    Collection<QGChangeEvent> qgChangeEvents = verifyListenersBroadcastedTo();
+    assertThat(qgChangeEvents)
+      .hasSize(3)
+      .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis)
+      .containsOnly(
+        tuple(branch1.branch, configuration1, analysis1),
+        tuple(branch2.branch, configuration2, analysis2),
+        tuple(branch3.branch, configuration3, analysis3));
+
+    Mockito.verify(componentDaoSpy, Mockito.times(0)).selectByUuids(Matchers.any(DbSession.class), Matchers.anyCollectionOf(String.class));
+    Mockito.verifyNoMoreInteractions(componentDaoSpy);
+  }
+
+  @Test
+  public void call_db_only_for_componentDto_not_in_inputData() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT);
+    ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT);
+    ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT);
+    SnapshotDto analysis1 = insertAnalysisTask(branch1);
+    SnapshotDto analysis2 = insertAnalysisTask(branch2);
+    SnapshotDto analysis3 = insertAnalysisTask(branch3);
+    List<IssueDto> issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3));
+    Configuration configuration1 = mock(Configuration.class);
+    Configuration configuration2 = mock(Configuration.class);
+    Configuration configuration3 = mock(Configuration.class);
+    mockLoadProjectConfigurations(
+      branch1.component, configuration1,
+      branch2.component, configuration2,
+      branch3.component, configuration3);
+
+    ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao());
+    BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao());
+    SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao());
+    Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
+    Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy);
+    Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
+    underTest.onChange(
+      issueChangeData(issueDtos, branch1, branch3),
+      new IssueChange(randomRuleType),
+      userChangeContext);
+
+    assertThat(verifyListenersBroadcastedTo()).hasSize(3);
+
+    Set<String> uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid());
+    Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(branch2.uuid())));
+    Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids));
+    Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(uuids));
+    Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy);
+  }
+
+  @Test
+  public void supports_issues_on_files_and_filter_on_short_branches_asap_when_calling_db() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project));
+    ComponentAndBranch shortBranch = insertProjectBranch(project, BranchType.SHORT, "foo");
+    ComponentAndBranch longBranch = insertProjectBranch(project, BranchType.LONG, "bar");
+    ComponentDto shortBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(shortBranch.component));
+    ComponentDto longBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(longBranch.component));
+    SnapshotDto analysis1 = insertAnalysisTask(project);
+    SnapshotDto analysis2 = insertAnalysisTask(shortBranch);
+    SnapshotDto analysis3 = insertAnalysisTask(longBranch);
+    List<IssueDto> issueDtos = asList(
+      newIssueDto(file, project),
+      newIssueDto(shortBranchFile, shortBranch),
+      newIssueDto(longBranchFile, longBranch));
+    Configuration configuration = mockLoadProjectConfiguration(shortBranch);
+
+    ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao());
+    BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao());
+    SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao());
+    Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy);
+    Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy);
+    Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy);
+    underTest.onChange(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext);
+
+    Collection<QGChangeEvent> qgChangeEvents = verifyListenersBroadcastedTo();
+    assertThat(qgChangeEvents)
+      .hasSize(1)
+      .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis)
+      .containsOnly(tuple(shortBranch.branch, configuration, analysis2));
+
+    Set<String> uuids = ImmutableSet.of(project.uuid(), shortBranch.uuid(), longBranch.uuid());
+    Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids));
+    Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid(), longBranch.uuid())));
+    Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid())));
+    Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy);
+  }
+
+  private Configuration mockLoadProjectConfiguration(ComponentAndBranch componentAndBranch) {
+    Configuration configuration = mock(Configuration.class);
+    Mockito.when(projectConfigurationLoader.loadProjectConfigurations(Matchers.any(DbSession.class), Matchers.eq(singleton(componentAndBranch.component))))
+      .thenReturn(ImmutableMap.of(componentAndBranch.uuid(), configuration));
+    return configuration;
+  }
+
+  private void mockLoadProjectConfigurations(Object... branchesAndConfiguration) {
+    checkArgument(branchesAndConfiguration.length % 2 == 0);
+    Set<ComponentDto> components = new HashSet<>();
+    Map<String, Configuration> result = new HashMap<>();
+    for (int i = 0; i < branchesAndConfiguration.length; i++) {
+      ComponentDto component = (ComponentDto) branchesAndConfiguration[i++];
+      Configuration configuration = (Configuration) branchesAndConfiguration[i];
+      components.add(component);
+      result.put(component.uuid(), configuration);
+    }
+    Mockito.when(projectConfigurationLoader.loadProjectConfigurations(Matchers.any(DbSession.class), Matchers.eq(components)))
+      .thenReturn(result);
+  }
+
+  private ComponentAndBranch insertPrivateBranch(OrganizationDto organization, BranchType branchType) {
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    BranchDto branchDto = newBranchDto(project.projectUuid(), branchType)
+      .setKey("foo");
+    ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto);
+    return new ComponentAndBranch(newComponent, branchDto);
+  }
+
+  public ComponentAndBranch insertProjectBranch(ComponentDto project, BranchType type, String branchKey) {
+    BranchDto branchDto = newBranchDto(project.projectUuid(), type).setKey(branchKey);
+    ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto);
+    return new ComponentAndBranch(newComponent, branchDto);
+  }
+
+  private static class ComponentAndBranch {
+    private final ComponentDto component;
+
+    private final BranchDto branch;
+
+    private ComponentAndBranch(ComponentDto component, BranchDto branch) {
+      this.component = component;
+      this.branch = branch;
+    }
+
+    public ComponentDto getComponent() {
+      return component;
+    }
+
+    public BranchDto getBranch() {
+      return branch;
+    }
+
+    public String uuid() {
+      return component.uuid();
+    }
+
+  }
+
+  private void insertPropertiesFor(String snapshotUuid, Map<String, String> properties) {
+    List<AnalysisPropertyDto> analysisProperties = properties.entrySet().stream()
+      .map(entry -> new AnalysisPropertyDto()
+        .setUuid(UuidFactoryFast.getInstance().create())
+        .setSnapshotUuid(snapshotUuid)
+        .setKey(entry.getKey())
+        .setValue(entry.getValue()))
+      .collect(toArrayList(properties.size()));
+    dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties);
+    dbTester.getSession().commit();
+  }
+
+  private SnapshotDto insertAnalysisTask(ComponentAndBranch componentAndBranch) {
+    return insertAnalysisTask(componentAndBranch.component);
+  }
+
+  private SnapshotDto insertAnalysisTask(ComponentDto component) {
+    return dbTester.components().insertSnapshot(component);
+  }
+
+  private Collection<QGChangeEvent> verifyListenersBroadcastedTo() {
+    Class<Collection<QGChangeEvent>> clazz = (Class<Collection<QGChangeEvent>>) (Class) Collection.class;
+    ArgumentCaptor<Collection<QGChangeEvent>> supplierCaptor = ArgumentCaptor.forClass(clazz);
+    Mockito.verify(qgChangeEventListeners).broadcast(
+      Matchers.same(Trigger.ISSUE_CHANGE),
+      supplierCaptor.capture());
+    return supplierCaptor.getValue();
+  }
+
+  @DataProvider
+  public static Object[][] validIssueChanges() {
+    return new Object[][] {
+      {new IssueChange(RuleType.BUG)},
+      {new IssueChange(RuleType.VULNERABILITY)},
+      {new IssueChange(RuleType.CODE_SMELL)},
+      {new IssueChange(DefaultTransitions.RESOLVE)},
+      {new IssueChange(RuleType.BUG, DefaultTransitions.RESOLVE)},
+      {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.RESOLVE)},
+      {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.RESOLVE)},
+      {new IssueChange(DefaultTransitions.FALSE_POSITIVE)},
+      {new IssueChange(RuleType.BUG, DefaultTransitions.FALSE_POSITIVE)},
+      {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.FALSE_POSITIVE)},
+      {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.FALSE_POSITIVE)},
+      {new IssueChange(DefaultTransitions.WONT_FIX)},
+      {new IssueChange(RuleType.BUG, DefaultTransitions.WONT_FIX)},
+      {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.WONT_FIX)},
+      {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.WONT_FIX)},
+      {new IssueChange(DefaultTransitions.REOPEN)},
+      {new IssueChange(RuleType.BUG, DefaultTransitions.REOPEN)},
+      {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.REOPEN)},
+      {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.REOPEN)}
+    };
+  }
+
+  private IssueChangeTrigger.IssueChangeData issueChangeData() {
+    return new IssueChangeTrigger.IssueChangeData(emptyList(), emptyList());
+  }
+
+  private IssueChangeTrigger.IssueChangeData issueChangeData(IssueDto issueDto) {
+    return new IssueChangeTrigger.IssueChangeData(singletonList(issueDto.toDefaultIssue()), emptyList());
+  }
+
+  private IssueChangeTrigger.IssueChangeData issueChangeData(Collection<IssueDto> issueDtos, ComponentAndBranch... components) {
+    return new IssueChangeTrigger.IssueChangeData(
+      issueDtos.stream().map(IssueDto::toDefaultIssue).collect(Collectors.toList()),
+      Arrays.stream(components).map(ComponentAndBranch::getComponent).collect(Collectors.toList()));
+  }
+
+  private IssueDto newIssueDto(@Nullable ComponentAndBranch projectAndBranch) {
+    return projectAndBranch == null ? newIssueDto() : newIssueDto(projectAndBranch.component, projectAndBranch.component);
+  }
+
+  private IssueDto newIssueDto(ComponentDto componentDto) {
+    return newIssueDto(componentDto, componentDto);
+  }
+
+  private IssueDto newIssueDto() {
+    return newIssueDto((ComponentDto) null, (ComponentDto) null);
+  }
+
+  private IssueDto newIssueDto(@Nullable ComponentDto component, @Nullable ComponentAndBranch componentAndBranch) {
+    return newIssueDto(component, componentAndBranch == null ? null : componentAndBranch.component);
+  }
+
+  private IssueDto newIssueDto(@Nullable ComponentDto component, @Nullable ComponentDto project) {
+    RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)];
+    String randomStatus = Issue.STATUSES.get(random.nextInt(Issue.STATUSES.size()));
+    IssueDto res = new IssueDto()
+      .setType(randomRuleType)
+      .setStatus(randomStatus)
+      .setRuleKey(randomAlphanumeric(3), randomAlphanumeric(4));
+    if (component != null) {
+      res.setComponent(component);
+    }
+    if (project != null) {
+      res.setProject(project);
+    }
+    return res;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerTest.java
new file mode 100644 (file)
index 0000000..1a05fce
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.qualitygate.changeevent;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.rules.RuleType;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueChangeTriggerTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void IssueChange_constructor_throws_IAE_if_both_args_are_null() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("At least one of ruleType and transitionKey must be non null");
+
+    new IssueChangeTrigger.IssueChange(null, null);
+  }
+
+  @Test
+  public void verify_IssueChange_getters() {
+    IssueChangeTrigger.IssueChange transitionKeyOnly = new IssueChangeTriggerImpl.IssueChange("foo");
+    assertThat(transitionKeyOnly.getTransitionKey()).contains("foo");
+    assertThat(transitionKeyOnly.getRuleType()).isEmpty();
+    IssueChangeTrigger.IssueChange ruleTypeOnly = new IssueChangeTriggerImpl.IssueChange(RuleType.BUG);
+    assertThat(ruleTypeOnly.getTransitionKey()).isEmpty();
+    assertThat(ruleTypeOnly.getRuleType()).contains(RuleType.BUG);
+    IssueChangeTrigger.IssueChange transitionKeyAndRuleType = new IssueChangeTriggerImpl.IssueChange(RuleType.VULNERABILITY, "bar");
+    assertThat(transitionKeyAndRuleType.getTransitionKey()).contains("bar");
+    assertThat(transitionKeyAndRuleType.getRuleType()).contains(RuleType.VULNERABILITY);
+  }
+
+  @Test
+  public void verify_IssueChange_equality() {
+    IssueChangeTrigger.IssueChange underTest = new IssueChangeTrigger.IssueChange(RuleType.BUG);
+
+    assertThat(underTest).isEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG));
+    assertThat(underTest).isEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG, null));
+
+    assertThat(underTest).isNotEqualTo(null);
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG, randomAlphanumeric(10)));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10)));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10)));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(randomAlphanumeric(10)));
+
+    String transitionKey = randomAlphanumeric(10);
+    underTest = new IssueChangeTrigger.IssueChange(transitionKey);
+
+    assertThat(underTest).isEqualTo(new IssueChangeTrigger.IssueChange(transitionKey));
+    assertThat(underTest).isEqualTo(new IssueChangeTrigger.IssueChange(null, transitionKey));
+
+    assertThat(underTest).isNotEqualTo(null);
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG, transitionKey));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.BUG, randomAlphanumeric(10)));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10)));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.CODE_SMELL, transitionKey));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10)));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(RuleType.VULNERABILITY, transitionKey));
+    assertThat(underTest).isNotEqualTo(new IssueChangeTrigger.IssueChange(randomAlphanumeric(9)));
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java
new file mode 100644 (file)
index 0000000..acbeca5
--- /dev/null
@@ -0,0 +1,482 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.webhook;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+import javax.annotation.Nullable;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.AnalysisPropertyDto;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.permission.index.AuthorizationTypeSupport;
+import org.sonar.server.qualitygate.Condition;
+import org.sonar.server.qualitygate.EvaluatedCondition;
+import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.QualityGate;
+import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
+import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
+import org.sonar.server.qualitygate.changeevent.Trigger;
+import org.sonar.server.tester.UserSessionRule;
+
+import static java.lang.String.valueOf;
+import static java.util.Collections.singletonList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
+import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY;
+import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
+import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
+import static org.sonar.db.component.BranchType.LONG;
+import static org.sonar.db.component.ComponentTesting.newBranchDto;
+import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
+import static org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN;
+
+public class WebhookQGChangeEventListenerTest {
+  private static final List<String> OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED);
+  private static final List<String> NON_OPEN_STATUSES = Issue.STATUSES.stream().filter(OPEN_STATUSES::contains).collect(MoreCollectors.toList());
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+  @Rule
+  public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));
+  @Rule
+  public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+  private DbClient dbClient = dbTester.getDbClient();
+
+  private Random random = new Random();
+  private String randomResolution = Issue.RESOLUTIONS.get(random.nextInt(Issue.RESOLUTIONS.size()));
+  private String randomOpenStatus = OPEN_STATUSES.get(random.nextInt(OPEN_STATUSES.size()));
+  private String randomNonOpenStatus = NON_OPEN_STATUSES.get(random.nextInt(NON_OPEN_STATUSES.size()));
+
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient()));
+  private WebHooks webHooks = mock(WebHooks.class);
+  private WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class);
+  private IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule));
+  private DbClient spiedOnDbClient = Mockito.spy(dbClient);
+  private WebhookQGChangeEventListener underTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, issueIndex, spiedOnDbClient, System2.INSTANCE);
+  private DbClient mockedDbClient = mock(DbClient.class);
+  private IssueIndex spiedOnIssueIndex = Mockito.spy(issueIndex);
+  private WebhookQGChangeEventListener mockedUnderTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient, System2.INSTANCE);
+
+  @Test
+  public void onChanges_has_no_effect_if_changeEvents_is_empty() {
+    mockedUnderTest.onChanges(Trigger.ISSUE_CHANGE, Collections.emptyList());
+
+    verifyZeroInteractions(webHooks, webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient);
+  }
+
+  @Test
+  public void onChanges_has_no_effect_if_no_webhook_is_configured() {
+    Configuration configuration1 = mock(Configuration.class);
+    Configuration configuration2 = mock(Configuration.class);
+    mockWebhookDisabled(configuration1, configuration2);
+
+    mockedUnderTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of(
+      new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration1),
+      new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration2)));
+
+    verify(webHooks).isEnabled(configuration1);
+    verify(webHooks).isEnabled(configuration2);
+    verifyZeroInteractions(webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient);
+  }
+
+  @Test
+  public void onChanges_calls_webhook_for_changeEvent_with_webhook_enabled() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPublicProject(organization);
+    ComponentAndBranch branch = insertProjectBranch(project, BranchType.SHORT, "foo");
+    SnapshotDto analysis = insertAnalysisTask(branch);
+    Configuration configuration = mock(Configuration.class);
+    mockWebhookEnabled(configuration);
+    mockPayloadSupplierConsumedByWebhooks();
+    Map<String, String> properties = new HashMap<>();
+    properties.put("sonar.analysis.test1", randomAlphanumeric(50));
+    properties.put("sonar.analysis.test2", randomAlphanumeric(5000));
+    insertPropertiesFor(analysis.getUuid(), properties);
+
+    underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
+
+    ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
+    Condition condition1 = new Condition(BUGS_KEY, GREATER_THAN, "0", null, false);
+    Condition condition2 = new Condition(VULNERABILITIES_KEY, GREATER_THAN, "0", null, false);
+    Condition condition3 = new Condition(CODE_SMELLS_KEY, GREATER_THAN, "0", null, false);
+    assertThat(projectAnalysis).isEqualTo(
+      new ProjectAnalysis(
+        new Project(project.uuid(), project.getKey(), project.name()),
+        null,
+        new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
+        new Branch(false, "foo", Branch.Type.SHORT),
+        EvaluatedQualityGate.newBuilder()
+          .setQualityGate(
+            new QualityGate(
+              valueOf(ShortLivingBranchQualityGate.ID),
+              ShortLivingBranchQualityGate.NAME,
+              ImmutableSet.of(condition1, condition2, condition3)))
+          .setStatus(EvaluatedQualityGate.Status.OK)
+          .addCondition(condition1, EvaluationStatus.OK, "0")
+          .addCondition(condition2, EvaluationStatus.OK, "0")
+          .addCondition(condition3, EvaluationStatus.OK, "0")
+          .build(),
+        null,
+        properties));
+  }
+
+  @Test
+  public void onChanges_does_not_call_webhook_if_disabled_for_QGChangeEvent() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = dbTester.components().insertPublicProject(organization);
+    ComponentAndBranch branch1 = insertProjectBranch(project, BranchType.SHORT, "foo");
+    ComponentAndBranch branch2 = insertProjectBranch(project, BranchType.SHORT, "bar");
+    SnapshotDto analysis1 = insertAnalysisTask(branch1);
+    SnapshotDto analysis2 = insertAnalysisTask(branch2);
+    Configuration configuration1 = mock(Configuration.class);
+    Configuration configuration2 = mock(Configuration.class);
+    mockWebhookDisabled(configuration1);
+    mockWebhookEnabled(configuration2);
+    mockPayloadSupplierConsumedByWebhooks();
+
+    underTest.onChanges(
+      Trigger.ISSUE_CHANGE,
+      ImmutableList.of(
+        newQGChangeEvent(branch1, analysis1, configuration1),
+        newQGChangeEvent(branch2, analysis2, configuration2)));
+
+    verifyWebhookNotCalled(branch1, analysis1, configuration1);
+    verifyWebhookCalled(branch2, analysis2, configuration2);
+  }
+
+  @Test
+  public void onChanges_calls_webhook_for_any_type_of_branch() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch mainBranch = insertMainBranch(organization);
+    ComponentAndBranch longBranch = insertProjectBranch(mainBranch.component, BranchType.LONG, "foo");
+    SnapshotDto analysis1 = insertAnalysisTask(mainBranch);
+    SnapshotDto analysis2 = insertAnalysisTask(longBranch);
+    Configuration configuration1 = mock(Configuration.class);
+    Configuration configuration2 = mock(Configuration.class);
+    mockWebhookEnabled(configuration1, configuration2);
+
+    underTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of(
+      newQGChangeEvent(mainBranch, analysis1, configuration1),
+      newQGChangeEvent(longBranch, analysis2, configuration2)));
+
+    verifyWebhookCalled(mainBranch, analysis1, configuration1);
+    verifyWebhookCalled(longBranch, analysis2, configuration2);
+  }
+
+  @Test
+  public void onChanges_calls_webhook_once_per_QGChangeEvent_even_for_same_branch_and_configuration() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT);
+    SnapshotDto analysis1 = insertAnalysisTask(branch1);
+    Configuration configuration1 = mock(Configuration.class);
+    mockWebhookEnabled(configuration1);
+    mockPayloadSupplierConsumedByWebhooks();
+
+    underTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of(
+      newQGChangeEvent(branch1, analysis1, configuration1),
+      newQGChangeEvent(branch1, analysis1, configuration1),
+      newQGChangeEvent(branch1, analysis1, configuration1)));
+
+    verify(webHooks, times(3)).isEnabled(configuration1);
+    verify(webHooks, times(3)).sendProjectAnalysisUpdate(
+      Matchers.same(configuration1),
+      Matchers.eq(new WebHooks.Analysis(branch1.uuid(), analysis1.getUuid(), null)),
+      any(Supplier.class));
+    extractPayloadFactoryArguments(3);
+  }
+
+  @Test
+  public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT);
+    SnapshotDto analysis = insertAnalysisTask(branch);
+    Configuration configuration = mock(Configuration.class);
+    mockWebhookEnabled(configuration);
+    mockPayloadSupplierConsumedByWebhooks();
+
+    underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
+
+    ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
+    EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
+    assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.OK);
+    assertThat(qualityGate.getEvaluatedConditions())
+      .extracting(EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
+      .containsOnly(tuple(EvaluationStatus.OK, Optional.of("0")));
+  }
+
+  @Test
+  public void computes_QG_error_if_there_is_one_unresolved_bug_issue_in_index_ignoring_permissions() {
+    int unresolvedIssues = 1 + random.nextInt(10);
+
+    computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+      unresolvedIssues,
+      RuleType.BUG,
+      tuple(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
+      tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")),
+      tuple(CODE_SMELLS_KEY, EvaluationStatus.OK, Optional.of("0")));
+  }
+
+  @Test
+  public void computes_QG_error_if_there_is_one_unresolved_vulnerability_issue_in_index_ignoring_permissions() {
+    int unresolvedIssues = 1 + random.nextInt(10);
+
+    computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+      unresolvedIssues,
+      RuleType.VULNERABILITY,
+      tuple(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")),
+      tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
+      tuple(CODE_SMELLS_KEY, EvaluationStatus.OK, Optional.of("0")));
+  }
+
+  @Test
+  public void computes_QG_error_if_there_is_one_unresolved_codeSmell_issue_in_index_ignoring_permissions() {
+    int unresolvedIssues = 1 + random.nextInt(10);
+
+    computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+      unresolvedIssues,
+      RuleType.CODE_SMELL,
+      tuple(BUGS_KEY, EvaluationStatus.OK, Optional.of("0")),
+      tuple(VULNERABILITIES_KEY, EvaluationStatus.OK, Optional.of("0")),
+      tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))));
+  }
+
+  private void computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+    int unresolvedIssues, RuleType ruleType, Tuple... expectedQGConditions) {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT);
+    SnapshotDto analysis = insertAnalysisTask(branch);
+    IntStream.range(0, unresolvedIssues).forEach(i -> insertIssue(branch, ruleType, randomOpenStatus, null));
+    IntStream.range(0, random.nextInt(10)).forEach(i -> insertIssue(branch, ruleType, randomNonOpenStatus, randomResolution));
+    indexIssues(branch);
+    Configuration configuration = mock(Configuration.class);
+    mockWebhookEnabled(configuration);
+    mockPayloadSupplierConsumedByWebhooks();
+
+    underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
+
+    ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
+    EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
+    assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
+    assertThat(qualityGate.getEvaluatedConditions())
+      .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
+      .containsOnly(expectedQGConditions);
+  }
+
+  @Test
+  public void computes_QG_error_with_all_failing_conditions() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentAndBranch branch = insertPrivateBranch(organization, BranchType.SHORT);
+    SnapshotDto analysis = insertAnalysisTask(branch);
+    int unresolvedBugs = 1 + random.nextInt(10);
+    int unresolvedVulnerabilities = 1 + random.nextInt(10);
+    int unresolvedCodeSmells = 1 + random.nextInt(10);
+    IntStream.range(0, unresolvedBugs).forEach(i -> insertIssue(branch, RuleType.BUG, randomOpenStatus, null));
+    IntStream.range(0, unresolvedVulnerabilities).forEach(i -> insertIssue(branch, RuleType.VULNERABILITY, randomOpenStatus, null));
+    IntStream.range(0, unresolvedCodeSmells).forEach(i -> insertIssue(branch, RuleType.CODE_SMELL, randomOpenStatus, null));
+    indexIssues(branch);
+    Configuration configuration = mock(Configuration.class);
+    mockWebhookEnabled(configuration);
+    mockPayloadSupplierConsumedByWebhooks();
+
+    underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
+
+    ProjectAnalysis projectAnalysis = verifyWebhookCalledAndExtractPayloadFactoryArgument(branch, configuration, analysis);
+    EvaluatedQualityGate qualityGate = projectAnalysis.getQualityGate().get();
+    assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
+    assertThat(qualityGate.getEvaluatedConditions())
+      .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
+      .containsOnly(
+        Tuple.tuple(BUGS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedBugs))),
+        Tuple.tuple(VULNERABILITIES_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedVulnerabilities))),
+        Tuple.tuple(CODE_SMELLS_KEY, EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedCodeSmells))));
+  }
+
+  private void mockWebhookEnabled(Configuration... configurations) {
+    for (Configuration configuration : configurations) {
+      Mockito.when(webHooks.isEnabled(configuration)).thenReturn(true);
+    }
+  }
+
+  private void mockWebhookDisabled(Configuration... configurations) {
+    for (Configuration configuration : configurations) {
+      Mockito.when(webHooks.isEnabled(configuration)).thenReturn(false);
+    }
+  }
+
+  private void mockPayloadSupplierConsumedByWebhooks() {
+    Mockito.doAnswer(invocationOnMock -> {
+      Supplier<WebhookPayload> supplier = (Supplier<WebhookPayload>) invocationOnMock.getArguments()[2];
+      supplier.get();
+      return null;
+    }).when(webHooks)
+      .sendProjectAnalysisUpdate(Matchers.any(Configuration.class), Matchers.any(), Matchers.any());
+  }
+
+  private void insertIssue(ComponentAndBranch componentAndBranch, RuleType ruleType, String status, @Nullable String resolution) {
+    ComponentDto component = componentAndBranch.component;
+    RuleDefinitionDto rule = RuleTesting.newRule();
+    dbTester.rules().insert(rule);
+    dbTester.commit();
+    dbTester.issues().insert(rule, component, component, i -> i.setType(ruleType).setStatus(status).setResolution(resolution));
+    dbTester.commit();
+  }
+
+  private void insertPropertiesFor(String snapshotUuid, Map<String, String> properties) {
+    List<AnalysisPropertyDto> analysisProperties = properties.entrySet().stream()
+      .map(entry -> new AnalysisPropertyDto()
+        .setUuid(UuidFactoryFast.getInstance().create())
+        .setSnapshotUuid(snapshotUuid)
+        .setKey(entry.getKey())
+        .setValue(entry.getValue()))
+      .collect(toArrayList(properties.size()));
+    dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties);
+    dbTester.getSession().commit();
+  }
+
+  private SnapshotDto insertAnalysisTask(ComponentAndBranch componentAndBranch) {
+    return dbTester.components().insertSnapshot(componentAndBranch.component);
+  }
+
+  private ProjectAnalysis verifyWebhookCalledAndExtractPayloadFactoryArgument(ComponentAndBranch componentAndBranch, Configuration configuration, SnapshotDto analysis) {
+    verifyWebhookCalled(componentAndBranch, analysis, configuration);
+
+    return extractPayloadFactoryArguments(1).iterator().next();
+  }
+
+  private void verifyWebhookCalled(ComponentAndBranch componentAndBranch, SnapshotDto analysis, Configuration branchConfiguration) {
+    verify(webHooks).isEnabled(branchConfiguration);
+    verify(webHooks).sendProjectAnalysisUpdate(
+      Matchers.same(branchConfiguration),
+      Matchers.eq(new WebHooks.Analysis(componentAndBranch.uuid(), analysis.getUuid(), null)),
+      any(Supplier.class));
+  }
+
+  private void verifyWebhookNotCalled(ComponentAndBranch componentAndBranch, SnapshotDto analysis, Configuration branchConfiguration) {
+    verify(webHooks).isEnabled(branchConfiguration);
+    verify(webHooks, times(0)).sendProjectAnalysisUpdate(
+      Matchers.same(branchConfiguration),
+      Matchers.eq(new WebHooks.Analysis(componentAndBranch.uuid(), analysis.getUuid(), null)),
+      any(Supplier.class));
+  }
+
+  private List<ProjectAnalysis> extractPayloadFactoryArguments(int time) {
+    ArgumentCaptor<ProjectAnalysis> projectAnalysisCaptor = ArgumentCaptor.forClass(ProjectAnalysis.class);
+    verify(webhookPayloadFactory, Mockito.times(time)).create(projectAnalysisCaptor.capture());
+    return projectAnalysisCaptor.getAllValues();
+  }
+
+  private void indexIssues(ComponentAndBranch componentAndBranch) {
+    issueIndexer.indexOnAnalysis(componentAndBranch.uuid());
+  }
+
+  private ComponentAndBranch insertPrivateBranch(OrganizationDto organization, BranchType branchType) {
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    BranchDto branchDto = newBranchDto(project.projectUuid(), branchType)
+      .setKey("foo");
+    ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto);
+    return new ComponentAndBranch(newComponent, branchDto);
+  }
+
+  public ComponentAndBranch insertMainBranch(OrganizationDto organization) {
+    ComponentDto project = newPrivateProjectDto(organization);
+    BranchDto branch = newBranchDto(project, LONG).setKey("master");
+    dbTester.components().insertComponent(project);
+    dbClient.branchDao().insert(dbTester.getSession(), branch);
+    dbTester.commit();
+    return new ComponentAndBranch(project, branch);
+  }
+
+  public ComponentAndBranch insertProjectBranch(ComponentDto project, BranchType type, String branchKey) {
+    BranchDto branchDto = newBranchDto(project.projectUuid(), type).setKey(branchKey);
+    ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto);
+    return new ComponentAndBranch(newComponent, branchDto);
+  }
+
+  private static class ComponentAndBranch {
+    private final ComponentDto component;
+
+    private final BranchDto branch;
+
+    private ComponentAndBranch(ComponentDto component, BranchDto branch) {
+      this.component = component;
+      this.branch = branch;
+    }
+
+    public ComponentDto getComponent() {
+      return component;
+    }
+
+    public BranchDto getBranch() {
+      return branch;
+    }
+
+    public String uuid() {
+      return component.uuid();
+    }
+
+  }
+
+  private static QGChangeEvent newQGChangeEvent(ComponentAndBranch branch, SnapshotDto analysis, Configuration configuration) {
+    return new QGChangeEvent(branch.component, branch.branch, analysis, configuration);
+  }
+
+}