]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10085 include EvaluatedQualityGate in QGChangeEvent 2808/head
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 17 Nov 2017 13:00:26 +0000 (14:00 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 24 Nov 2017 08:23:58 +0000 (09:23 +0100)
13 files changed:
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImpl.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEvent.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/IssueChangeTriggerImplTest.java
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java

index 2c750380e4daeda0f339fab5c2232aa9b6cb873a..5a0e17d044e8df80392bf980bddc1c8bef5bc9bf 100644 (file)
@@ -28,6 +28,7 @@ import org.sonar.server.issue.ServerIssueStorage;
 import org.sonar.server.issue.TransitionService;
 import org.sonar.server.issue.workflow.FunctionExecutor;
 import org.sonar.server.issue.workflow.IssueWorkflow;
+import org.sonar.server.qualitygate.LiveQualityGateFactoryImpl;
 import org.sonar.server.qualitygate.changeevent.IssueChangeTriggerImpl;
 import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl;
 import org.sonar.server.settings.ProjectConfigurationLoaderImpl;
@@ -67,6 +68,7 @@ public class IssueWsModule extends Module {
       ChangelogAction.class,
       BulkChangeAction.class,
       ProjectConfigurationLoaderImpl.class,
+      LiveQualityGateFactoryImpl.class,
       IssueChangeTriggerImpl.class,
       WebhookQGChangeEventListener.class,
       QGChangeEventListenersImpl.class);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java
new file mode 100644 (file)
index 0000000..ec23ac9
--- /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;
+
+import org.sonar.db.component.ComponentDto;
+
+public interface LiveQualityGateFactory {
+  EvaluatedQualityGate buildForShortLivedBranch(ComponentDto componentDto);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java
new file mode 100644 (file)
index 0000000..b118693
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * 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;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+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.db.component.ComponentDto;
+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.rule.index.RuleIndex;
+
+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 LiveQualityGateFactoryImpl implements LiveQualityGateFactory {
+
+  private final IssueIndex issueIndex;
+  private final System2 system2;
+
+  public LiveQualityGateFactoryImpl(IssueIndex issueIndex, System2 system2) {
+    this.issueIndex = issueIndex;
+    this.system2 = system2;
+  }
+
+  @Override
+  public EvaluatedQualityGate buildForShortLivedBranch(ComponentDto componentDto) {
+    return createQualityGate(componentDto, issueIndex);
+  }
+
+  private EvaluatedQualityGate createQualityGate(ComponentDto project, IssueIndex issueIndex) {
+    SearchResponse searchResponse = issueIndex.search(IssueQuery.builder()
+            .projectUuids(singletonList(project.getMainBranchProjectUuid()))
+            .branchUuid(project.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);
+          EvaluatedCondition.EvaluationStatus status = measure > 0 ? EvaluatedCondition.EvaluationStatus.ERROR : EvaluatedCondition.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() == EvaluatedCondition.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;
+  }
+}
index 25af68798c922be1e0fd61a2386ffba5ce34d6c2..bdca8f10ad73883b55acf2c8bbf014c78251a008 100644 (file)
@@ -24,6 +24,7 @@ import com.google.common.collect.Sets;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -38,6 +39,7 @@ 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.qualitygate.LiveQualityGateFactory;
 import org.sonar.server.settings.ProjectConfigurationLoader;
 
 import static org.sonar.core.util.stream.MoreCollectors.toSet;
@@ -49,11 +51,14 @@ public class IssueChangeTriggerImpl implements IssueChangeTrigger {
   private final DbClient dbClient;
   private final ProjectConfigurationLoader projectConfigurationLoader;
   private final QGChangeEventListeners qgEventListeners;
+  private final LiveQualityGateFactory liveQualityGateFactory;
 
-  public IssueChangeTriggerImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader, QGChangeEventListeners qgEventListeners) {
+  public IssueChangeTriggerImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader,
+    QGChangeEventListeners qgEventListeners, LiveQualityGateFactory liveQualityGateFactory) {
     this.dbClient = dbClient;
     this.projectConfigurationLoader = projectConfigurationLoader;
     this.qgEventListeners = qgEventListeners;
+    this.liveQualityGateFactory = liveQualityGateFactory;
   }
 
   @Override
@@ -62,7 +67,7 @@ public class IssueChangeTriggerImpl implements IssueChangeTrigger {
       return;
     }
 
-    callWebHook(issueChangeData);
+    broadcastToListeners(issueChangeData);
   }
 
   private static boolean isRelevant(IssueChange issueChange) {
@@ -81,7 +86,7 @@ public class IssueChangeTriggerImpl implements IssueChangeTrigger {
     return MEANINGFUL_TRANSITIONS.contains(transitionKey);
   }
 
-  private void callWebHook(IssueChangeData issueChangeData) {
+  private void broadcastToListeners(IssueChangeData issueChangeData) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       Map<String, ComponentDto> branchesByUuid = getBranchComponents(dbSession, issueChangeData);
       if (branchesByUuid.isEmpty()) {
@@ -116,7 +121,8 @@ public class IssueChangeTriggerImpl implements IssueChangeTrigger {
           if (branch != null && analysis != null) {
             Configuration configuration = configurationByUuid.get(shortBranch.getUuid());
 
-            return new QGChangeEvent(branch, shortBranch, analysis, configuration);
+            return new QGChangeEvent(branch, shortBranch, analysis, configuration,
+              () -> Optional.of(liveQualityGateFactory.buildForShortLivedBranch(branch)));
           }
           return null;
         })
index a3d82c6985615850c949c0692e3f117541a5aa93..93ba771ffbabf89cf57f9866f1ffba84808d48c9 100644 (file)
  */
 package org.sonar.server.qualitygate.changeevent;
 
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
+import java.util.Optional;
+import java.util.function.Supplier;
+import javax.annotation.concurrent.Immutable;
 import org.sonar.api.config.Configuration;
 import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.SnapshotDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
 
+import static java.util.Objects.requireNonNull;
+
+@Immutable
 public class QGChangeEvent {
   private final ComponentDto project;
   private final BranchDto branch;
   private final SnapshotDto analysis;
   private final Configuration projectConfiguration;
+  private final Supplier<Optional<EvaluatedQualityGate>> qualityGateSupplier;
 
-  public QGChangeEvent(ComponentDto project, BranchDto branch, SnapshotDto analysis, Configuration projectConfiguration) {
-    this.branch = branch;
-    this.project = project;
-    this.analysis = analysis;
-    this.projectConfiguration = projectConfiguration;
+  public QGChangeEvent(ComponentDto project, BranchDto branch, SnapshotDto analysis, Configuration projectConfiguration,
+    Supplier<Optional<EvaluatedQualityGate>> qualityGateSupplier) {
+    this.project = requireNonNull(project, "project can't be null");
+    this.branch = requireNonNull(branch, "branch can't be null");
+    this.analysis = requireNonNull(analysis, "analysis can't be null");
+    this.projectConfiguration = requireNonNull(projectConfiguration, "projectConfiguration can't be null");
+    this.qualityGateSupplier = requireNonNull(qualityGateSupplier, "qualityGateSupplier can't be null");
   }
 
   public BranchDto getBranch() {
@@ -55,37 +63,30 @@ public class QGChangeEvent {
     return projectConfiguration;
   }
 
+  public Supplier<Optional<EvaluatedQualityGate>> getQualityGateSupplier() {
+    return qualityGateSupplier;
+  }
+
   @Override
   public String toString() {
     return "QGChangeEvent{" +
-      "branch=" + toString(branch) +
-      ", project=" + toString(project) +
+      "project=" + toString(project) +
+      ", branch=" + toString(branch) +
       ", analysis=" + toString(analysis) +
       ", projectConfiguration=" + projectConfiguration +
+      ", qualityGateSupplier=" + qualityGateSupplier +
       '}';
   }
 
-  @CheckForNull
-  private static String toString(@Nullable BranchDto shortBranch) {
-    if (shortBranch == null) {
-      return null;
-    }
-    return shortBranch.getBranchType() + ":" + shortBranch.getUuid() + ":" + shortBranch.getProjectUuid() + ":" + shortBranch.getMergeBranchUuid();
+  private static String toString(ComponentDto project) {
+    return project.uuid() + ":" + project.getKey();
   }
 
-  @CheckForNull
-  private static String toString(@Nullable ComponentDto shortBranchComponent) {
-    if (shortBranchComponent == null) {
-      return null;
-    }
-    return shortBranchComponent.uuid() + ":" + shortBranchComponent.getKey();
+  private static String toString(BranchDto branch) {
+    return branch.getBranchType() + ":" + branch.getUuid() + ":" + branch.getProjectUuid() + ":" + branch.getMergeBranchUuid();
   }
 
-  @CheckForNull
-  private static String toString(@Nullable SnapshotDto latestAnalysis) {
-    if (latestAnalysis == null) {
-      return null;
-    }
-    return latestAnalysis.getUuid() + ":" + latestAnalysis.getCreatedAt();
+  private static String toString(SnapshotDto analysis) {
+    return analysis.getUuid() + ":" + analysis.getCreatedAt();
   }
 }
index 1c1d3d8750dc5b43a7f4bad3af3eb4889a3fa1c1..a697aad3f363a8a6850177ac9bc8519338b7b21c 100644 (file)
  */
 package org.sonar.server.qualitygate.changeevent;
 
+import com.google.common.collect.ImmutableList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 
@@ -56,16 +58,21 @@ public class QGChangeEventListenersImpl implements QGChangeEventListeners {
 
   @Override
   public void broadcast(Trigger trigger, Collection<QGChangeEvent> changeEvents) {
+    if (changeEvents.isEmpty()) {
+      return;
+    }
+
     try {
-      Arrays.stream(listeners).forEach(listener -> broadcastTo(trigger, changeEvents, listener));
+      List<QGChangeEvent> immutableChangeEvents = ImmutableList.copyOf(changeEvents);
+      Arrays.stream(listeners).forEach(listener -> broadcastTo(trigger, immutableChangeEvents, 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) {
+  private static void broadcastTo(Trigger trigger, Collection<QGChangeEvent> changeEvents, QGChangeEventListener listener) {
     try {
-      LOG.debug("calling onChange() on listener {} for events {}...", listener.getClass().getName(), changeEvents);
+      LOG.trace("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);
index 4f3c6baf8fd5c854ab3dc617b7f8f50580fbc70c..59a2204cba43dd272e54c16ca78a759ff853d753 100644 (file)
 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;
@@ -36,47 +30,19 @@ 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) {
+  public WebhookQGChangeEventListener(WebHooks webhooks, WebhookPayloadFactory webhookPayloadFactory, DbClient dbClient) {
     this.webhooks = webhooks;
     this.webhookPayloadFactory = webhookPayloadFactory;
-    this.issueIndex = issueIndex;
     this.dbClient = dbClient;
-    this.system2 = system2;
   }
 
   @Override
@@ -101,10 +67,13 @@ public class WebhookQGChangeEventListener implements QGChangeEventListener {
     webhooks.sendProjectAnalysisUpdate(
       event.getProjectConfiguration(),
       new WebHooks.Analysis(event.getBranch().getUuid(), event.getAnalysis().getUuid(), null),
-      () -> buildWebHookPayload(dbSession, event.getProject(), event.getBranch(), event.getAnalysis()));
+      () -> buildWebHookPayload(dbSession, event));
   }
 
-  private WebhookPayload buildWebHookPayload(DbSession dbSession, ComponentDto branch, BranchDto shortBranch, SnapshotDto analysis) {
+  private WebhookPayload buildWebHookPayload(DbSession dbSession, QGChangeEvent event) {
+    ComponentDto branch = event.getProject();
+    BranchDto shortBranch = event.getBranch();
+    SnapshotDto analysis = event.getAnalysis();
     Map<String, String> analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid())
       .stream()
       .collect(Collectors.toMap(AnalysisPropertyDto::getKey, AnalysisPropertyDto::getValue));
@@ -113,87 +82,10 @@ public class WebhookQGChangeEventListener implements QGChangeEventListener {
       null,
       new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
       new Branch(false, shortBranch.getKey(), Branch.Type.SHORT),
-      createQualityGate(branch, issueIndex),
+      event.getQualityGateSupplier().get().orElse(null),
       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;
-  }
 }
index e4a4bdea80ba029a3a8af6ba0ee2b3adac676804..903f908c965447c846854ddf2b58a54b2cd3c6e3 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 + 32);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 33);
   }
 }
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java
new file mode 100644 (file)
index 0000000..9337e87
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+ * 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;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+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.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.stream.MoreCollectors;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+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.tester.UserSessionRule;
+
+import static java.lang.String.valueOf;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+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.db.component.ComponentTesting.newBranchDto;
+
+public class LiveQualityGateFactoryImplTest {
+  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 Random random = new Random();
+  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 String randomResolution = Issue.RESOLUTIONS.get(random.nextInt(Issue.RESOLUTIONS.size()));
+  private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient()));
+  private IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule));
+
+  private LiveQualityGateFactoryImpl underTest = new LiveQualityGateFactoryImpl(issueIndex, System2.INSTANCE);
+
+  @Test
+  public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = insertPrivateBranch(organization, BranchType.SHORT);
+
+    EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project);
+
+    assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.OK);
+    assertThat(qualityGate.getEvaluatedConditions())
+      .extracting(EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
+      .containsOnly(tuple(EvaluatedCondition.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, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
+      tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")),
+      tuple(CODE_SMELLS_KEY, EvaluatedCondition.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, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")),
+      tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))),
+      tuple(CODE_SMELLS_KEY, EvaluatedCondition.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, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")),
+      tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")),
+      tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))));
+  }
+
+  private void computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions(
+    int unresolvedIssues, RuleType ruleType, Tuple... expectedQGConditions) {
+    OrganizationDto organization = dbTester.organizations().insert();
+    ComponentDto project = insertPrivateBranch(organization, BranchType.SHORT);
+    IntStream.range(0, unresolvedIssues).forEach(i -> insertIssue(project, ruleType, randomOpenStatus, null));
+    IntStream.range(0, random.nextInt(10)).forEach(i -> insertIssue(project, ruleType, randomNonOpenStatus, randomResolution));
+    indexIssues(project);
+
+    EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project);
+
+    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 project = insertPrivateBranch(organization, BranchType.SHORT);
+    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(project, RuleType.BUG, randomOpenStatus, null));
+    IntStream.range(0, unresolvedVulnerabilities).forEach(i -> insertIssue(project, RuleType.VULNERABILITY, randomOpenStatus, null));
+    IntStream.range(0, unresolvedCodeSmells).forEach(i -> insertIssue(project, RuleType.CODE_SMELL, randomOpenStatus, null));
+    indexIssues(project);
+
+    EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project);
+
+    assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR);
+    assertThat(qualityGate.getEvaluatedConditions())
+      .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue)
+      .containsOnly(
+        Tuple.tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedBugs))),
+        Tuple.tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedVulnerabilities))),
+        Tuple.tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedCodeSmells))));
+  }
+
+  private void indexIssues(ComponentDto project) {
+    issueIndexer.indexOnAnalysis(project.uuid());
+  }
+
+  private void insertIssue(ComponentDto component, RuleType ruleType, String status, @Nullable String resolution) {
+    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 ComponentDto insertPrivateBranch(OrganizationDto organization, BranchType branchType) {
+    ComponentDto project = dbTester.components().insertPrivateProject(organization);
+    BranchDto branchDto = newBranchDto(project.projectUuid(), branchType)
+      .setKey("foo");
+    return dbTester.components().insertProjectBranch(project, branchDto);
+  }
+
+}
index d0d48fb01ec2d1b8759e4cb27b8edb27bf60f30e..9489f4695105de84fcf44e8ee8a4ee1aac09dc13 100644 (file)
@@ -66,6 +66,7 @@ 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.LiveQualityGateFactory;
 import org.sonar.server.qualitygate.changeevent.IssueChangeTrigger.IssueChange;
 import org.sonar.server.settings.ProjectConfigurationLoader;
 import org.sonar.server.tester.UserSessionRule;
@@ -101,9 +102,10 @@ public class IssueChangeTriggerImplTest {
   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 LiveQualityGateFactory liveQualityGateFactory = mock(LiveQualityGateFactory.class);
+  private IssueChangeTriggerImpl underTest = new IssueChangeTriggerImpl(spiedOnDbClient, projectConfigurationLoader, qgChangeEventListeners, liveQualityGateFactory);
   private DbClient mockedDbClient = mock(DbClient.class);
-  private IssueChangeTriggerImpl mockedUnderTest = new IssueChangeTriggerImpl(mockedDbClient, projectConfigurationLoader, qgChangeEventListeners);
+  private IssueChangeTriggerImpl mockedUnderTest = new IssueChangeTriggerImpl(mockedDbClient, projectConfigurationLoader, qgChangeEventListeners, liveQualityGateFactory);
 
   @Test
   public void on_type_change_has_no_effect_if_SearchResponseData_has_no_issue() {
diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
new file mode 100644 (file)
index 0000000..9df7be5
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * 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.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class QGChangeEventListenersImplTest {
+  @Rule
+  public LogTester logTester = new LogTester();
+
+  private QGChangeEventListener listener1 = mock(QGChangeEventListener.class);
+  private QGChangeEventListener listener2 = mock(QGChangeEventListener.class);
+  private QGChangeEventListener listener3 = mock(QGChangeEventListener.class);
+  private InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
+  private List<QGChangeEvent> threeChangeEvents = Arrays.asList(mock(QGChangeEvent.class), mock(QGChangeEvent.class));
+
+  private QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener1, listener2, listener3});
+
+  @Test
+  public void isEmpty_returns_true_for_constructor_without_argument() {
+    QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl();
+
+    assertThat(underTest.isEmpty()).isTrue();
+  }
+
+  @Test
+  public void isEmpty_returns_false_for_constructor_with_one_argument() {
+    QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener2});
+
+    assertThat(underTest.isEmpty()).isFalse();
+  }
+
+  @Test
+  public void isEmpty_returns_false_for_constructor_with_multiple_arguments() {
+    QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener2, listener3});
+
+    assertThat(underTest.isEmpty()).isFalse();
+  }
+
+  @Test
+  public void no_effect_when_no_changeEvent() {
+    underTest.broadcast(Trigger.ISSUE_CHANGE, Collections.emptySet());
+
+    verifyZeroInteractions(listener1, listener2, listener3);
+  }
+
+  @Test
+  public void broadcast_passes_Trigger_and_collection_to_all_listeners_in_order_of_addition_to_constructor() {
+    underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+    inOrder.verify(listener1).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+    inOrder.verify(listener2).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+    inOrder.verify(listener3).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+    inOrder.verifyNoMoreInteractions();
+  }
+
+  @Test
+  public void broadcast_calls_all_listeners_even_if_one_throws_an_exception() {
+    QGChangeEventListener failingListener = new QGChangeEventListener[] {listener1, listener2, listener3}[new Random().nextInt(3)];
+    doThrow(new RuntimeException("Faking an exception thrown by onChanges"))
+      .when(failingListener)
+      .onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+    underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+    inOrder.verify(listener1).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+    inOrder.verify(listener2).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+    inOrder.verify(listener3).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+    inOrder.verifyNoMoreInteractions();
+    assertThat(logTester.logs()).hasSize(4);
+    assertThat(logTester.logs(LoggerLevel.WARN)).hasSize(1);
+  }
+
+  @Test
+  public void broadcast_stops_calling_listeners_when_one_throws_an_ERROR() {
+    doThrow(new Error("Faking an error thrown by a listener"))
+      .when(listener2)
+      .onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+    underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+    inOrder.verify(listener1).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+    inOrder.verify(listener2).onChanges(Trigger.ISSUE_CHANGE, threeChangeEvents);
+    inOrder.verifyNoMoreInteractions();
+    assertThat(logTester.logs()).hasSize(3);
+    assertThat(logTester.logs(LoggerLevel.WARN)).hasSize(1);
+  }
+
+  @Test
+  public void broadcast_logs_each_listener_call_at_TRACE_level() {
+    underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+    assertThat(logTester.logs()).hasSize(3);
+    List<String> traceLogs = logTester.logs(LoggerLevel.TRACE);
+    assertThat(traceLogs).hasSize(3)
+      .containsOnly(
+        "calling onChange() on listener " + listener1.getClass().getName() + " for events " + threeChangeEvents.toString() + "...",
+        "calling onChange() on listener " + listener2.getClass().getName() + " for events " + threeChangeEvents.toString() + "...",
+        "calling onChange() on listener " + listener3.getClass().getName() + " for events " + threeChangeEvents.toString() + "...");
+  }
+
+  @Test
+  public void broadcast_passes_immutable_list_of_events() {
+    QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener1});
+
+    underTest.broadcast(Trigger.ISSUE_CHANGE, threeChangeEvents);
+
+    ArgumentCaptor<Collection> collectionCaptor = ArgumentCaptor.forClass(Collection.class);
+    verify(listener1).onChanges(eq(Trigger.ISSUE_CHANGE), collectionCaptor.capture());
+    assertThat(collectionCaptor.getValue()).isInstanceOf(ImmutableList.class);
+  }
+
+  @Test
+  public void no_effect_when_no_listener() {
+    QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl();
+
+    underTest.broadcast(Trigger.ISSUE_CHANGE, Collections.emptySet());
+
+    verifyZeroInteractions(listener1, listener2, listener3);
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventTest.java
new file mode 100644 (file)
index 0000000..f8f5d8f
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * 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.Optional;
+import java.util.function.Supplier;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+import org.sonar.api.config.Configuration;
+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.qualitygate.EvaluatedQualityGate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class QGChangeEventTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private ComponentDto project = new ComponentDto()
+    .setDbKey("foo")
+    .setUuid("bar");
+  private BranchDto branch = new BranchDto()
+    .setBranchType(BranchType.SHORT)
+    .setUuid("bar")
+    .setProjectUuid("doh")
+    .setMergeBranchUuid("zop");
+  private SnapshotDto analysis = new SnapshotDto()
+    .setUuid("pto")
+    .setCreatedAt(8_999_999_765L);
+  private Configuration configuration = Mockito.mock(Configuration.class);
+  private Supplier<Optional<EvaluatedQualityGate>> supplier = Optional::empty;
+
+  @Test
+  public void constructor_fails_with_NPE_if_project_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("project can't be null");
+
+    new QGChangeEvent(null, branch, analysis, configuration, supplier);
+  }
+
+  @Test
+  public void constructor_fails_with_NPE_if_branch_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("branch can't be null");
+
+    new QGChangeEvent(project, null, analysis, configuration, supplier);
+  }
+
+  @Test
+  public void constructor_fails_with_NPE_if_analysis_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("analysis can't be null");
+
+    new QGChangeEvent(project, branch, null, configuration, supplier);
+  }
+
+  @Test
+  public void constructor_fails_with_NPE_if_configuration_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("projectConfiguration can't be null");
+
+    new QGChangeEvent(project, branch, analysis, null, supplier);
+  }
+
+  @Test
+  public void constructor_fails_with_NPE_if_supplier_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("qualityGateSupplier can't be null");
+
+    new QGChangeEvent(project, branch, analysis, configuration, null);
+  }
+
+  @Test
+  public void verify_getters() {
+    QGChangeEvent underTest = new QGChangeEvent(project, branch, analysis, configuration, supplier);
+
+    assertThat(underTest.getProject()).isSameAs(project);
+    assertThat(underTest.getBranch()).isSameAs(branch);
+    assertThat(underTest.getAnalysis()).isSameAs(analysis);
+    assertThat(underTest.getProjectConfiguration()).isSameAs(configuration);
+    assertThat(underTest.getQualityGateSupplier()).isSameAs(supplier);
+  }
+
+  @Test
+  public void overrides_toString() {
+    QGChangeEvent underTest = new QGChangeEvent(project, branch, analysis, configuration, supplier);
+
+    assertThat(underTest.toString())
+      .isEqualTo("QGChangeEvent{project=bar:foo, branch=SHORT:bar:doh:zop, analysis=pto:8999999765, projectConfiguration=" + configuration.toString() +
+        ", qualityGateSupplier=" + supplier + "}");
+
+  }
+}
index acbeca51332ce4204f016b1d8fb58f11b06f3e6d..3e7c4651195733acd9f18c518a72aa4e2bcdacdb 100644 (file)
 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;
@@ -51,76 +43,52 @@ 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.emptySet;
 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.mockito.Mockito.when;
 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());
+
+  private static final EvaluatedQualityGate EVALUATED_QUALITY_GATE_1 = EvaluatedQualityGate.newBuilder()
+    .setQualityGate(new QualityGate(valueOf(ShortLivingBranchQualityGate.ID), ShortLivingBranchQualityGate.NAME, emptySet()))
+    .setStatus(EvaluatedQualityGate.Status.OK)
+    .build();
 
   @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 WebhookQGChangeEventListener underTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, spiedOnDbClient);
   private DbClient mockedDbClient = mock(DbClient.class);
-  private IssueIndex spiedOnIssueIndex = Mockito.spy(issueIndex);
-  private WebhookQGChangeEventListener mockedUnderTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient, System2.INSTANCE);
+  private WebhookQGChangeEventListener mockedUnderTest = new WebhookQGChangeEventListener(webHooks, webhookPayloadFactory, mockedDbClient);
 
   @Test
   public void onChanges_has_no_effect_if_changeEvents_is_empty() {
     mockedUnderTest.onChanges(Trigger.ISSUE_CHANGE, Collections.emptyList());
 
-    verifyZeroInteractions(webHooks, webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient);
+    verifyZeroInteractions(webHooks, webhookPayloadFactory, mockedDbClient);
   }
 
   @Test
@@ -130,12 +98,12 @@ public class WebhookQGChangeEventListenerTest {
     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)));
+      new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration1, Optional::empty),
+      new QGChangeEvent(new ComponentDto(), new BranchDto(), new SnapshotDto(), configuration2, Optional::empty)));
 
     verify(webHooks).isEnabled(configuration1);
     verify(webHooks).isEnabled(configuration2);
-    verifyZeroInteractions(webhookPayloadFactory, spiedOnIssueIndex, mockedDbClient);
+    verifyZeroInteractions(webhookPayloadFactory, mockedDbClient);
   }
 
   @Test
@@ -152,29 +120,16 @@ public class WebhookQGChangeEventListenerTest {
     properties.put("sonar.analysis.test2", randomAlphanumeric(5000));
     insertPropertiesFor(analysis.getUuid(), properties);
 
-    underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration)));
+    underTest.onChanges(Trigger.ISSUE_CHANGE, singletonList(newQGChangeEvent(branch, analysis, configuration, EVALUATED_QUALITY_GATE_1)));
 
     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(),
+        EVALUATED_QUALITY_GATE_1,
         null,
         properties));
   }
@@ -196,8 +151,8 @@ public class WebhookQGChangeEventListenerTest {
     underTest.onChanges(
       Trigger.ISSUE_CHANGE,
       ImmutableList.of(
-        newQGChangeEvent(branch1, analysis1, configuration1),
-        newQGChangeEvent(branch2, analysis2, configuration2)));
+        newQGChangeEvent(branch1, analysis1, configuration1, null),
+        newQGChangeEvent(branch2, analysis2, configuration2, EVALUATED_QUALITY_GATE_1)));
 
     verifyWebhookNotCalled(branch1, analysis1, configuration1);
     verifyWebhookCalled(branch2, analysis2, configuration2);
@@ -215,8 +170,8 @@ public class WebhookQGChangeEventListenerTest {
     mockWebhookEnabled(configuration1, configuration2);
 
     underTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of(
-      newQGChangeEvent(mainBranch, analysis1, configuration1),
-      newQGChangeEvent(longBranch, analysis2, configuration2)));
+      newQGChangeEvent(mainBranch, analysis1, configuration1, EVALUATED_QUALITY_GATE_1),
+      newQGChangeEvent(longBranch, analysis2, configuration2, null)));
 
     verifyWebhookCalled(mainBranch, analysis1, configuration1);
     verifyWebhookCalled(longBranch, analysis2, configuration2);
@@ -232,9 +187,9 @@ public class WebhookQGChangeEventListenerTest {
     mockPayloadSupplierConsumedByWebhooks();
 
     underTest.onChanges(Trigger.ISSUE_CHANGE, ImmutableList.of(
-      newQGChangeEvent(branch1, analysis1, configuration1),
-      newQGChangeEvent(branch1, analysis1, configuration1),
-      newQGChangeEvent(branch1, analysis1, configuration1)));
+      newQGChangeEvent(branch1, analysis1, configuration1, null),
+      newQGChangeEvent(branch1, analysis1, configuration1, EVALUATED_QUALITY_GATE_1),
+      newQGChangeEvent(branch1, analysis1, configuration1, null)));
 
     verify(webHooks, times(3)).isEnabled(configuration1);
     verify(webHooks, times(3)).sendProjectAnalysisUpdate(
@@ -244,121 +199,15 @@ public class WebhookQGChangeEventListenerTest {
     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);
+      when(webHooks.isEnabled(configuration)).thenReturn(true);
     }
   }
 
   private void mockWebhookDisabled(Configuration... configurations) {
     for (Configuration configuration : configurations) {
-      Mockito.when(webHooks.isEnabled(configuration)).thenReturn(false);
+      when(webHooks.isEnabled(configuration)).thenReturn(false);
     }
   }
 
@@ -371,15 +220,6 @@ public class WebhookQGChangeEventListenerTest {
       .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()
@@ -424,10 +264,6 @@ public class WebhookQGChangeEventListenerTest {
     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)
@@ -475,8 +311,8 @@ public class WebhookQGChangeEventListenerTest {
 
   }
 
-  private static QGChangeEvent newQGChangeEvent(ComponentAndBranch branch, SnapshotDto analysis, Configuration configuration) {
-    return new QGChangeEvent(branch.component, branch.branch, analysis, configuration);
+  private static QGChangeEvent newQGChangeEvent(ComponentAndBranch branch, SnapshotDto analysis, Configuration configuration, @Nullable EvaluatedQualityGate evaluatedQualityGate) {
+    return new QGChangeEvent(branch.component, branch.branch, analysis, configuration, () -> Optional.ofNullable(evaluatedQualityGate));
   }
 
 }