1
0
mirror of https://github.com/mc1arke/sonarqube-community-branch-plugin.git synced 2025-02-21 19:20:09 +02:00

Refactor Bitbucket operations to prevent leaking scope

The Bitbucket clients require different properties to be used from the
relevant configuration DTOs depending on whether Bitbucket cloud or
server are being used, with the management of the property retrieval
being delegated to the relevant client implementation. However, this
requires each client to reference DTO classes from Sonarqube core, where
the clients should really only interact with their own models.

As the work on retrieving the relevant details has already been
performed in the `DefaultBitbucketClientFactory`, the logic for
performing the retrieval has been removed from each client
implementations, and the calculated values are passed into the
constructor for each client instead. This does make each client instance
constrained to a single  repository, but given the way the clients are
used within the decorators and validators, this isn't an issue.
The client API has therefore been altered to remove the references to
project and repository in any method signatures since the client now
retrieves this internally from the client configuration.

The clients have also been altered now to depend directly on the status
from the Quality Gate, with a new enum being used by the client to
indicate the report status, and the decorator performing the mapping
between the Quality Gate and report status.

Finally, to allow for the `DefaultBitbucketClientFactory` to have a
single constructor rather than a test-specific constructor, the facility
for creating an Http Client has been moved into an
`HttpClientBuilderFactory` and this new class configured for injection
in both the Compute Engine and server components.
This commit is contained in:
Michael Clarke 2022-04-08 22:06:25 +01:00 committed by Michael Clarke
parent 8e8a31c448
commit d7bb8b4894
23 changed files with 330 additions and 294 deletions

View File

@ -21,6 +21,7 @@ package com.github.mc1arke.sonarqube.plugin;
import com.github.mc1arke.sonarqube.plugin.almclient.DefaultLinkHeaderReader;
import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.DefaultAzureDevopsClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.DefaultBitbucketClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.HttpClientBuilderFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.github.DefaultGithubClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.RestApplicationAuthenticationProvider;
import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultGitlabClientFactory;
@ -90,6 +91,7 @@ public class CommunityBranchPlugin implements Plugin, CoreExtension {
DefaultGithubClientFactory.class,
DefaultLinkHeaderReader.class,
RestApplicationAuthenticationProvider.class,
HttpClientBuilderFactory.class,
DefaultBitbucketClientFactory.class,
BitbucketValidator.class,
GitlabValidator.class,

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Marvin Wichmann, Michael Clarke
* Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -23,10 +23,8 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsight
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.Repository;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import java.io.IOException;
import java.time.Instant;
@ -55,21 +53,21 @@ public interface BitbucketClient {
*/
CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData,
String reportDescription, Instant creationDate, String dashboardUrl,
String logoUrl, QualityGate.Status status);
String logoUrl, ReportStatus reportStatus);
/**
* Deletes all code insights annotations for the given parameters.
*
* @throws IOException if the annotations cannot be deleted
*/
void deleteAnnotations(String project, String repo, String commitSha) throws IOException;
void deleteAnnotations(String commitSha) throws IOException;
/**
* Uploads CodeInsights Annotations for the given commit.
*
* @throws IOException if the annotations cannot be uploaded
*/
void uploadAnnotations(String project, String repo, String commitSha, Set<CodeInsightsAnnotation> annotations) throws IOException;
void uploadAnnotations(String commitSha, Set<CodeInsightsAnnotation> annotations) throws IOException;
/**
* Creates a DataValue of type DataValue.Link or DataValue.CloudLink depending on the implementation
@ -79,7 +77,7 @@ public interface BitbucketClient {
/**
* Uploads the code insights report for the given commit
*/
void uploadReport(String project, String repo, String commitSha, CodeInsightsReport codeInsightReport) throws IOException;
void uploadReport(String commitSha, CodeInsightsReport codeInsightReport) throws IOException;
/**
* <p>
@ -104,32 +102,10 @@ public interface BitbucketClient {
*/
AnnotationUploadLimit getAnnotationUploadLimit();
/**
* Extract the name of the project from the relevant configuration. The project is
* the value that should be used in the calls that take a `project` parameter.
*
* @param almSettingDto the global `AlmSettingDto` containing the global configuration for this ALM
* @param projectAlmSettingDto the `ProjectAlmSettingDto` assigned to the current project
* @return the resolved project name.
*/
String resolveProject(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto);
/**
* Extract the name of the repository from the relevant configuration. The project is
* the value that should be used in the calls that take a `repository` parameter.
*
* @param almSettingDto the global `AlmSettingDto` containing the global configuration for this ALM
* @param projectAlmSettingDto the `ProjectAlmSettingDto` assigned to the current project
* @return the resolved repository name.
*/
String resolveRepository(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto);
/**
* Retrieve the details of the repository from the target Bitbucket instance.
* @param project the project as resolved from {@link #resolveProject(AlmSettingDto, ProjectAlmSettingDto)}
* @param repo the repository as resolved from {@link #resolveRepository(AlmSettingDto, ProjectAlmSettingDto)}
* @return the repository details retrieved from Bitbucket.
*/
Repository retrieveRepository(String project, String repo) throws IOException;
Repository retrieveRepository() throws IOException;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Marvin Wichmann, Michael Clarke
* Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -21,12 +21,13 @@ package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.Repository;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.BitbucketCloudConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudCreateReportRequest;
import okhttp3.MediaType;
@ -34,11 +35,8 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -52,7 +50,6 @@ import java.util.stream.Collectors;
import static java.lang.String.format;
class BitbucketCloudClient implements BitbucketClient {
private static final Logger LOGGER = Loggers.get(BitbucketCloudClient.class);
@ -64,27 +61,25 @@ class BitbucketCloudClient implements BitbucketClient {
private final ObjectMapper objectMapper;
private final OkHttpClient okHttpClient;
private final BitbucketConfiguration bitbucketConfiguration;
BitbucketCloudClient(BitbucketCloudConfiguration config, ObjectMapper objectMapper, OkHttpClient.Builder baseClientBuilder) {
this(objectMapper, createAuthorisingClient(baseClientBuilder, negotiateBearerToken(config, objectMapper, baseClientBuilder.build())));
}
BitbucketCloudClient(ObjectMapper objectMapper, OkHttpClient okHttpClient) {
BitbucketCloudClient(ObjectMapper objectMapper, OkHttpClient okHttpClient, BitbucketConfiguration bitbucketConfiguration) {
this.objectMapper = objectMapper;
this.okHttpClient = okHttpClient;
this.bitbucketConfiguration = bitbucketConfiguration;
}
private static String negotiateBearerToken(BitbucketCloudConfiguration bitbucketCloudConfiguration, ObjectMapper objectMapper, OkHttpClient okHttpClient) {
static String negotiateBearerToken(String clientId, String clientSecret, ObjectMapper objectMapper, OkHttpClient okHttpClient) {
Request request = new Request.Builder()
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString((bitbucketCloudConfiguration.getClientId() + ":" + bitbucketCloudConfiguration.getSecret()).getBytes(
StandardCharsets.UTF_8)))
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8)))
.url("https://bitbucket.org/site/oauth2/access_token")
.post(RequestBody.create("grant_type=client_credentials", MediaType.parse("application/x-www-form-urlencoded")))
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
AuthToken authToken = objectMapper.readValue(
Optional.ofNullable(response.body()).orElseThrow(() -> new IllegalStateException("No response returned by Bitbucket Oauth")).string(), AuthToken.class);
BitbucketCloudClient.AuthToken authToken = objectMapper.readValue(
Optional.ofNullable(response.body()).orElseThrow(() -> new IllegalStateException("No response returned by Bitbucket Oauth")).string(), BitbucketCloudClient.AuthToken.class);
return authToken.getAccessToken();
} catch (IOException ex) {
throw new IllegalStateException("Could not retrieve bearer token", ex);
@ -106,7 +101,7 @@ class BitbucketCloudClient implements BitbucketClient {
@Override
public CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData, String reportDescription,
Instant creationDate, String dashboardUrl, String logoUrl,
QualityGate.Status status) {
ReportStatus status) {
return new CloudCreateReportRequest(
reportData,
reportDescription,
@ -116,16 +111,17 @@ class BitbucketCloudClient implements BitbucketClient {
dashboardUrl, // you need to change this to a real https URL for local debugging since localhost will get declined by the API
logoUrl,
"COVERAGE",
QualityGate.Status.ERROR.equals(status) ? "FAILED" : "PASSED"
ReportStatus.FAILED == status ? "FAILED" : "PASSED"
);
}
@Override
public void deleteAnnotations(String project, String repo, String commitSha) {
public void deleteAnnotations(String commitSha) {
// not needed here.
}
public void uploadAnnotations(String project, String repository, String commit, Set<CodeInsightsAnnotation> baseAnnotations) throws IOException {
@Override
public void uploadAnnotations(String commit, Set<CodeInsightsAnnotation> baseAnnotations) throws IOException {
Set<CloudAnnotation> annotations = baseAnnotations.stream().map(CloudAnnotation.class::cast).collect(Collectors.toSet());
if (annotations.isEmpty()) {
@ -134,7 +130,7 @@ class BitbucketCloudClient implements BitbucketClient {
Request req = new Request.Builder()
.post(RequestBody.create(objectMapper.writeValueAsString(annotations), APPLICATION_JSON_MEDIA_TYPE))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s/annotations", project, repository, commit, REPORT_KEY))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s/annotations", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, REPORT_KEY))
.build();
LOGGER.info("Creating annotations on bitbucket cloud");
@ -151,10 +147,10 @@ class BitbucketCloudClient implements BitbucketClient {
}
@Override
public void uploadReport(String project, String repository, String commit, CodeInsightsReport codeInsightReport) throws IOException {
deleteExistingReport(project, repository, commit);
public void uploadReport(String commit, CodeInsightsReport codeInsightReport) throws IOException {
deleteExistingReport(commit);
String targetUrl = format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", project, repository, commit, REPORT_KEY);
String targetUrl = format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, REPORT_KEY);
String body = objectMapper.writeValueAsString(codeInsightReport);
Request req = new Request.Builder()
.put(RequestBody.create(body, APPLICATION_JSON_MEDIA_TYPE))
@ -180,20 +176,10 @@ class BitbucketCloudClient implements BitbucketClient {
}
@Override
public String resolveProject(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) {
return almSettingDto.getAppId();
}
@Override
public String resolveRepository(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) {
return projectAlmSettingDto.getAlmRepo();
}
@Override
public Repository retrieveRepository(String project, String repo) throws IOException {
public Repository retrieveRepository() throws IOException {
Request req = new Request.Builder()
.get()
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s", project, repo))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository()))
.build();
try (Response response = okHttpClient.newCall(req).execute()) {
validate(response);
@ -205,10 +191,10 @@ class BitbucketCloudClient implements BitbucketClient {
}
}
void deleteExistingReport(String project, String repository, String commit) throws IOException {
void deleteExistingReport(String commit) throws IOException {
Request req = new Request.Builder()
.delete()
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", project, repository, commit, REPORT_KEY))
.url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, REPORT_KEY))
.build();
LOGGER.info("Deleting existing reports on bitbucket cloud");
@ -218,17 +204,6 @@ class BitbucketCloudClient implements BitbucketClient {
}
}
private static OkHttpClient createAuthorisingClient(OkHttpClient.Builder baseClientBuilder, String bearerToken) {
return baseClientBuilder.addInterceptor(chain -> {
Request newRequest = chain.request().newBuilder()
.addHeader("Authorization", format("Bearer %s", bearerToken))
.addHeader("Accept", APPLICATION_JSON_MEDIA_TYPE.toString())
.build();
return chain.proceed(newRequest);
})
.build();
}
void validate(Response response) {
if (!response.isSuccessful()) {
String error = Optional.ofNullable(response.body()).map(b -> {
@ -242,7 +217,7 @@ class BitbucketCloudClient implements BitbucketClient {
}
}
static class AuthToken {
private static class AuthToken {
private final String accessToken;

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Mathias Åhsberg, Michael Clarke
* Copyright (C) 2020-2022 Mathias Åhsberg, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -24,6 +24,7 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsight
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.Repository;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.Annotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.BitbucketServerConfiguration;
@ -36,11 +37,8 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import java.io.IOException;
import java.time.Instant;
@ -63,10 +61,6 @@ class BitbucketServerClient implements BitbucketClient {
private final ObjectMapper objectMapper;
private final OkHttpClient okHttpClient;
BitbucketServerClient(BitbucketServerConfiguration config, ObjectMapper objectMapper, OkHttpClient.Builder baseClientBuilder) {
this(config, objectMapper, createAuthorisingClient(baseClientBuilder, config));
}
BitbucketServerClient(BitbucketServerConfiguration config, ObjectMapper objectMapper, OkHttpClient okHttpClient) {
this.config = config;
this.objectMapper = objectMapper;
@ -85,7 +79,7 @@ class BitbucketServerClient implements BitbucketClient {
}
@Override
public CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData, String reportDescription, Instant creationDate, String dashboardUrl, String logoUrl, QualityGate.Status status) {
public CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData, String reportDescription, Instant creationDate, String dashboardUrl, String logoUrl, ReportStatus status) {
return new CreateReportRequest(
reportData,
reportDescription,
@ -94,14 +88,15 @@ class BitbucketServerClient implements BitbucketClient {
creationDate,
dashboardUrl,
logoUrl,
QualityGate.Status.ERROR.equals(status) ? "FAIL" : "PASS"
ReportStatus.FAILED == status ? "FAIL" : "PASS"
);
}
public void deleteAnnotations(String project, String repository, String commit) throws IOException {
@Override
public void deleteAnnotations(String commit) throws IOException {
Request req = new Request.Builder()
.delete()
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), project, repository, commit, REPORT_KEY))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), config.getProject(), config.getRepository(), commit, REPORT_KEY))
.build();
try (Response response = okHttpClient.newCall(req).execute()) {
validate(response);
@ -109,7 +104,7 @@ class BitbucketServerClient implements BitbucketClient {
}
@Override
public void uploadAnnotations(String project, String repository, String commit, Set<CodeInsightsAnnotation> annotations) throws IOException {
public void uploadAnnotations(String commit, Set<CodeInsightsAnnotation> annotations) throws IOException {
if (annotations.isEmpty()) {
return;
}
@ -117,7 +112,7 @@ class BitbucketServerClient implements BitbucketClient {
CreateAnnotationsRequest request = new CreateAnnotationsRequest(annotationSet);
Request req = new Request.Builder()
.post(RequestBody.create(objectMapper.writeValueAsString(request), APPLICATION_JSON_MEDIA_TYPE))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), project, repository, commit, REPORT_KEY))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), config.getProject(), config.getRepository(), commit, REPORT_KEY))
.build();
try (Response response = okHttpClient.newCall(req).execute()) {
validate(response);
@ -130,11 +125,11 @@ class BitbucketServerClient implements BitbucketClient {
}
@Override
public void uploadReport(String project, String repository, String commit, CodeInsightsReport codeInsightReport) throws IOException {
public void uploadReport(String commit, CodeInsightsReport codeInsightReport) throws IOException {
String body = objectMapper.writeValueAsString(codeInsightReport);
Request req = new Request.Builder()
.put(RequestBody.create(body, APPLICATION_JSON_MEDIA_TYPE))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s", config.getUrl(), project, repository, commit, REPORT_KEY))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s", config.getUrl(), config.getProject(), config.getRepository(), commit, REPORT_KEY))
.build();
try (Response response = okHttpClient.newCall(req).execute()) {
@ -166,20 +161,10 @@ class BitbucketServerClient implements BitbucketClient {
}
@Override
public String resolveProject(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) {
return projectAlmSettingDto.getAlmRepo();
}
@Override
public String resolveRepository(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) {
return projectAlmSettingDto.getAlmSlug();
}
@Override
public Repository retrieveRepository(String project, String repo) throws IOException {
public Repository retrieveRepository() throws IOException {
Request req = new Request.Builder()
.get()
.url(format("%s/rest/api/1.0/projects/%s/repos/%s", config.getUrl(), project, repo))
.url(format("%s/rest/api/1.0/projects/%s/repos/%s", config.getUrl(), config.getProject(), config.getRepository()))
.build();
try (Response response = okHttpClient.newCall(req).execute()) {
validate(response);
@ -206,17 +191,6 @@ class BitbucketServerClient implements BitbucketClient {
}
}
private static OkHttpClient createAuthorisingClient(OkHttpClient.Builder clientBuilder, BitbucketServerConfiguration config) {
return clientBuilder.addInterceptor(chain -> {
Request newRequest = chain.request().newBuilder()
.addHeader("Authorization", format("Bearer %s", config.getPersonalAccessToken()))
.addHeader("Accept", APPLICATION_JSON_MEDIA_TYPE.toString())
.build();
return chain.proceed(newRequest);
})
.build();
}
void validate(Response response) throws IOException {
if (!response.isSuccessful()) {
ErrorResponse errors = null;

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Marvin Wichmann, Michael Clarke
* Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -22,9 +22,10 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.InvalidConfigurationException;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.BitbucketCloudConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.BitbucketServerConfiguration;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.logging.HttpLoggingInterceptor;
import org.apache.commons.lang3.StringUtils;
import org.sonar.api.ce.ComputeEngineSide;
@ -37,7 +38,8 @@ import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import java.util.Optional;
import java.util.function.Supplier;
import static java.lang.String.format;
@ServerSide
@ComputeEngineSide
@ -45,15 +47,11 @@ public class DefaultBitbucketClientFactory implements BitbucketClientFactory {
private static final Logger LOGGER = Loggers.get(DefaultBitbucketClientFactory.class);
private final Supplier<OkHttpClient.Builder> okHttpClientBuilderSupplier;
private final HttpClientBuilderFactory httpClientBuilderFactory;
private final Settings settings;
public DefaultBitbucketClientFactory(Settings settings) {
this(settings, OkHttpClient.Builder::new);
}
DefaultBitbucketClientFactory(Settings settings, Supplier<OkHttpClient.Builder> okHttpClientBuilderSupplier) {
this.okHttpClientBuilderSupplier = okHttpClientBuilderSupplier;
public DefaultBitbucketClientFactory(Settings settings, HttpClientBuilderFactory httpClientBuilderFactory) {
this.httpClientBuilderFactory = httpClientBuilderFactory;
this.settings = settings;
}
@ -61,6 +59,10 @@ public class DefaultBitbucketClientFactory implements BitbucketClientFactory {
public BitbucketClient createClient(ProjectAlmSettingDto projectAlmSettingDto, AlmSettingDto almSettingDto) {
String almRepo = Optional.ofNullable(StringUtils.trimToNull(projectAlmSettingDto.getAlmRepo()))
.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "ALM Repo must be set in configuration"));
ObjectMapper objectMapper = createObjectMapper();
OkHttpClient.Builder clientBuilder = createBaseClientBuilder(httpClientBuilderFactory);
if (almSettingDto.getAlm() == ALM.BITBUCKET_CLOUD) {
String appId = Optional.ofNullable(StringUtils.trimToNull(almSettingDto.getAppId()))
.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "App ID must be set in configuration"));
@ -68,7 +70,8 @@ public class DefaultBitbucketClientFactory implements BitbucketClientFactory {
.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "Client ID must be set in configuration"));
String clientSecret = Optional.ofNullable(StringUtils.trimToNull(almSettingDto.getDecryptedClientSecret(settings.getEncryption())))
.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "Client Secret must be set in configuration"));
return new BitbucketCloudClient(new BitbucketCloudConfiguration(appId, almRepo, clientId, clientSecret), createObjectMapper(), createBaseClientBuilder(okHttpClientBuilderSupplier));
String bearerToken = BitbucketCloudClient.negotiateBearerToken(clientId, clientSecret, objectMapper, clientBuilder.build());
return new BitbucketCloudClient(objectMapper, createAuthorisingClient(clientBuilder, bearerToken), new BitbucketConfiguration(appId, almRepo));
} else {
String almSlug = Optional.ofNullable(StringUtils.trimToNull(projectAlmSettingDto.getAlmSlug()))
.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "ALM slug must be set in configuration"));
@ -76,7 +79,7 @@ public class DefaultBitbucketClientFactory implements BitbucketClientFactory {
.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "URL must be set in configuration"));
String personalAccessToken = Optional.ofNullable(StringUtils.trimToNull(almSettingDto.getDecryptedPersonalAccessToken(settings.getEncryption())))
.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Personal access token must be set in configuration"));
return new BitbucketServerClient(new BitbucketServerConfiguration(almRepo, almSlug, url, personalAccessToken), createObjectMapper(), createBaseClientBuilder(okHttpClientBuilderSupplier));
return new BitbucketServerClient(new BitbucketServerConfiguration(almRepo, almSlug, url), objectMapper, createAuthorisingClient(clientBuilder, personalAccessToken));
}
}
@ -86,9 +89,19 @@ public class DefaultBitbucketClientFactory implements BitbucketClientFactory {
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
private static OkHttpClient.Builder createBaseClientBuilder(Supplier<OkHttpClient.Builder> builderSupplier) {
private static OkHttpClient.Builder createBaseClientBuilder(HttpClientBuilderFactory httpClientBuilderFactory) {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(LOGGER::debug);
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return builderSupplier.get().addInterceptor(httpLoggingInterceptor);
return httpClientBuilderFactory.createClientBuilder().addInterceptor(httpLoggingInterceptor);
}
private static OkHttpClient createAuthorisingClient(OkHttpClient.Builder clientBuilder, String bearerToken) {
return clientBuilder.addInterceptor(chain -> {
Request newRequest = chain.request().newBuilder()
.addHeader("Authorization", format("Bearer %s", bearerToken))
.addHeader("Accept", "application/json")
.build();
return chain.proceed(newRequest);
}).build();
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2022 Michael Clarke
*
* 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 com.github.mc1arke.sonarqube.plugin.almclient.bitbucket;
import okhttp3.OkHttpClient;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;
@ServerSide
@ComputeEngineSide
public class HttpClientBuilderFactory {
public OkHttpClient.Builder createClientBuilder() {
return new OkHttpClient.Builder();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Michael Clarke
* Copyright (C) 2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -16,26 +16,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud;
package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration;
public class BitbucketCloudConfiguration extends BitbucketConfiguration {
private final String clientId;
private final String secret;
public BitbucketCloudConfiguration(String repository, String project, String clientId, String secret) {
super(repository, project);
this.clientId = clientId;
this.secret = secret;
}
public String getClientId() {
return clientId;
}
public String getSecret() {
return secret;
}
public enum ReportStatus {
PASSED, FAILED
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Michael Clarke
* Copyright (C) 2021-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -23,19 +23,13 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketCo
public class BitbucketServerConfiguration extends BitbucketConfiguration {
private final String url;
private final String personalAccessToken;
public BitbucketServerConfiguration(String almRepo, String almSlug, String url, String personalAccessToken) {
public BitbucketServerConfiguration(String almRepo, String almSlug, String url) {
super(almRepo, almSlug);
this.url = url;
this.personalAccessToken = personalAccessToken;
}
public String getUrl() {
return url;
}
public String getPersonalAccessToken() {
return personalAccessToken;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Michael Clarke
* Copyright (C) 2019-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -21,6 +21,7 @@ package com.github.mc1arke.sonarqube.plugin.ce;
import com.github.mc1arke.sonarqube.plugin.almclient.DefaultLinkHeaderReader;
import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.DefaultAzureDevopsClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.DefaultBitbucketClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.HttpClientBuilderFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.github.DefaultGithubClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.RestApplicationAuthenticationProvider;
import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultGitlabClientFactory;
@ -45,7 +46,7 @@ public class CommunityReportAnalysisComponentProvider implements ReportAnalysisC
return Arrays.asList(CommunityBranchLoaderDelegate.class, PullRequestPostAnalysisTask.class,
PostAnalysisIssueVisitor.class, DefaultLinkHeaderReader.class,
DefaultGithubClientFactory.class, RestApplicationAuthenticationProvider.class, GithubPullRequestDecorator.class,
DefaultBitbucketClientFactory.class, BitbucketPullRequestDecorator.class,
HttpClientBuilderFactory.class, DefaultBitbucketClientFactory.class, BitbucketPullRequestDecorator.class,
DefaultGitlabClientFactory.class, GitlabMergeRequestDecorator.class,
DefaultAzureDevopsClientFactory.class, AzureDevOpsPullRequestDecorator.class);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Michael Clarke
* Copyright (C) 2020-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -68,6 +68,9 @@ import java.util.stream.Collectors;
public class AnalysisDetails {
private static final List<String> CLOSED_ISSUE_STATUS = Arrays.asList(Issue.STATUS_CLOSED, Issue.STATUS_RESOLVED);
private static final List<String> OPEN_ISSUE_STATUSES =
Issue.STATUSES.stream().filter(s -> !CLOSED_ISSUE_STATUS.contains(s))
.collect(Collectors.toList());
private static final List<BigDecimal> COVERAGE_LEVELS =
Arrays.asList(BigDecimal.valueOf(100), BigDecimal.valueOf(90), BigDecimal.valueOf(60),
@ -399,6 +402,15 @@ public class AnalysisDetails {
return findMeasure(CoreMetrics.COVERAGE_KEY).map(Measure::getDoubleValue).map(BigDecimal::new);
}
public List<PostAnalysisIssueVisitor.ComponentIssue> getScmReportableIssues() {
return postAnalysisIssueVisitor.getIssues().stream()
.filter(i -> getSCMPathForIssue(i).isPresent())
.filter(i -> i.getComponent().getType() == Component.Type.FILE)
.filter(i -> i.getIssue().resolution() == null)
.filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().status()))
.collect(Collectors.toList());
}
public static class BranchDetails {
private final String branchName;

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Michael Clarke
* Copyright (C) 2019-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -29,6 +29,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.CheckForNull;
@ -38,7 +39,9 @@ public class PostAnalysisIssueVisitor extends IssueVisitor {
@Override
public void onIssue(Component component, DefaultIssue defaultIssue) {
collectedIssues.add(new ComponentIssue(component, defaultIssue));
collectedIssues.add(new ComponentIssue(component, Optional.ofNullable(defaultIssue)
.map(LightIssue::new)
.orElse(null)));
}
public List<ComponentIssue> getIssues() {
@ -50,11 +53,10 @@ public class PostAnalysisIssueVisitor extends IssueVisitor {
private final Component component;
private final LightIssue issue;
ComponentIssue(Component component, DefaultIssue issue) {
ComponentIssue(Component component, LightIssue issue) {
super();
this.component = component;
this.issue = (issue != null) ? new LightIssue(issue) : null;
// the null test is to please PostAnalysisIssueVisitorTest.checkAllIssuesCollected()
this.issue = issue;
}
public Component getComponent() {
@ -85,7 +87,7 @@ public class PostAnalysisIssueVisitor extends IssueVisitor {
private final DbIssues.Locations locations;
private final RuleKey ruleKey;
private LightIssue(DefaultIssue issue) {
LightIssue(DefaultIssue issue) {
this.effortInMinutes = issue.effortInMinutes();
this.key = issue.key();
this.line = issue.getLine();

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Mathias Åhsberg, Michael Clarke
* Copyright (C) 2020-2022 Mathias Åhsberg, Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -26,6 +26,7 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsight
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator;
@ -37,7 +38,6 @@ import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
@ -63,10 +63,6 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
private static final DecorationResult DEFAULT_DECORATION_RESULT = DecorationResult.builder().build();
private static final List<String> OPEN_ISSUE_STATUSES =
Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s))
.collect(Collectors.toList());
private final BitbucketClientFactory bitbucketClientFactory;
public BitbucketPullRequestDecorator(BitbucketClientFactory bitbucketClientFactory) {
@ -82,22 +78,18 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
return DEFAULT_DECORATION_RESULT;
}
String project = client.resolveProject(almSettingDto, projectAlmSettingDto);
String repo = client.resolveRepository(almSettingDto, projectAlmSettingDto);
CodeInsightsReport codeInsightsReport = client.createCodeInsightsReport(
toReport(client, analysisDetails),
reportDescription(analysisDetails),
analysisDetails.getAnalysisDate().toInstant(),
analysisDetails.getDashboardUrl(),
format("%s/common/icon.png", analysisDetails.getBaseImageUrl()),
analysisDetails.getQualityGateStatus()
analysisDetails.getQualityGateStatus() == QualityGate.Status.OK ? ReportStatus.PASSED : ReportStatus.FAILED
);
client.uploadReport(project, repo,
analysisDetails.getCommitSha(), codeInsightsReport);
client.uploadReport(analysisDetails.getCommitSha(), codeInsightsReport);
updateAnnotations(client, project, repo, analysisDetails);
updateAnnotations(client, analysisDetails);
} catch (IOException e) {
LOGGER.error("Could not decorate pull request for project {}", analysisDetails.getAnalysisProjectKey(), e);
}
@ -124,17 +116,14 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
return reportData;
}
private void updateAnnotations(BitbucketClient client, String project, String repo, AnalysisDetails analysisDetails) throws IOException {
private void updateAnnotations(BitbucketClient client, AnalysisDetails analysisDetails) throws IOException {
final AtomicInteger chunkCounter = new AtomicInteger(0);
client.deleteAnnotations(project, repo, analysisDetails.getCommitSha());
client.deleteAnnotations(analysisDetails.getCommitSha());
AnnotationUploadLimit uploadLimit = client.getAnnotationUploadLimit();
Map<Integer, Set<CodeInsightsAnnotation>> annotationChunks = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream()
.filter(i -> i.getComponent().getReportAttributes().getScmPath().isPresent())
.filter(i -> i.getComponent().getType() == Component.Type.FILE)
.filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().status()))
Map<Integer, Set<CodeInsightsAnnotation>> annotationChunks = analysisDetails.getScmReportableIssues().stream()
.filter(i -> !(i.getIssue().type() == RuleType.SECURITY_HOTSPOT && Issue.SECURITY_HOTSPOT_RESOLUTIONS
.contains(i.getIssue().resolution())))
.sorted(Comparator.comparing(a -> Severity.ALL.indexOf(a.getIssue().severity())))
@ -158,7 +147,7 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
break;
}
client.uploadAnnotations(project, repo, analysisDetails.getCommitSha(), annotations);
client.uploadAnnotations(analysisDetails.getCommitSha(), annotations);
} catch (BitbucketException e) {
if (e.isError(BitbucketException.PAYLOAD_TOO_LARGE)) {
LOGGER.warn("The annotations will be truncated since the maximum number of annotations for this report has been reached.");
@ -174,7 +163,7 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
return (chunkCounter * uploadLimit.getAnnotationBatchSize()) > uploadLimit.getTotalAllowedAnnotations();
}
private String toBitbucketSeverity(String severity) {
private static String toBitbucketSeverity(String severity) {
if (severity == null) {
return "LOW";
}
@ -189,7 +178,7 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
}
}
private String toBitbucketType(RuleType sonarqubeType) {
private static String toBitbucketType(RuleType sonarqubeType) {
switch (sonarqubeType) {
case SECURITY_HOTSPOT:
case VULNERABILITY:
@ -203,24 +192,24 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
}
}
private ReportData securityReport(Long vulnerabilities, Long hotspots) {
private static ReportData securityReport(Long vulnerabilities, Long hotspots) {
String vulnerabilityDescription = vulnerabilities == 1 ? "Vulnerability" : "Vulnerabilities";
String hotspotDescription = hotspots == 1 ? "Hotspot" : "Hotspots";
String security = format("%d %s (and %d %s)", vulnerabilities, vulnerabilityDescription, hotspots, hotspotDescription);
return new ReportData("Security", new DataValue.Text(security));
}
private ReportData reliabilityReport(Long bugs) {
private static ReportData reliabilityReport(Long bugs) {
String description = bugs == 1 ? "Bug" : "Bugs";
return new ReportData("Reliability", new DataValue.Text(format("%d %s", bugs, description)));
}
private ReportData maintainabilityReport(Long codeSmells) {
private static ReportData maintainabilityReport(Long codeSmells) {
String description = codeSmells == 1 ? "Code Smell" : "Code Smells";
return new ReportData("Maintainability", new DataValue.Text(format("%d %s", codeSmells, description)));
}
private String reportDescription(AnalysisDetails details) {
private static String reportDescription(AnalysisDetails details) {
String header = details.getQualityGateStatus() == QualityGate.Status.OK ? "Quality Gate passed" : "Quality Gate failed";
String body = details.findFailedConditions().stream()
.map(AnalysisDetails::format)
@ -229,7 +218,7 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
return format("%s%n%s", header, body);
}
private BigDecimal newCoverage(AnalysisDetails details) {
private static BigDecimal newCoverage(AnalysisDetails details) {
return details.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY)
.filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE)
.map(QualityGate.Condition::getValue)
@ -237,7 +226,7 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco
.orElse(BigDecimal.ZERO);
}
private BigDecimal newDuplication(AnalysisDetails details) {
private static BigDecimal newDuplication(AnalysisDetails details) {
return details.findQualityGateCondition(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY)
.filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE)
.map(QualityGate.Condition::getValue)

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Michael Clarke
* Copyright (C) 2021-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -56,7 +56,7 @@ public class BitbucketValidator implements Validator {
throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Could not create Bitbucket client - " + ex.getMessage(), ex);
}
try {
bitbucketClient.retrieveRepository(bitbucketClient.resolveProject(almSettingDto, projectAlmSettingDto), bitbucketClient.resolveRepository(almSettingDto, projectAlmSettingDto));
bitbucketClient.retrieveRepository();
} catch (IOException | RuntimeException ex) {
throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Could not retrieve repository details from Bitbucket - " + ex.getMessage(), ex);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Michael Clarke
* Copyright (C) 2020-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -118,7 +118,7 @@ public class CommunityBranchPluginTest {
final ArgumentCaptor<Object> argumentCaptor = ArgumentCaptor.forClass(Object.class);
verify(context, times(2)).addExtensions(argumentCaptor.capture(), argumentCaptor.capture());
assertEquals(22, argumentCaptor.getAllValues().size());
assertEquals(23, argumentCaptor.getAllValues().size());
assertEquals(Arrays.asList(CommunityBranchFeatureExtension.class, CommunityBranchSupportDelegate.class),
argumentCaptor.getAllValues().subList(0, 2));

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Michael Clarke
* Copyright (C) 2021-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -20,9 +20,11 @@ package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudCreateReportRequest;
import com.google.common.collect.Sets;
@ -37,7 +39,6 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.ce.posttask.QualityGate;
import java.io.IOException;
import java.time.Instant;
@ -67,9 +68,10 @@ public class BitbucketCloudClientUnitTest {
@Before
public void before() {
BitbucketConfiguration bitbucketConfiguration = new BitbucketConfiguration("repository", "project");
Call call = mock(Call.class);
when(client.newCall(any())).thenReturn(call);
underTest = new BitbucketCloudClient(mapper, client);
underTest = new BitbucketCloudClient(mapper, client, bitbucketConfiguration);
}
@Test
@ -87,7 +89,7 @@ public class BitbucketCloudClientUnitTest {
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
// when
underTest.uploadReport("project", "repository", "commit", report);
underTest.uploadReport("commit", report);
// then
verify(client, times(2)).newCall(captor.capture());
@ -107,7 +109,7 @@ public class BitbucketCloudClientUnitTest {
when(call.execute()).thenReturn(response);
// when
underTest.deleteExistingReport("project", "repository", "commit");
underTest.deleteExistingReport("commit");
// then
verify(client).newCall(captor.capture());
@ -132,7 +134,7 @@ public class BitbucketCloudClientUnitTest {
when(mapper.writeValueAsString(any())).thenReturn("{payload}");
// when
underTest.uploadAnnotations("project", "repository", "commit", annotations);
underTest.uploadAnnotations("commit", annotations);
// then
verify(client).newCall(captor.capture());
@ -170,7 +172,7 @@ public class BitbucketCloudClientUnitTest {
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
// when,then
assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report))
assertThatThrownBy(() -> underTest.uploadReport("commit", report))
.isInstanceOf(BitbucketCloudException.class)
.hasMessage("HTTP Status Code: 400; Message:error!")
.extracting(e -> ((BitbucketCloudException) e).isError(400))
@ -183,7 +185,7 @@ public class BitbucketCloudClientUnitTest {
Set<CodeInsightsAnnotation> annotations = Sets.newHashSet();
// when
underTest.uploadAnnotations("project", "repository", "commit", annotations);
underTest.uploadAnnotations("commit", annotations);
// then
verify(client, never()).newCall(any());
@ -199,10 +201,10 @@ public class BitbucketCloudClientUnitTest {
// then
assertTrue(annotation instanceof CloudAnnotation);
assertEquals("issueKey", ((CloudAnnotation) annotation).getExternalId());
assertEquals(12, ((CloudAnnotation) annotation).getLine());
assertEquals(12, annotation.getLine());
assertEquals("http://localhost:9000/dashboard", ((CloudAnnotation) annotation).getLink());
assertEquals("/path/to/file", ((CloudAnnotation) annotation).getPath());
assertEquals("MAJOR", ((CloudAnnotation) annotation).getSeverity());
assertEquals("/path/to/file", annotation.getPath());
assertEquals("MAJOR", annotation.getSeverity());
assertEquals("BUG", ((CloudAnnotation) annotation).getAnnotationType());
}
@ -234,15 +236,15 @@ public class BitbucketCloudClientUnitTest {
// given
// when
CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", QualityGate.Status.ERROR);
CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", ReportStatus.FAILED);
// then
assertTrue(result instanceof CloudCreateReportRequest);
assertEquals(0, ((CloudCreateReportRequest) result).getData().size());
assertEquals("reportDescription", ((CloudCreateReportRequest) result).getDetails());
assertEquals("dashboardUrl", ((CloudCreateReportRequest) result).getLink());
assertEquals(0, result.getData().size());
assertEquals("reportDescription", result.getDetails());
assertEquals("dashboardUrl", result.getLink());
assertEquals("logoUrl", ((CloudCreateReportRequest) result).getLogoUrl());
assertEquals("FAILED", ((CloudCreateReportRequest) result).getResult());
assertEquals("FAILED", result.getResult());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Michael Clarke
* Copyright (C) 2021-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -24,6 +24,7 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationU
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.Annotation;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.BitbucketServerConfiguration;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.CreateReportRequest;
@ -43,7 +44,6 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.ce.posttask.QualityGate;
import java.io.IOException;
import java.time.Instant;
@ -74,7 +74,7 @@ public class BitbucketServerClientUnitTest {
@Before
public void before() {
BitbucketServerConfiguration
config = new BitbucketServerConfiguration("repo", "slug", "https://my-server.org", "token");
config = new BitbucketServerConfiguration("repository", "slug", "https://my-server.org");
underTest = new BitbucketServerClient(config, mapper, client);
}
@ -240,13 +240,13 @@ public class BitbucketServerClientUnitTest {
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
// when
underTest.uploadReport("project", "repository", "commit", report);
underTest.uploadReport("commit", report);
// then
verify(client).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("PUT", request.method());
assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube", request.url().toString());
assertEquals("https://my-server.org/rest/insights/1.0/projects/slug/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube", request.url().toString());
}
@Test
@ -264,7 +264,7 @@ public class BitbucketServerClientUnitTest {
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
// when,then
assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report))
assertThatThrownBy(() -> underTest.uploadReport("commit", report))
.isInstanceOf(BitbucketException.class);
}
@ -295,7 +295,7 @@ public class BitbucketServerClientUnitTest {
// when,then
assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report))
assertThatThrownBy(() -> underTest.uploadReport("commit", report))
.isInstanceOf(BitbucketException.class)
.hasMessage("error!")
.extracting(e -> ((BitbucketException) e).isError(400))
@ -323,13 +323,13 @@ public class BitbucketServerClientUnitTest {
when(response.isSuccessful()).thenReturn(true);
// when
underTest.uploadAnnotations("project", "repository", "commit", annotations);
underTest.uploadAnnotations("commit", annotations);
// then
verify(client).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("POST", request.method());
assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString());
assertEquals("https://my-server.org/rest/insights/1.0/projects/slug/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString());
try (Buffer bodyContent = new Buffer()) {
request.body().writeTo(bodyContent);
@ -343,7 +343,7 @@ public class BitbucketServerClientUnitTest {
Set<CodeInsightsAnnotation> annotations = Sets.newHashSet();
// when
underTest.uploadAnnotations("project", "repository", "commit", annotations);
underTest.uploadAnnotations("commit", annotations);
// then
verify(client, never()).newCall(any());
@ -361,13 +361,13 @@ public class BitbucketServerClientUnitTest {
when(response.isSuccessful()).thenReturn(true);
// when
underTest.deleteAnnotations("project", "repository", "commit");
underTest.deleteAnnotations("commit");
// then
verify(client).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("DELETE", request.method());
assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString());
assertEquals("https://my-server.org/rest/insights/1.0/projects/slug/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString());
}
@Test
@ -413,7 +413,7 @@ public class BitbucketServerClientUnitTest {
// given
// when
CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", QualityGate.Status.ERROR);
CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", ReportStatus.FAILED);
// then
assertTrue(result instanceof CreateReportRequest);

View File

@ -1,8 +1,11 @@
package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.ResponseBody;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.sonar.api.config.internal.Encryption;
import org.sonar.api.config.internal.Settings;
@ -15,6 +18,8 @@ import java.io.IOException;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class DefaultBitbucketClientFactoryUnitTest {
@ -32,7 +37,7 @@ public class DefaultBitbucketClientFactoryUnitTest {
when(builder.addInterceptor(any())).thenReturn(builder);
ResponseBody responseBody = mock(ResponseBody.class);
when(responseBody.string()).thenReturn("{}");
when(responseBody.string()).thenReturn("{\"access_token\": \"dummy\"}");
when(builder.build().newCall(any()).execute().body()).thenReturn(responseBody);
Settings settings = mock(Settings.class);
@ -40,10 +45,31 @@ public class DefaultBitbucketClientFactoryUnitTest {
// when
when(settings.getEncryption()).thenReturn(encryption);
BitbucketClient client = new DefaultBitbucketClientFactory(settings, () -> builder).createClient(projectAlmSettingDto, almSettingDto);
HttpClientBuilderFactory httpClientBuilderFactory = mock(HttpClientBuilderFactory.class);
when(httpClientBuilderFactory.createClientBuilder()).then(i -> builder);
BitbucketClient client = new DefaultBitbucketClientFactory(settings, httpClientBuilderFactory).createClient(projectAlmSettingDto, almSettingDto);
// then
assertTrue(client instanceof BitbucketCloudClient);
ArgumentCaptor<Interceptor> interceptorArgumentCaptor = ArgumentCaptor.forClass(Interceptor.class);
verify(builder, times(2)).addInterceptor(interceptorArgumentCaptor.capture());
Interceptor.Chain chain = mock(Interceptor.Chain.class);
Request request = mock(Request.class);
when(chain.request()).thenReturn(request);
Request.Builder requestBuilder = mock(Request.Builder.class);
when(requestBuilder.addHeader(any(), any())).thenReturn(requestBuilder);
when(request.newBuilder()).thenReturn(requestBuilder);
Request request2 = mock(Request.class);
when(requestBuilder.build()).thenReturn(request2);
interceptorArgumentCaptor.getValue().intercept(chain);
verify(requestBuilder).addHeader("Authorization", "Bearer dummy");
verify(requestBuilder).addHeader("Accept", "application/json");
verify(chain).proceed(request2);
}
@Test
@ -61,7 +87,9 @@ public class DefaultBitbucketClientFactoryUnitTest {
// when
when(settings.getEncryption()).thenReturn(encryption);
BitbucketClient client = new DefaultBitbucketClientFactory(settings, () -> mock(OkHttpClient.Builder.class, Mockito.RETURNS_DEEP_STUBS)).createClient(projectAlmSettingDto, almSettingDto);
HttpClientBuilderFactory httpClientBuilderFactory = mock(HttpClientBuilderFactory.class);
when(httpClientBuilderFactory.createClientBuilder()).then(i -> mock(OkHttpClient.Builder.class, Mockito.RETURNS_DEEP_STUBS));
BitbucketClient client = new DefaultBitbucketClientFactory(settings, httpClientBuilderFactory).createClient(projectAlmSettingDto, almSettingDto);
// then
assertTrue(client instanceof BitbucketServerClient);

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2022 Michael Clarke
*
* 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 com.github.mc1arke.sonarqube.plugin.almclient.bitbucket;
import okhttp3.OkHttpClient;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class HttpClientBuilderFactoryTest {
@Test
void verifyNotSameInstanceReturnedByFactory() {
HttpClientBuilderFactory underTest = new HttpClientBuilderFactory();
OkHttpClient.Builder builder1 = underTest.createClientBuilder();
OkHttpClient.Builder builder2 = underTest.createClientBuilder();
assertThat(builder1).isNotSameAs(builder2);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Michael Clarke
* Copyright (C) 2019-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -32,7 +32,7 @@ public class CommunityReportAnalysisComponentProviderTest {
@Test
public void testGetComponents() {
List<Object> result = new CommunityReportAnalysisComponentProvider().getComponents();
assertEquals(13, result.size());
assertEquals(14, result.size());
assertEquals(CommunityBranchLoaderDelegate.class, result.get(0));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Michael Clarke
* Copyright (C) 2020-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -40,6 +40,7 @@ import org.sonar.api.issue.Issue;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.rules.RuleType;
import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.ce.task.projectanalysis.component.ReportAttributes;
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
import org.sonar.ce.task.projectanalysis.measure.Measure;
import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
@ -910,4 +911,52 @@ public class AnalysisDetailsTest {
mock(Configuration.class),"", mock(ScannerContext.class));
assertThat(analysisDetails.parseIssueIdFromUrl("http://subdomain.sonarqube.dummy/path/issue?id=projectId&other_parameter=123")).isEmpty();
}
@Test
public void shouldOnlyReturnNonClosedFileIssuesWithScmInfo() {
PostAnalysisIssueVisitor.LightIssue lightIssue1 = mock(PostAnalysisIssueVisitor.LightIssue.class);
when(lightIssue1.status()).thenReturn(Issue.STATUS_OPEN);
Component component1 = mock(Component.class);
when(component1.getType()).thenReturn(Component.Type.FILE);
ReportAttributes reportAttributes1 = mock(ReportAttributes.class);
when(reportAttributes1.getScmPath()).thenReturn(Optional.of("path"));
when(component1.getReportAttributes()).thenReturn(reportAttributes1);
PostAnalysisIssueVisitor.ComponentIssue componentIssue1 = new PostAnalysisIssueVisitor.ComponentIssue(component1, lightIssue1);
PostAnalysisIssueVisitor.LightIssue lightIssue2 = mock(PostAnalysisIssueVisitor.LightIssue.class);
when(lightIssue2.status()).thenReturn(Issue.STATUS_OPEN);
Component component2 = mock(Component.class);
when(component2.getType()).thenReturn(Component.Type.FILE);
ReportAttributes reportAttributes2 = mock(ReportAttributes.class);
when(reportAttributes2.getScmPath()).thenReturn(Optional.empty());
when(component2.getReportAttributes()).thenReturn(reportAttributes2);
PostAnalysisIssueVisitor.ComponentIssue componentIssue2 = new PostAnalysisIssueVisitor.ComponentIssue(component2, lightIssue2);
PostAnalysisIssueVisitor.LightIssue lightIssue3 = mock(PostAnalysisIssueVisitor.LightIssue.class);
when(lightIssue3.status()).thenReturn(Issue.STATUS_OPEN);
Component component3 = mock(Component.class);
when(component3.getType()).thenReturn(Component.Type.PROJECT);
ReportAttributes reportAttributes3 = mock(ReportAttributes.class);
when(reportAttributes3.getScmPath()).thenReturn(Optional.of("path"));
when(component3.getReportAttributes()).thenReturn(reportAttributes3);
PostAnalysisIssueVisitor.ComponentIssue componentIssue3 = new PostAnalysisIssueVisitor.ComponentIssue(component3, lightIssue3);
PostAnalysisIssueVisitor.LightIssue lightIssue4 = mock(PostAnalysisIssueVisitor.LightIssue.class);
when(lightIssue4.status()).thenReturn(Issue.STATUS_CLOSED);
Component component4 = mock(Component.class);
when(component4.getType()).thenReturn(Component.Type.FILE);
ReportAttributes reportAttributes4 = mock(ReportAttributes.class);
when(reportAttributes4.getScmPath()).thenReturn(Optional.of("path"));
when(component4.getReportAttributes()).thenReturn(reportAttributes4);
PostAnalysisIssueVisitor.ComponentIssue componentIssue4 = new PostAnalysisIssueVisitor.ComponentIssue(component4, lightIssue4);
PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class);
when(postAnalysisIssueVisitor.getIssues()).thenReturn(Arrays.asList(componentIssue1, componentIssue2, componentIssue3, componentIssue4));
AnalysisDetails underTest = new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), postAnalysisIssueVisitor,
mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), mock(Analysis.class), mock(Project.class),
mock(Configuration.class),"", mock(ScannerContext.class));
assertThat(underTest.getScmReportableIssues()).containsOnly(componentIssue1);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Michael Clarke
* Copyright (C) 2019-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -57,7 +57,7 @@ public class PostAnalysisIssueVisitorTest {
for (int i = 0; i < 100; i++) {
DefaultIssue issue = (i == 10 ? null : mock(DefaultIssue.class));
Component component = (i == 5 ? null : mock(Component.class));
expected.add(new PostAnalysisIssueVisitor.ComponentIssue(component, issue));
expected.add(new PostAnalysisIssueVisitor.ComponentIssue(component, null == issue ? null : new PostAnalysisIssueVisitor.LightIssue(issue)));
testCase.onIssue(component, issue);
}

View File

@ -3,12 +3,13 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.BitbucketClient;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.BitbucketClientFactory;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit;
import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.issue.Issue;
import org.sonar.api.measures.CoreMetrics;
@ -27,18 +28,15 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.junit.Assert.assertFalse;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class BitbucketPullRequestDecoratorTest {
class BitbucketPullRequestDecoratorTest {
private static final String PROJECT = "project";
private static final String REPO = "repo";
private static final String COMMIT = "commit";
private static final String ISSUE_KEY = "issue-key";
@ -57,15 +55,13 @@ public class BitbucketPullRequestDecoratorTest {
private final AlmSettingDto almSettingDto = mock(AlmSettingDto.class);
private final ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class);
@Before
public void setUp() {
@BeforeEach
void setUp() {
when(bitbucketClientFactory.createClient(any(), any())).thenReturn(client);
when(client.resolveProject(any(), any())).thenReturn(PROJECT);
when(client.resolveRepository(any(), any())).thenReturn(REPO);
}
@Test
public void testValidAnalysis() throws IOException {
void testValidAnalysis() throws IOException {
when(client.supportsCodeInsights()).thenReturn(true);
AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(1000, 1000);
when(client.getAnnotationUploadLimit()).thenReturn(uploadLimit);
@ -73,43 +69,19 @@ public class BitbucketPullRequestDecoratorTest {
mockValidAnalysis();
underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto);
verify(client).createCodeInsightsAnnotation(eq(ISSUE_KEY), eq(ISSUE_LINE), eq(ISSUE_LINK), eq(ISSUE_MESSAGE), eq(ISSUE_PATH), eq("HIGH"), eq("BUG"));
verify(client).createCodeInsightsAnnotation(ISSUE_KEY, ISSUE_LINE, ISSUE_LINK, ISSUE_MESSAGE, ISSUE_PATH, "HIGH", "BUG");
verify(client).createLinkDataValue(DASHBOARD_URL);
verify(client).createCodeInsightsReport(any(), eq("Quality Gate passed" + System.lineSeparator()), any(), eq(DASHBOARD_URL), eq(String.format("%s/common/icon.png", IMAGE_URL)), eq(QualityGate.Status.OK));
verify(client).deleteAnnotations(PROJECT, REPO, COMMIT);
verify(client).createCodeInsightsReport(any(), eq("Quality Gate passed" + System.lineSeparator()), any(), eq(DASHBOARD_URL), eq(String.format("%s/common/icon.png", IMAGE_URL)), eq(ReportStatus.PASSED));
verify(client).deleteAnnotations(COMMIT);
}
@Test
public void testExceedsMaximumNumberOfAnnotations() {
@ParameterizedTest(name = "{arguments}")
@CsvSource({"100, 1000, 2",
"1000, 1000, 1",
"100, 1000, 10"})
void testExceedsMaximumNumberOfAnnotations(int annotationBatchSize, int totalAllowedAnnotations, int counter) {
// given
AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(100, 1000);
int counter = 2;
// when
boolean result = BitbucketPullRequestDecorator.exceedsMaximumNumberOfAnnotations(counter, uploadLimit);
// then
assertFalse(result);
}
@Test
public void testExceedsMaximumNumberOfAnnotationsEdgeCase() {
// given
AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(1000, 1000);
int counter = 1;
// when
boolean result = BitbucketPullRequestDecorator.exceedsMaximumNumberOfAnnotations(counter, uploadLimit);
// then
assertFalse(result);
}
@Test
public void testExceedsMaximumNumberOfAnnotationsEdgeBatchCase() {
// given
AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(100, 1000);
int counter = 10;
AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(annotationBatchSize, totalAllowedAnnotations);
// when
boolean result = BitbucketPullRequestDecorator.exceedsMaximumNumberOfAnnotations(counter, uploadLimit);
@ -155,10 +127,7 @@ public class BitbucketPullRequestDecoratorTest {
when(componentIssue.getIssue()).thenReturn(defaultIssue);
when(componentIssue.getComponent()).thenReturn(component);
PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class);
when(postAnalysisIssueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue));
when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor);
when(analysisDetails.getScmReportableIssues()).thenReturn(Collections.singletonList(componentIssue));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Michael Clarke
* Copyright (C) 2021-2022 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -72,7 +72,7 @@ class BitbucketValidatorTest {
void testInvalidConfigurationExceptionThrownIfRetrieveRepositoryFails() throws IOException {
BitbucketValidator underTest = new BitbucketValidator(bitbucketClientFactory);
BitbucketClient bitbucketClient = mock(BitbucketClient.class);
when(bitbucketClient.retrieveRepository(any(), any())).thenThrow(new IOException("dummy"));
when(bitbucketClient.retrieveRepository()).thenThrow(new IOException("dummy"));
when(bitbucketClientFactory.createClient(any(), any())).thenReturn(bitbucketClient);
assertThatThrownBy(() -> underTest.validate(projectAlmSettingDto, almSettingDto))
.isInstanceOf(InvalidConfigurationException.class)