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;
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;
}
+++ /dev/null
-/*
- * 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 +
- '}';
- }
- }
-}
+++ /dev/null
-/*
- * 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));
- }
-}
+++ /dev/null
-/*
- * 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;
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;
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
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,
};
}
- 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)
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) {
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;
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;
this.transitionService = transitionService;
this.responseWriter = responseWriter;
this.system2 = system2;
- this.issueChangeWebhook = issueChangeWebhook;
+ this.issueChangeTrigger = issueChangeTrigger;
}
@Override
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;
}
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 {
ChangelogAction.class,
BulkChangeAction.class,
ProjectConfigurationLoaderImpl.class,
- IssueChangeWebhookImpl.class);
+ IssueChangeTriggerImpl.class,
+ WebhookQGChangeEventListener.class,
+ QGChangeEventListenersImpl.class);
}
}
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;
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;
this.issueUpdater = issueUpdater;
this.responseWriter = responseWriter;
this.system2 = system2;
- this.issueChangeWebhook = issueChangeWebhook;
+ this.issueChangeTrigger = issueChangeTrigger;
}
@Override
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;
}
--- /dev/null
+/*
+ * 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 +
+ '}';
+ }
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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;
--- /dev/null
+/*
+ * 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;
+ }
+}
+++ /dev/null
-/*
- * 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;
- }
-}
+++ /dev/null
-/*
- * 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)));
- }
-}
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;
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;
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 {
assertThat(reloaded.getSeverity()).isEqualTo(MINOR);
assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
- verifyZeroInteractions(issueChangeWebhook);
+ verifyZeroInteractions(issueChangeTrigger);
}
@Test
assertThat(reloaded.getTags()).containsOnly("tag1", "tag2", "tag3");
assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
- verifyZeroInteractions(issueChangeWebhook);
+ verifyZeroInteractions(issueChangeTrigger);
}
@Test
assertThat(reloaded.getAssignee()).isNull();
assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW);
- verifyZeroInteractions(issueChangeWebhook);
+ verifyZeroInteractions(issueChangeTrigger);
}
@Test
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));
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;
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
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());
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);
}
}
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;
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 {
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());
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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)));
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}