Browse Source

SONAR-21119 Provide method to get groups for GitLab & refactored GithubPaginatedHttpClient and GithubApplicationHttpClient to make them generic

tags/10.4.0.87286
Aurelien Poscia 6 months ago
parent
commit
68de595dc6
33 changed files with 683 additions and 294 deletions
  1. 1
    0
      server/sonar-alm-client/build.gradle
  2. 1
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/ApplicationHttpClient.java
  3. 1
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/DevopsPlatformHeaders.java
  4. 10
    12
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/GenericApplicationHttpClient.java
  5. 98
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/GenericPaginatedHttpClient.java
  6. 2
    4
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/PaginatedHttpClient.java
  7. 3
    3
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/RatioBasedRateLimitChecker.java
  8. 17
    36
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
  9. 1
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationHttpClient.java
  10. 1
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubHeaders.java
  11. 5
    57
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubPaginatedHttpClient.java
  12. 23
    31
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabApplicationClient.java
  13. 33
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabApplicationHttpClient.java
  14. 7
    7
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabGlobalSettingsValidator.java
  15. 60
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHeaders.java
  16. 35
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabPaginatedHttpClient.java
  17. 58
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabToken.java
  18. 29
    5
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/GenericPaginatedHttpClientImplTest.java
  19. 2
    2
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/RatioBasedRateLimitCheckerTest.java
  20. 37
    18
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GenericApplicationHttpClientTest.java
  21. 46
    63
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
  22. 66
    3
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabApplicationClientTest.java
  23. 1
    3
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabGlobalSettingsValidatorTest.java
  24. 76
    0
      server/sonar-alm-client/src/test/resources/org/sonar/alm/client/gitlab/groups-full-response.json
  25. 17
    3
      server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonGroup.java
  26. 3
    1
      server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonGroupTest.java
  27. 5
    5
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/CheckPatActionIT.java
  28. 11
    11
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionIT.java
  29. 7
    7
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposActionIT.java
  30. 5
    5
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/CheckPatAction.java
  31. 6
    6
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java
  32. 5
    5
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposAction.java
  33. 11
    5
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java

+ 1
- 0
server/sonar-alm-client/build.gradle View File

@@ -13,6 +13,7 @@ dependencies {
api 'org.bouncycastle:bcpkix-jdk18on:1.76'
api 'org.sonarsource.api.plugin:sonar-plugin-api'
api project(':server:sonar-auth-github')
api project(':server:sonar-auth-gitlab')

testImplementation project(':sonar-plugin-api-impl')


server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/ApplicationHttpClient.java → server/sonar-alm-client/src/main/java/org/sonar/alm/client/ApplicationHttpClient.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github;
package org.sonar.alm.client;

import java.io.IOException;
import java.util.Optional;

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/DevopsPlatformHeaders.java → server/sonar-alm-client/src/main/java/org/sonar/alm/client/DevopsPlatformHeaders.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github;
package org.sonar.alm.client;

import java.util.Optional;


server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GenericApplicationHttpClient.java → server/sonar-alm-client/src/main/java/org/sonar/alm/client/GenericApplicationHttpClient.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github;
package org.sonar.alm.client;

import java.io.IOException;
import java.net.MalformedURLException;
@@ -37,7 +37,6 @@ import okhttp3.ResponseBody;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.TimeoutConfiguration;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonarqube.ws.client.OkHttpClientBuilder;

@@ -58,7 +57,7 @@ public abstract class GenericApplicationHttpClient implements ApplicationHttpCli
private final DevopsPlatformHeaders devopsPlatformHeaders;
private final OkHttpClient client;

public GenericApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
protected GenericApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
this.devopsPlatformHeaders = devopsPlatformHeaders;
client = new OkHttpClientBuilder()
.setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
@@ -184,7 +183,7 @@ public abstract class GenericApplicationHttpClient implements ApplicationHttpCli
private Request.Builder newRequestBuilder(String appUrl, @Nullable AccessToken token, String endPoint) {
Request.Builder url = new Request.Builder().url(toAbsoluteEndPoint(appUrl, endPoint));
if (token != null) {
url.addHeader(devopsPlatformHeaders.getAuthorizationHeader(), token.getAuthorizationHeaderPrefix() + " " + token);
url.addHeader(devopsPlatformHeaders.getAuthorizationHeader(), token.getAuthorizationHeaderPrefix() + " " + token.getValue());
devopsPlatformHeaders.getApiVersion().ifPresent(apiVersion ->
url.addHeader(devopsPlatformHeaders.getApiVersionHeader().orElseThrow(), apiVersion)
);
@@ -224,17 +223,16 @@ public abstract class GenericApplicationHttpClient implements ApplicationHttpCli

@CheckForNull
private static String readNextEndPoint(okhttp3.Response response) {
String links = response.headers().get("link");
if (links == null || links.isEmpty() || !links.contains("rel=\"next\"")) {
return null;
}

String links = Optional.ofNullable(response.headers().get("link")).orElse("");
Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(links);
if (!nextLinkMatcher.find()) {
return null;
}

return nextLinkMatcher.group(1);
String nextUrl = nextLinkMatcher.group(1);
if (response.request().url().toString().equals(nextUrl)) {
return null;
}
return nextUrl;
}

@CheckForNull
@@ -250,7 +248,7 @@ public abstract class GenericApplicationHttpClient implements ApplicationHttpCli

@CheckForNull
private static <T> T headerValueOrNull(okhttp3.Response response, String header, Function<String, T> mapper) {
return ofNullable(response.header(header)).map(mapper::apply).orElse(null);
return ofNullable(response.headers().get(header)).map(mapper::apply).orElse(null);
}

private static class ResponseImpl implements Response {

+ 98
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/GenericPaginatedHttpClient.java View File

@@ -0,0 +1,98 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.alm.client;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.ApplicationHttpClient.GetResponse;
import org.sonar.alm.client.github.security.AccessToken;

import static java.lang.String.format;

public abstract class GenericPaginatedHttpClient implements PaginatedHttpClient {

private static final Logger LOG = LoggerFactory.getLogger(GenericPaginatedHttpClient.class);
private final ApplicationHttpClient appHttpClient;
private final RatioBasedRateLimitChecker rateLimitChecker;

protected GenericPaginatedHttpClient(ApplicationHttpClient appHttpClient, RatioBasedRateLimitChecker rateLimitChecker) {
this.appHttpClient = appHttpClient;
this.rateLimitChecker = rateLimitChecker;
}

@Override
public <E> List<E> get(String appUrl, AccessToken token, String query, Function<String, List<E>> responseDeserializer) {
List<E> results = new ArrayList<>();
String nextEndpoint = query + "?per_page=100";
if (query.contains("?")) {
nextEndpoint = query + "&per_page=100";
}
ApplicationHttpClient.RateLimit rateLimit = null;
while (nextEndpoint != null) {
checkRateLimit(rateLimit);
GetResponse response = executeCall(appUrl, token, nextEndpoint);
response.getContent()
.ifPresent(content -> results.addAll(responseDeserializer.apply(content)));
nextEndpoint = response.getNextEndPoint().orElse(null);
rateLimit = response.getRateLimit();
}
return results;
}

private void checkRateLimit(@Nullable ApplicationHttpClient.RateLimit rateLimit) {
if (rateLimit == null) {
return;
}
try {
rateLimitChecker.checkRateLimit(rateLimit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.warn(format("Thread interrupted: %s", e.getMessage()), e);
}
}

private GetResponse executeCall(String appUrl, AccessToken token, String endpoint) {
try {
GetResponse response = appHttpClient.get(appUrl, token, endpoint);
if (response.getCode() < 200 || response.getCode() >= 300) {
throw new IllegalStateException(
format("Error while executing a call to %s. Return code %s. Error message: %s.", appUrl, response.getCode(), response.getContent().orElse("")));
}
return response;
} catch (Exception e) {
String errorMessage = format("SonarQube was not able to retrieve resources from external system. Error while executing a paginated call to %s, endpoint:%s.",
appUrl, endpoint);
logException(errorMessage, e);
throw new IllegalStateException(errorMessage + " " + e.getMessage());
}
}

private static void logException(String message, Exception e) {
if (LOG.isDebugEnabled()) {
LOG.warn(message, e);
} else {
LOG.warn(message, e.getMessage());
}
}
}

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/PaginatedHttpClient.java → server/sonar-alm-client/src/main/java/org/sonar/alm/client/PaginatedHttpClient.java View File

@@ -17,14 +17,12 @@
* 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.alm.client.github;
package org.sonar.alm.client;

import java.io.IOException;
import java.util.List;
import java.util.function.Function;
import org.sonar.alm.client.github.security.AccessToken;

public interface PaginatedHttpClient {

<E> List<E> get(String appUrl, AccessToken token, String query, Function<String, List<E>> responseDeserializer) throws IOException;
<E> List<E> get(String appUrl, AccessToken token, String query, Function<String, List<E>> responseDeserializer);
}

server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/RatioBasedRateLimitChecker.java → server/sonar-alm-client/src/main/java/org/sonar/alm/client/RatioBasedRateLimitChecker.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github;
package org.sonar.alm.client;

import com.google.common.annotations.VisibleForTesting;
import org.kohsuke.github.GHRateLimit;
@@ -33,7 +33,7 @@ public class RatioBasedRateLimitChecker extends RateLimitChecker {
private static final Logger LOGGER = LoggerFactory.getLogger(RatioBasedRateLimitChecker.class);

@VisibleForTesting
static final String RATE_RATIO_EXCEEDED_MESSAGE = "The GitHub API rate limit is almost reached. Pausing GitHub provisioning until the next rate limit reset. "
static final String RATE_RATIO_EXCEEDED_MESSAGE = "The external system API rate limit is almost reached. Pausing GitHub provisioning until the next rate limit reset. "
+ "{} out of {} calls were used.";

private static final int MAX_PERCENTAGE_OF_CALLS_FOR_PROVISIONING = 90;
@@ -42,7 +42,7 @@ public class RatioBasedRateLimitChecker extends RateLimitChecker {
int limit = rateLimitRecord.limit();
int apiCallsUsed = limit - rateLimitRecord.remaining();
double percentageOfCallsUsed = computePercentageOfCallsUsed(apiCallsUsed, limit);
LOGGER.debug("{} GitHub API calls used of {} available per hours", apiCallsUsed, limit);
LOGGER.debug("{} external system API calls used of {}", apiCallsUsed, limit);
if (percentageOfCallsUsed >= MAX_PERCENTAGE_OF_CALLS_FOR_PROVISIONING) {
LOGGER.warn(RATE_RATIO_EXCEEDED_MESSAGE, apiCallsUsed, limit);
GHRateLimit.Record rateLimit = new GHRateLimit.Record(rateLimitRecord.limit(), rateLimitRecord.remaining(), rateLimitRecord.reset());

+ 17
- 36
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java View File

@@ -37,7 +37,8 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.github.ApplicationHttpClient.GetResponse;
import org.sonar.alm.client.ApplicationHttpClient;
import org.sonar.alm.client.ApplicationHttpClient.GetResponse;
import org.sonar.alm.client.github.GithubBinding.GsonGithubRepository;
import org.sonar.alm.client.github.GithubBinding.GsonInstallations;
import org.sonar.alm.client.github.GithubBinding.GsonRepositorySearch;
@@ -69,21 +70,17 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
protected static final String WRITE_PERMISSION_NAME = "write";
protected static final String READ_PERMISSION_NAME = "read";
protected static final String FAILED_TO_REQUEST_BEGIN_MSG = "Failed to request ";

private static final String EXCEPTION_MESSAGE = "SonarQube was not able to retrieve resources from GitHub. "
+ "This is likely due to a connectivity problem or a temporary network outage";

private static final Type REPOSITORY_TEAM_LIST_TYPE = TypeToken.getParameterized(List.class, GsonRepositoryTeam.class).getType();
private static final Type REPOSITORY_COLLABORATORS_LIST_TYPE = TypeToken.getParameterized(List.class, GsonRepositoryCollaborator.class).getType();
private static final Type ORGANIZATION_LIST_TYPE = TypeToken.getParameterized(List.class, GithubBinding.GsonInstallation.class).getType();
protected final ApplicationHttpClient appHttpClient;
protected final GithubApplicationHttpClient githubApplicationHttpClient;
protected final GithubAppSecurity appSecurity;
private final GitHubSettings gitHubSettings;
private final PaginatedHttpClient githubPaginatedHttpClient;
private final GithubPaginatedHttpClient githubPaginatedHttpClient;

public GithubApplicationClientImpl(ApplicationHttpClient appHttpClient, GithubAppSecurity appSecurity, GitHubSettings gitHubSettings,
PaginatedHttpClient githubPaginatedHttpClient) {
this.appHttpClient = appHttpClient;
public GithubApplicationClientImpl(GithubApplicationHttpClient githubApplicationHttpClient, GithubAppSecurity appSecurity, GitHubSettings gitHubSettings,
GithubPaginatedHttpClient githubPaginatedHttpClient) {
this.githubApplicationHttpClient = githubApplicationHttpClient;
this.appSecurity = appSecurity;
this.gitHubSettings = gitHubSettings;
this.githubPaginatedHttpClient = githubPaginatedHttpClient;
@@ -106,7 +103,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {

private <T> Optional<T> post(String baseUrl, AccessToken token, String endPoint, Class<T> gsonClass) {
try {
ApplicationHttpClient.Response response = appHttpClient.post(baseUrl, token, endPoint);
ApplicationHttpClient.Response response = githubApplicationHttpClient.post(baseUrl, token, endPoint);
return handleResponse(response, endPoint, gsonClass);
} catch (Exception e) {
LOG.warn(FAILED_TO_REQUEST_BEGIN_MSG + endPoint, e);
@@ -146,7 +143,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
String endPoint = "/app";
GetResponse response;
try {
response = appHttpClient.get(githubAppConfiguration.getApiEndpoint(), appToken, endPoint);
response = githubApplicationHttpClient.get(githubAppConfiguration.getApiEndpoint(), appToken, endPoint);
} catch (IOException e) {
LOG.warn(FAILED_TO_REQUEST_BEGIN_MSG + githubAppConfiguration.getApiEndpoint() + endPoint, e);
throw new IllegalArgumentException("Failed to validate configuration, check URL and Private Key");
@@ -189,7 +186,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {

try {
Organizations organizations = new Organizations();
GetResponse response = appHttpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", page, pageSize));
GetResponse response = githubApplicationHttpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", page, pageSize));
Optional<GsonInstallations> gsonInstallations = response.getContent().map(content -> GSON.fromJson(content, GsonInstallations.class));

if (!gsonInstallations.isPresent()) {
@@ -247,7 +244,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {

protected <T> Optional<T> get(String baseUrl, AccessToken token, String endPoint, Class<T> gsonClass) {
try {
GetResponse response = appHttpClient.get(baseUrl, token, endPoint);
GetResponse response = githubApplicationHttpClient.get(baseUrl, token, endPoint);
return handleResponse(response, endPoint, gsonClass);
} catch (Exception e) {
LOG.warn(FAILED_TO_REQUEST_BEGIN_MSG + endPoint, e);
@@ -264,7 +261,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
}
try {
Repositories repositories = new Repositories();
GetResponse response = appHttpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", searchQuery, page, pageSize));
GetResponse response = githubApplicationHttpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", searchQuery, page, pageSize));
Optional<GsonRepositorySearch> gsonRepositories = response.getContent().map(content -> GSON.fromJson(content, GsonRepositorySearch.class));
if (!gsonRepositories.isPresent()) {
return repositories;
@@ -288,7 +285,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
@Override
public Optional<Repository> getRepository(String appUrl, AccessToken accessToken, String organizationAndRepository) {
try {
GetResponse response = appHttpClient.get(appUrl, accessToken, String.format("/repos/%s", organizationAndRepository));
GetResponse response = githubApplicationHttpClient.get(appUrl, accessToken, String.format("/repos/%s", organizationAndRepository));
return Optional.of(response)
.filter(r -> r.getCode() == HTTP_OK)
.flatMap(ApplicationHttpClient.Response::getContent)
@@ -315,7 +312,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
baseAppUrl = appUrl;
}

ApplicationHttpClient.Response response = appHttpClient.post(baseAppUrl, null, endpoint);
ApplicationHttpClient.Response response = githubApplicationHttpClient.post(baseAppUrl, null, endpoint);

if (response.getCode() != HTTP_OK) {
throw new IllegalStateException("Failed to create GitHub's user access token. GitHub returned code " + code + ". " + response.getContent().orElse(""));
@@ -333,7 +330,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
}

// If token is not in the 200's body, it's because the client ID or client secret are incorrect
LOG.error("Failed to create GitHub's user access token. GitHub's response: " + content);
LOG.error("Failed to create GitHub's user access token. GitHub's response: {}", content);
throw new IllegalArgumentException();
} catch (IOException e) {
throw new IllegalStateException("Failed to create GitHub's user access token", e);
@@ -349,7 +346,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {

private <T> T getOrThrowIfNotHttpOk(String baseUrl, AccessToken token, String endPoint, Class<T> gsonClass) {
try {
GetResponse response = appHttpClient.get(baseUrl, token, endPoint);
GetResponse response = githubApplicationHttpClient.get(baseUrl, token, endPoint);
if (response.getCode() != HTTP_OK) {
throw new HttpException(baseUrl + endPoint, response.getCode(), response.getContent().orElse(""));
}
@@ -386,23 +383,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
}

private <E> List<E> executePaginatedQuery(String appUrl, AccessToken token, String query, Function<String, List<E>> responseDeserializer) {
try {
return githubPaginatedHttpClient.get(appUrl, token, query, responseDeserializer);
} catch (IOException ioException) {
throw logAndCreateException(ioException, format("Error while executing a paginated call to GitHub - appUrl: %s, path: %s.", appUrl, query));
}
return githubPaginatedHttpClient.get(appUrl, token, query, responseDeserializer);
}

private static IllegalStateException logAndCreateException(IOException ioException, String errorMessage) {
log(errorMessage, ioException);
return new IllegalStateException(EXCEPTION_MESSAGE + ": " + errorMessage + " " + ioException.getMessage());
}

private static void log(String message, Exception e) {
if (LOG.isDebugEnabled()) {
LOG.warn(message, e);
} else {
LOG.warn(message);
}
}
}

+ 1
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationHttpClient.java View File

@@ -19,6 +19,7 @@
*/
package org.sonar.alm.client.github;

import org.sonar.alm.client.GenericApplicationHttpClient;
import org.sonar.alm.client.TimeoutConfiguration;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;

+ 1
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubHeaders.java View File

@@ -20,6 +20,7 @@
package org.sonar.alm.client.github;

import java.util.Optional;
import org.sonar.alm.client.DevopsPlatformHeaders;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;


+ 5
- 57
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubPaginatedHttpClient.java View File

@@ -19,69 +19,17 @@
*/
package org.sonar.alm.client.github;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.GenericPaginatedHttpClient;
import org.sonar.alm.client.RatioBasedRateLimitChecker;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;

import static java.lang.String.format;

@ServerSide
@ComputeEngineSide
public class GithubPaginatedHttpClient implements PaginatedHttpClient {

private static final Logger LOG = LoggerFactory.getLogger(GithubPaginatedHttpClient.class);
private final ApplicationHttpClient appHttpClient;
private final RatioBasedRateLimitChecker rateLimitChecker;

public GithubPaginatedHttpClient(ApplicationHttpClient appHttpClient, RatioBasedRateLimitChecker rateLimitChecker) {
this.appHttpClient = appHttpClient;
this.rateLimitChecker = rateLimitChecker;
}
public class GithubPaginatedHttpClient extends GenericPaginatedHttpClient {

@Override
public <E> List<E> get(String appUrl, AccessToken token, String query, Function<String, List<E>> responseDeserializer) throws IOException {
List<E> results = new ArrayList<>();
String nextEndpoint = query + "?per_page=100";
if (query.contains("?")) {
nextEndpoint = query + "&per_page=100";
}
ApplicationHttpClient.RateLimit rateLimit = null;
while (nextEndpoint != null) {
checkRateLimit(rateLimit);
ApplicationHttpClient.GetResponse response = executeCall(appUrl, token, nextEndpoint);
response.getContent()
.ifPresent(content -> results.addAll(responseDeserializer.apply(content)));
nextEndpoint = response.getNextEndPoint().orElse(null);
rateLimit = response.getRateLimit();
}
return results;
public GithubPaginatedHttpClient(GithubApplicationHttpClient appHttpClient, RatioBasedRateLimitChecker rateLimitChecker) {
super(appHttpClient, rateLimitChecker);
}

private void checkRateLimit(@Nullable ApplicationHttpClient.RateLimit rateLimit) {
if (rateLimit == null) {
return;
}
try {
rateLimitChecker.checkRateLimit(rateLimit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.warn(format("Thread interrupted: %s", e.getMessage()), e);
}
}

private ApplicationHttpClient.GetResponse executeCall(String appUrl, AccessToken token, String endpoint) throws IOException {
ApplicationHttpClient.GetResponse response = appHttpClient.get(appUrl, token, endpoint);
if (response.getCode() < 200 || response.getCode() >= 300) {
throw new IllegalStateException(
format("Error while executing a call to GitHub. Return code %s. Error message: %s.", response.getCode(), response.getContent().orElse("")));
}
return response;
}
}

server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java → server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabApplicationClient.java View File

@@ -19,15 +19,20 @@
*/
package org.sonar.alm.client.gitlab;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
@@ -39,6 +44,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.TimeoutConfiguration;
import org.sonar.api.server.ServerSide;
import org.sonar.auth.gitlab.GsonGroup;
import org.sonarqube.ws.MediaTypes;
import org.sonarqube.ws.client.OkHttpClientBuilder;

@@ -47,13 +53,18 @@ import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static java.nio.charset.StandardCharsets.UTF_8;

@ServerSide
public class GitlabHttpClient {
public class GitlabApplicationClient {
private static final Logger LOG = LoggerFactory.getLogger(GitlabApplicationClient.class);
private static final Gson GSON = new Gson();
private static final Type GITLAB_GROUP = TypeToken.getParameterized(List.class, GsonGroup.class).getType();

private static final Logger LOG = LoggerFactory.getLogger(GitlabHttpClient.class);
protected static final String PRIVATE_TOKEN = "Private-Token";
protected final OkHttpClient client;

public GitlabHttpClient(TimeoutConfiguration timeoutConfiguration) {
private final GitlabPaginatedHttpClient gitlabPaginatedHttpClient;

public GitlabApplicationClient(GitlabPaginatedHttpClient gitlabPaginatedHttpClient, TimeoutConfiguration timeoutConfiguration) {
this.gitlabPaginatedHttpClient = gitlabPaginatedHttpClient;
client = new OkHttpClientBuilder()
.setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
.setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
@@ -324,34 +335,6 @@ public class GitlabHttpClient {
}
}

/*public void getGroups(String gitlabUrl, String token) {
String url = String.format("%s/groups", gitlabUrl);
LOG.debug(String.format("get groups : [%s]", url));

Request request = new Request.Builder()
.addHeader(PRIVATE_TOKEN, token)
.url(url)
.get()
.build();


try (Response response = client.newCall(request).execute()) {
Headers headers = response.headers();
checkResponseIsSuccessful(response, "Could not get projects from GitLab instance");
List<Project> projectList = Project.parseJsonArray(response.body().string());
int returnedPageNumber = parseAndGetIntegerHeader(headers.get("X-Page"));
int returnedPageSize = parseAndGetIntegerHeader(headers.get("X-Per-Page"));
String xtotal = headers.get("X-Total");
Integer totalProjects = Strings.isEmpty(xtotal) ? null : parseAndGetIntegerHeader(xtotal);
return new ProjectList(projectList, returnedPageNumber, returnedPageSize, totalProjects);
} catch (JsonSyntaxException e) {
throw new IllegalArgumentException("Could not parse GitLab answer to search projects. Got a non-json payload as result.");
} catch (IOException e) {
logException(url, e);
throw new IllegalStateException(e.getMessage(), e);
}
}*/

private static int parseAndGetIntegerHeader(@Nullable String header) {
if (header == null) {
throw new IllegalArgumentException("Pagination data from GitLab response is missing");
@@ -364,4 +347,13 @@ public class GitlabHttpClient {
}
}

public Set<GsonGroup> getGroups(String gitlabUrl, String token) {
return Set.copyOf(executePaginatedQuery(gitlabUrl, token, "/groups", resp -> GSON.fromJson(resp, GITLAB_GROUP)));
}

private <E> List<E> executePaginatedQuery(String appUrl, String token, String query, Function<String, List<E>> responseDeserializer) {
GitlabToken gitlabToken = new GitlabToken(token);
return gitlabPaginatedHttpClient.get(appUrl, gitlabToken, query, responseDeserializer);
}

}

+ 33
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabApplicationHttpClient.java View File

@@ -0,0 +1,33 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.alm.client.gitlab;

import org.sonar.alm.client.TimeoutConfiguration;
import org.sonar.alm.client.GenericApplicationHttpClient;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;

@ServerSide
@ComputeEngineSide
public class GitlabApplicationHttpClient extends GenericApplicationHttpClient {
public GitlabApplicationHttpClient(GitlabHeaders gitlabHeaders, TimeoutConfiguration timeoutConfiguration) {
super(gitlabHeaders, timeoutConfiguration);
}
}

+ 7
- 7
server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabGlobalSettingsValidator.java View File

@@ -28,11 +28,11 @@ import org.sonar.db.alm.setting.AlmSettingDto;
public class GitlabGlobalSettingsValidator {

private final Encryption encryption;
private final GitlabHttpClient gitlabHttpClient;
private final GitlabApplicationClient gitlabApplicationClient;

public GitlabGlobalSettingsValidator(GitlabHttpClient gitlabHttpClient, Settings settings) {
public GitlabGlobalSettingsValidator(GitlabApplicationClient gitlabApplicationClient, Settings settings) {
this.encryption = settings.getEncryption();
this.gitlabHttpClient = gitlabHttpClient;
this.gitlabApplicationClient = gitlabApplicationClient;
}

public void validate(AlmSettingDto almSettingDto) {
@@ -43,10 +43,10 @@ public class GitlabGlobalSettingsValidator {
throw new IllegalArgumentException("Your Gitlab global configuration is incomplete.");
}

gitlabHttpClient.checkUrl(gitlabUrl);
gitlabHttpClient.checkToken(gitlabUrl, accessToken);
gitlabHttpClient.checkReadPermission(gitlabUrl, accessToken);
gitlabHttpClient.checkWritePermission(gitlabUrl, accessToken);
gitlabApplicationClient.checkUrl(gitlabUrl);
gitlabApplicationClient.checkToken(gitlabUrl, accessToken);
gitlabApplicationClient.checkReadPermission(gitlabUrl, accessToken);
gitlabApplicationClient.checkWritePermission(gitlabUrl, accessToken);
}

}

+ 60
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHeaders.java View File

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.alm.client.gitlab;

import java.util.Optional;
import org.sonar.alm.client.DevopsPlatformHeaders;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;

@ServerSide
@ComputeEngineSide
public class GitlabHeaders implements DevopsPlatformHeaders {

@Override
public Optional<String> getApiVersionHeader() {
return Optional.empty();
}

@Override
public Optional<String> getApiVersion() {
return Optional.empty();
}

@Override
public String getRateLimitRemainingHeader() {
return "ratelimit-remaining";
}

@Override
public String getRateLimitLimitHeader() {
return "ratelimit-limit";
}

@Override
public String getRateLimitResetHeader() {
return "ratelimit-reset";
}

@Override
public String getAuthorizationHeader() {
return "PRIVATE-TOKEN";
}
}

+ 35
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabPaginatedHttpClient.java View File

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.alm.client.gitlab;

import org.sonar.alm.client.GenericPaginatedHttpClient;
import org.sonar.alm.client.RatioBasedRateLimitChecker;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.server.ServerSide;

@ServerSide
@ComputeEngineSide
public class GitlabPaginatedHttpClient extends GenericPaginatedHttpClient {

public GitlabPaginatedHttpClient(GitlabApplicationHttpClient appHttpClient, RatioBasedRateLimitChecker rateLimitChecker) {
super(appHttpClient, rateLimitChecker);
}

}

+ 58
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabToken.java View File

@@ -0,0 +1,58 @@
/*
* SonarQube
* Copyright (C) 2009-2023 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.alm.client.gitlab;

import java.util.Objects;
import org.sonar.alm.client.github.security.AccessToken;

public class GitlabToken implements AccessToken {
private final String token;

public GitlabToken(String token) {
this.token = token;
}

@Override
public String getValue() {
return token;
}

@Override
public String getAuthorizationHeaderPrefix() {
return "";
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GitlabToken that = (GitlabToken) o;
return Objects.equals(token, that.token);
}

@Override
public int hashCode() {
return Objects.hash(token);
}
}

server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubPaginatedHttpClientImplTest.java → server/sonar-alm-client/src/test/java/org/sonar/alm/client/GenericPaginatedHttpClientImplTest.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github;
package org.sonar.alm.client;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
@@ -45,10 +45,10 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.sonar.alm.client.github.ApplicationHttpClient.GetResponse;
import static org.sonar.alm.client.ApplicationHttpClient.GetResponse;

@RunWith(MockitoJUnitRunner.class)
public class GithubPaginatedHttpClientImplTest {
public class GenericPaginatedHttpClientImplTest {

private static final String APP_URL = "https://github.com/";

@@ -71,7 +71,13 @@ public class GithubPaginatedHttpClientImplTest {
ApplicationHttpClient appHttpClient;

@InjectMocks
private GithubPaginatedHttpClient underTest;
private TestPaginatedHttpClient underTest;

private static class TestPaginatedHttpClient extends GenericPaginatedHttpClient {
protected TestPaginatedHttpClient(ApplicationHttpClient appHttpClient, RatioBasedRateLimitChecker rateLimitChecker) {
super(appHttpClient, rateLimitChecker);
}
}

@Test
public void get_whenNoPagination_ReturnsCorrectResponse() throws IOException {
@@ -141,7 +147,8 @@ public class GithubPaginatedHttpClientImplTest {

assertThatIllegalStateException()
.isThrownBy(() -> underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE)))
.withMessage("Error while executing a call to GitHub. Return code 400. Error message: failed.");
.withMessage("SonarQube was not able to retrieve resources from external system. Error while executing a paginated call to https://github.com/, endpoint:/next-endpoint. "
+ "Error while executing a call to https://github.com/. Return code 400. Error message: failed.");
}

private static GetResponse mockFailedResponse(String content) {
@@ -166,4 +173,21 @@ public class GithubPaginatedHttpClientImplTest {
assertThat(logTester.logs(Level.WARN))
.containsExactly("Thread interrupted: interrupted");
}

@Test
public void getRepositoryCollaborators_whenDevOpsPlatformCallThrowsIOException_shouldLogAndReThrow() throws IOException {
AccessToken accessToken = mock();
when(appHttpClient.get(APP_URL, accessToken, "query?per_page=100")).thenThrow(new IOException("error"));

assertThatIllegalStateException()
.isThrownBy(() -> underTest.get(APP_URL, accessToken, "query", mock()))
.isInstanceOf(IllegalStateException.class)
.withMessage("SonarQube was not able to retrieve resources from external system. Error while executing a paginated call to https://github.com/, "
+ "endpoint:query?per_page=100. error");

assertThat(logTester.logs()).hasSize(1);
assertThat(logTester.logs(Level.WARN))
.containsExactly("SonarQube was not able to retrieve resources from external system. "
+ "Error while executing a paginated call to https://github.com/, endpoint:query?per_page=100.");
}
}

server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/RatioBasedRateLimitCheckerTest.java → server/sonar-alm-client/src/test/java/org/sonar/alm/client/RatioBasedRateLimitCheckerTest.java View File

@@ -17,7 +17,7 @@
* 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.alm.client.github;
package org.sonar.alm.client;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
@@ -32,7 +32,7 @@ import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.alm.client.github.RatioBasedRateLimitChecker.RATE_RATIO_EXCEEDED_MESSAGE;
import static org.sonar.alm.client.RatioBasedRateLimitChecker.RATE_RATIO_EXCEEDED_MESSAGE;

@RunWith(DataProviderRunner.class)
public class RatioBasedRateLimitCheckerTest {

+ 37
- 18
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GenericApplicationHttpClientTest.java View File

@@ -36,9 +36,11 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.event.Level;
import org.sonar.alm.client.ConstantTimeoutConfiguration;
import org.sonar.alm.client.DevopsPlatformHeaders;
import org.sonar.alm.client.GenericApplicationHttpClient;
import org.sonar.alm.client.TimeoutConfiguration;
import org.sonar.alm.client.github.ApplicationHttpClient.GetResponse;
import org.sonar.alm.client.github.ApplicationHttpClient.Response;
import org.sonar.alm.client.ApplicationHttpClient.GetResponse;
import org.sonar.alm.client.ApplicationHttpClient.Response;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.api.testfixtures.log.LogTester;
@@ -49,7 +51,7 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.fail;
import static org.sonar.alm.client.github.ApplicationHttpClient.RateLimit;
import static org.sonar.alm.client.ApplicationHttpClient.RateLimit;

@RunWith(DataProviderRunner.class)
public class GenericApplicationHttpClientTest {
@@ -76,7 +78,7 @@ public class GenericApplicationHttpClientTest {
logTester.clear();
}

private class TestApplicationHttpClient extends GenericApplicationHttpClient {
private static class TestApplicationHttpClient extends GenericApplicationHttpClient {
public TestApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
super(devopsPlatformHeaders, timeoutConfiguration);
}
@@ -183,7 +185,7 @@ public class GenericApplicationHttpClientTest {
public void get_returns_empty_endPoint_when_link_header_does_not_have_next_rel() throws IOException {
server.enqueue(new MockResponse().setBody(randomBody)
.setHeader("link", "<https://api.github.com/installation/repositories?per_page=5&page=4>; rel=\"prev\", " +
"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""));
"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""));

GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);

@@ -212,18 +214,30 @@ public class GenericApplicationHttpClientTest {
assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2");
}

@Test
public void get_returns_endPoint_when_link_header_is_from_gitlab() throws IOException {
String linkHeader = "<https://gitlab.com/api/v4/groups?all_available=false&order_by=name&owned=false&page=2&per_page=2&sort=asc&statistics=false&with_custom_attributes=false>; rel=\"next\", <https://gitlab.com/api/v4/groups?all_available=false&order_by=name&owned=false&page=1&per_page=2&sort=asc&statistics=false&with_custom_attributes=false>; rel=\"first\", <https://gitlab.com/api/v4/groups?all_available=false&order_by=name&owned=false&page=8&per_page=2&sort=asc&statistics=false&with_custom_attributes=false>; rel=\"last\"";
server.enqueue(new MockResponse().setBody(randomBody)
.setHeader("link", linkHeader));

GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint);

assertThat(response.getNextEndPoint()).contains("https://gitlab.com/api/v4/groups?all_available=false"
+ "&order_by=name&owned=false&page=2&per_page=2&sort=asc&statistics=false&with_custom_attributes=false");
}

@DataProvider
public static Object[][] linkHeadersWithNextRel() {
String expected = "https://api.github.com/installation/repositories?per_page=5&page=2";
return new Object[][] {
{"<" + expected + ">; rel=\"next\""},
{"<" + expected + ">; rel=\"next\", " +
"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""},
"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\""},
{"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
"<" + expected + ">; rel=\"next\""},
"<" + expected + ">; rel=\"next\""},
{"<https://api.github.com/installation/repositories?per_page=5&page=1>; rel=\"first\", " +
"<" + expected + ">; rel=\"next\", " +
"<https://api.github.com/installation/repositories?per_page=5&page=5>; rel=\"last\""},
"<" + expected + ">; rel=\"next\", " +
"<https://api.github.com/installation/repositories?per_page=5&page=5>; rel=\"last\""},
};
}

@@ -416,14 +430,19 @@ public class GenericApplicationHttpClientTest {

@Test
public void get_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
testRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint));
testRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint), false);
}

@Test
public void get_whenRateLimitHeadersArePresentAndUppercased_returnsRateLimit() throws Exception {
testRateLimitHeader(() -> underTest.get(appUrl, accessToken, randomEndPoint), true);
}

private void testRateLimitHeader(Callable<Response> request ) throws Exception {
private void testRateLimitHeader(Callable<Response> request, boolean uppercasedHeaders) throws Exception {
server.enqueue(new MockResponse().setBody(randomBody)
.setHeader("x-ratelimit-remaining", "1")
.setHeader("x-ratelimit-limit", "10")
.setHeader("x-ratelimit-reset", "1000"));
.setHeader(uppercasedHeaders ? "x-ratelimit-remaining" : "x-ratelimit-REMAINING", "1")
.setHeader(uppercasedHeaders ? "x-ratelimit-limit" : "X-RATELIMIT-LIMIT", "10")
.setHeader(uppercasedHeaders ? "x-ratelimit-reset" : "X-ratelimit-reset", "1000"));

Response response = request.call();

@@ -438,7 +457,7 @@ public class GenericApplicationHttpClientTest {

}

private void testMissingRateLimitHeader(Callable<Response> request ) throws Exception {
private void testMissingRateLimitHeader(Callable<Response> request) throws Exception {
server.enqueue(new MockResponse().setBody(randomBody));

Response response = request.call();
@@ -448,7 +467,7 @@ public class GenericApplicationHttpClientTest {

@Test
public void delete_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
testRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint));
testRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint), false);

}

@@ -460,7 +479,7 @@ public class GenericApplicationHttpClientTest {

@Test
public void patch_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
testRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"));
testRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"), false);
}

@Test
@@ -470,7 +489,7 @@ public class GenericApplicationHttpClientTest {

@Test
public void post_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
testRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint));
testRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint), false);
}

@Test

+ 46
- 63
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java View File

@@ -36,7 +36,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.slf4j.event.Level;
import org.sonar.alm.client.github.ApplicationHttpClient.RateLimit;
import org.sonar.alm.client.ApplicationHttpClient.RateLimit;
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
import org.sonar.alm.client.github.api.GsonRepositoryTeam;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
@@ -68,7 +68,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.sonar.alm.client.github.ApplicationHttpClient.GetResponse;
import static org.sonar.alm.client.ApplicationHttpClient.GetResponse;

@RunWith(DataProviderRunner.class)
public class GithubApplicationClientImplTest {
@@ -114,12 +114,12 @@ public class GithubApplicationClientImplTest {
@ClassRule
public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);

private GenericApplicationHttpClient httpClient = mock();
private GithubApplicationHttpClient githubApplicationHttpClient = mock();
private GithubAppSecurity appSecurity = mock();
private GithubAppConfiguration githubAppConfiguration = mock();
private GitHubSettings gitHubSettings = mock();

private PaginatedHttpClient githubPaginatedHttpClient = mock();
private GithubPaginatedHttpClient githubPaginatedHttpClient = mock();
private AppInstallationToken appInstallationToken = mock();
private GithubApplicationClient underTest;

@@ -129,7 +129,7 @@ public class GithubApplicationClientImplTest {
@Before
public void setup() {
when(githubAppConfiguration.getApiEndpoint()).thenReturn(appUrl);
underTest = new GithubApplicationClientImpl(httpClient, appSecurity, gitHubSettings, githubPaginatedHttpClient);
underTest = new GithubApplicationClientImpl(githubApplicationHttpClient, appSecurity, gitHubSettings, githubPaginatedHttpClient);
logTester.clear();
}

@@ -179,7 +179,7 @@ public class GithubApplicationClientImplTest {
public void checkAppPermissions_IOException() throws IOException {
AppToken appToken = mockAppToken();

when(httpClient.get(appUrl, appToken, "/app")).thenThrow(new IOException("OOPS"));
when(githubApplicationHttpClient.get(appUrl, appToken, "/app")).thenThrow(new IOException("OOPS"));

assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
.isInstanceOf(IllegalArgumentException.class)
@@ -191,7 +191,7 @@ public class GithubApplicationClientImplTest {
public void checkAppPermissions_ErrorCodes(int errorCode, String expectedMessage) throws IOException {
AppToken appToken = mockAppToken();

when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new ErrorGetResponse(errorCode, null));
when(githubApplicationHttpClient.get(appUrl, appToken, "/app")).thenReturn(new ErrorGetResponse(errorCode, null));

assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
.isInstanceOf(IllegalArgumentException.class)
@@ -211,7 +211,7 @@ public class GithubApplicationClientImplTest {
public void checkAppPermissions_MissingPermissions() throws IOException {
AppToken appToken = mockAppToken();

when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse("{}"));
when(githubApplicationHttpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse("{}"));

assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
.isInstanceOf(IllegalArgumentException.class)
@@ -230,7 +230,7 @@ public class GithubApplicationClientImplTest {
+ " }\n"
+ "}";

when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));
when(githubApplicationHttpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));

assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
.isInstanceOf(IllegalArgumentException.class)
@@ -249,7 +249,7 @@ public class GithubApplicationClientImplTest {
+ " }\n"
+ "}";

when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));
when(githubApplicationHttpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));

assertThatCode(() -> underTest.checkAppPermissions(githubAppConfiguration)).isNull();
}
@@ -258,7 +258,7 @@ public class GithubApplicationClientImplTest {
public void getInstallationId_returns_installation_id_of_given_account() throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(httpClient.get(appUrl, appToken, "/repos/torvalds/linux/installation"))
when(githubApplicationHttpClient.get(appUrl, appToken, "/repos/torvalds/linux/installation"))
.thenReturn(new OkGetResponse("{" +
" \"id\": 2," +
" \"account\": {" +
@@ -281,7 +281,7 @@ public class GithubApplicationClientImplTest {
public void getInstallationId_return_empty_if_no_installation_found_for_githubAccount() throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(httpClient.get(appUrl, appToken, "/repos/torvalds/linux/installation"))
when(githubApplicationHttpClient.get(appUrl, appToken, "/repos/torvalds/linux/installation"))
.thenReturn(new ErrorGetResponse(404, null));

assertThat(underTest.getInstallationId(githubAppConfiguration, "torvalds")).isEmpty();
@@ -290,44 +290,44 @@ public class GithubApplicationClientImplTest {
@Test
@UseDataProvider("githubServers")
public void createUserAccessToken_returns_empty_if_access_token_cant_be_created(String apiUrl, String appUrl) throws IOException {
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
when(githubApplicationHttpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
.thenReturn(new Response(400, null));

assertThatThrownBy(() -> underTest.createUserAccessToken(appUrl, "clientId", "clientSecret", "code"))
.isInstanceOf(IllegalStateException.class);
verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
verify(githubApplicationHttpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
}

@Test
@UseDataProvider("githubServers")
public void createUserAccessToken_fail_if_access_token_request_fails(String apiUrl, String appUrl) throws IOException {
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
when(githubApplicationHttpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
.thenThrow(new IOException("OOPS"));

assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Failed to create GitHub's user access token");

verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
verify(githubApplicationHttpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
}

@Test
@UseDataProvider("githubServers")
public void createUserAccessToken_throws_illegal_argument_exception_if_access_token_code_is_expired(String apiUrl, String appUrl) throws IOException {
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
when(githubApplicationHttpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
.thenReturn(new OkGetResponse("error_code=100&error=expired_or_invalid"));

assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"))
.isInstanceOf(IllegalArgumentException.class);

verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
verify(githubApplicationHttpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
}

@Test
@UseDataProvider("githubServers")
public void createUserAccessToken_from_authorization_code_returns_access_token(String apiUrl, String appUrl) throws IOException {
String token = randomAlphanumeric(10);
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
when(githubApplicationHttpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
.thenReturn(new OkGetResponse("access_token=" + token + "&status="));

UserAccessToken userAccessToken = underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code");
@@ -335,14 +335,14 @@ public class GithubApplicationClientImplTest {
assertThat(userAccessToken)
.extracting(UserAccessToken::getValue, UserAccessToken::getAuthorizationHeaderPrefix)
.containsOnly(token, "token");
verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
verify(githubApplicationHttpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
}

@Test
public void getApp_returns_id() throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(httpClient.get(appUrl, appToken, "/app"))
when(githubApplicationHttpClient.get(appUrl, appToken, "/app"))
.thenReturn(new OkGetResponse("{\"installations_count\": 2}"));

assertThat(underTest.getApp(githubAppConfiguration).getInstallationsCount()).isEqualTo(2L);
@@ -352,7 +352,7 @@ public class GithubApplicationClientImplTest {
public void getApp_whenStatusCodeIsNotOk_shouldThrowHttpException() throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(httpClient.get(appUrl, appToken, "/app"))
when(githubApplicationHttpClient.get(appUrl, appToken, "/app"))
.thenReturn(new ErrorGetResponse(418, "I'm a teapot"));

assertThatThrownBy(() -> underTest.getApp(githubAppConfiguration))
@@ -378,7 +378,7 @@ public class GithubApplicationClientImplTest {
String appUrl = "https://github.sonarsource.com";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));

when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
when(githubApplicationHttpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
.thenThrow(new IOException("OOPS"));

assertThatThrownBy(() -> underTest.listOrganizations(appUrl, accessToken, 1, 100))
@@ -413,7 +413,7 @@ public class GithubApplicationClientImplTest {
+ " \"total_count\": 0\n"
+ "} ";

when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
when(githubApplicationHttpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
.thenReturn(new OkGetResponse(responseJson));

GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100);
@@ -504,7 +504,7 @@ public class GithubApplicationClientImplTest {
+ " ]\n"
+ "} ";

when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
when(githubApplicationHttpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
.thenReturn(new OkGetResponse(responseJson));

GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100);
@@ -581,18 +581,14 @@ public class GithubApplicationClientImplTest {
}

@Test
public void getWhitelistedGithubAppInstallations_whenGithubReturnsError_shouldThrow() throws IOException {
public void getWhitelistedGithubAppInstallations_whenGithubReturnsError_shouldReThrow() {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(githubPaginatedHttpClient.get(any(), any(), any(), any())).thenThrow(new IOException("io exception"));
when(githubPaginatedHttpClient.get(any(), any(), any(), any())).thenThrow(new IllegalStateException("exception"));

assertThatThrownBy(() -> underTest.getWhitelistedGithubAppInstallations(githubAppConfiguration))
.isInstanceOf(IllegalStateException.class)
.hasMessage(
"SonarQube was not able to retrieve resources from GitHub. "
+ "This is likely due to a connectivity problem or a temporary network outage: "
+ "Error while executing a paginated call to GitHub - appUrl: Any URL, path: /app/installations. io exception"
);
.hasMessage("exception");
}

@Test
@@ -600,7 +596,7 @@ public class GithubApplicationClientImplTest {
String appUrl = "https://github.sonarsource.com";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));

when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:test", 1, 100)))
when(githubApplicationHttpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:test", 1, 100)))
.thenThrow(new IOException("OOPS"));

assertThatThrownBy(() -> underTest.listRepositories(appUrl, accessToken, "test", null, 1, 100))
@@ -635,7 +631,7 @@ public class GithubApplicationClientImplTest {
+ " \"total_count\": 0\n"
+ "}";

when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
when(githubApplicationHttpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
.thenReturn(new OkGetResponse(responseJson));

GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100);
@@ -723,7 +719,7 @@ public class GithubApplicationClientImplTest {
+ " ]\n"
+ "}";

when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
when(githubApplicationHttpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
.thenReturn(new OkGetResponse(responseJson));
GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100);

@@ -778,7 +774,7 @@ public class GithubApplicationClientImplTest {
+ " ]\n"
+ "}";

when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "world+fork:true+org:github", 1, 100)))
when(githubApplicationHttpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "world+fork:true+org:github", 1, 100)))
.thenReturn(new GetResponse() {
@Override
public Optional<String> getNextEndPoint() {
@@ -811,7 +807,7 @@ public class GithubApplicationClientImplTest {

@Test
public void getRepository_returns_empty_when_repository_doesnt_exist() throws IOException {
when(httpClient.get(any(), any(), any()))
when(githubApplicationHttpClient.get(any(), any(), any()))
.thenReturn(new Response(404, null));

Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, new UserAccessToken("temp"), "octocat/Hello-World");
@@ -823,7 +819,7 @@ public class GithubApplicationClientImplTest {
public void getRepository_fails_on_failure() throws IOException {
String repositoryKey = "octocat/Hello-World";

when(httpClient.get(any(), any(), any()))
when(githubApplicationHttpClient.get(any(), any(), any()))
.thenThrow(new IOException("OOPS"));

UserAccessToken token = new UserAccessToken("temp");
@@ -974,7 +970,7 @@ public class GithubApplicationClientImplTest {
+ " }"
+ "}";

when(httpClient.get(appUrl, accessToken, "/repos/octocat/Hello-World"))
when(githubApplicationHttpClient.get(appUrl, accessToken, "/repos/octocat/Hello-World"))
.thenReturn(new GetResponse() {
@Override
public Optional<String> getNextEndPoint() {
@@ -1022,7 +1018,7 @@ public class GithubApplicationClientImplTest {
@Test
public void createAppInstallationToken_returns_empty_if_post_throws_IOE() throws IOException {
mockAppToken();
when(httpClient.post(anyString(), any(AccessToken.class), anyString())).thenThrow(IOException.class);
when(githubApplicationHttpClient.post(anyString(), any(AccessToken.class), anyString())).thenThrow(IOException.class);
Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);

assertThat(accessToken).isEmpty();
@@ -1037,7 +1033,7 @@ public class GithubApplicationClientImplTest {
Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);

assertThat(accessToken).isEmpty();
verify(httpClient).post(appUrl, appToken, "/app/installations/" + INSTALLATION_ID + "/access_tokens");
verify(githubApplicationHttpClient).post(appUrl, appToken, "/app/installations/" + INSTALLATION_ID + "/access_tokens");
}

@Test
@@ -1048,7 +1044,7 @@ public class GithubApplicationClientImplTest {
Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);

assertThat(accessToken).hasValue(installToken);
verify(httpClient).post(appUrl, appToken, "/app/installations/" + INSTALLATION_ID + "/access_tokens");
verify(githubApplicationHttpClient).post(appUrl, appToken, "/app/installations/" + INSTALLATION_ID + "/access_tokens");
}

@Test
@@ -1067,18 +1063,12 @@ public class GithubApplicationClientImplTest {
}

@Test
public void getRepositoryTeams_whenGitHubCallThrowsIOException_shouldLogAndThrow() throws IOException {
when(githubPaginatedHttpClient.get(eq(APP_URL), eq(appInstallationToken), eq(REPO_TEAMS_ENDPOINT), any())).thenThrow(new IOException("error"));
public void getRepositoryTeams_whenGitHubCallThrowsException_shouldRethrow() {
when(githubPaginatedHttpClient.get(eq(APP_URL), eq(appInstallationToken), eq(REPO_TEAMS_ENDPOINT), any())).thenThrow(new IllegalStateException("error"));

assertThatIllegalStateException()
.isThrownBy(() -> underTest.getRepositoryTeams(APP_URL, appInstallationToken, ORG_NAME, REPO_NAME))
.isInstanceOf(IllegalStateException.class)
.withMessage(
"SonarQube was not able to retrieve resources from GitHub. This is likely due to a connectivity problem or a temporary network outage: Error while executing a paginated call to GitHub - appUrl: https://github.com/, path: /repos/ORG_NAME/repo1/teams. error");

assertThat(logTester.logs()).hasSize(1);
assertThat(logTester.logs(Level.WARN))
.containsExactly("Error while executing a paginated call to GitHub - appUrl: https://github.com/, path: /repos/ORG_NAME/repo1/teams.");
.withMessage("error");
}

private static List<GsonRepositoryTeam> expectedTeams() {
@@ -1104,19 +1094,12 @@ public class GithubApplicationClientImplTest {
}

@Test
public void getRepositoryCollaborators_whenGitHubCallThrowsIOException_shouldLogAndThrow() throws IOException {
when(githubPaginatedHttpClient.get(eq(APP_URL), eq(appInstallationToken), eq(REPO_COLLABORATORS_ENDPOINT), any())).thenThrow(new IOException("error"));
public void getRepositoryCollaborators_whenGitHubCallThrowsException_shouldRethrow() {
when(githubPaginatedHttpClient.get(eq(APP_URL), eq(appInstallationToken), eq(REPO_COLLABORATORS_ENDPOINT), any())).thenThrow(new IllegalStateException("error"));

assertThatIllegalStateException()
.isThrownBy(() -> underTest.getRepositoryCollaborators(APP_URL, appInstallationToken, ORG_NAME, REPO_NAME))
.isInstanceOf(IllegalStateException.class)
.withMessage(
"SonarQube was not able to retrieve resources from GitHub. This is likely due to a connectivity problem or a temporary network outage: "
+ "Error while executing a paginated call to GitHub - appUrl: https://github.com/, path: /repos/ORG_NAME/repo1/collaborators?affiliation=direct. error");

assertThat(logTester.logs()).hasSize(1);
assertThat(logTester.logs(Level.WARN))
.containsExactly("Error while executing a paginated call to GitHub - appUrl: https://github.com/, path: /repos/ORG_NAME/repo1/collaborators?affiliation=direct.");
.withMessage("error");
}

private static String getResponseContent(String path) throws IOException {
@@ -1133,7 +1116,7 @@ public class GithubApplicationClientImplTest {
Response response = mock(Response.class);
when(response.getContent()).thenReturn(Optional.empty());
when(response.getCode()).thenReturn(HTTP_UNAUTHORIZED);
when(httpClient.post(eq(appUrl), any(AppToken.class), eq("/app/installations/" + INSTALLATION_ID + "/access_tokens"))).thenReturn(response);
when(githubApplicationHttpClient.post(eq(appUrl), any(AppToken.class), eq("/app/installations/" + INSTALLATION_ID + "/access_tokens"))).thenReturn(response);
}

private AppToken mockAppToken() {
@@ -1149,7 +1132,7 @@ public class GithubApplicationClientImplTest {
" \"token\": \"" + token + "\"" +
"}"));
when(response.getCode()).thenReturn(HTTP_CREATED);
when(httpClient.post(eq(appUrl), any(AppToken.class), eq("/app/installations/" + INSTALLATION_ID + "/access_tokens"))).thenReturn(response);
when(githubApplicationHttpClient.post(eq(appUrl), any(AppToken.class), eq("/app/installations/" + INSTALLATION_ID + "/access_tokens"))).thenReturn(response);
return new AppInstallationToken(token);
}


server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java → server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabApplicationClientTest.java View File

@@ -20,31 +20,46 @@
package org.sonar.alm.client.gitlab;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.slf4j.event.Level;
import org.sonar.alm.client.ConstantTimeoutConfiguration;
import org.sonar.alm.client.TimeoutConfiguration;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.auth.gitlab.GsonGroup;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class GitlabHttpClientTest {
public class GitlabApplicationClientTest {

@Rule
public LogTester logTester = new LogTester();


private GitlabPaginatedHttpClient gitlabPaginatedHttpClient = mock();

private final MockWebServer server = new MockWebServer();
private GitlabHttpClient underTest;
private GitlabApplicationClient underTest;
private String gitlabUrl;

@Before
@@ -54,7 +69,7 @@ public class GitlabHttpClientTest {
gitlabUrl = urlWithEndingSlash.substring(0, urlWithEndingSlash.length() - 1);

TimeoutConfiguration timeoutConfiguration = new ConstantTimeoutConfiguration(10_000);
underTest = new GitlabHttpClient(timeoutConfiguration);
underTest = new GitlabApplicationClient(gitlabPaginatedHttpClient, timeoutConfiguration);
}

@After
@@ -523,4 +538,52 @@ public class GitlabHttpClientTest {
+ "] " +
"failed with error message : [Failed to connect to " + server.getHostName());
}

@Test
public void getGroups_whenCallIsInError_rethrows() throws IOException {
String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/groups"), any())).thenThrow(new IllegalStateException("exception"));

assertThatIllegalStateException()
.isThrownBy(() -> underTest.getGroups(gitlabUrl, token))
.withMessage("exception");
}

@Test
public void getGroups_whenCallIsSuccessful_deserializesAndReturnsCorrectlyGroups() throws IOException {
ArgumentCaptor<Function<String, List<GsonGroup>>> deserializerCaptor = ArgumentCaptor.forClass(Function.class);

String token = "token-toto";
GitlabToken gitlabToken = new GitlabToken(token);
List<GsonGroup> expectedGroups = expectedGroups();
when(gitlabPaginatedHttpClient.get(eq(gitlabUrl), eq(gitlabToken), eq("/groups"), deserializerCaptor.capture())).thenReturn(expectedGroups);

Set<GsonGroup> groups = underTest.getGroups(gitlabUrl, token);
assertThat(groups).containsExactlyInAnyOrderElementsOf(expectedGroups);

String responseContent = getResponseContent("groups-full-response.json");

List<GsonGroup> deserializedGroups = deserializerCaptor.getValue().apply(responseContent);
assertThat(deserializedGroups).usingRecursiveComparison().isEqualTo(expectedGroups);
}

private static List<GsonGroup> expectedGroups() {
GsonGroup gsonGroup = createGsonGroup("56232243", "sonarsource/cfamily", "this is a long description");
GsonGroup gsonGroup2 = createGsonGroup("78902256", "sonarsource/sonarqube/mmf-3052-ant1", "");
return List.of(gsonGroup, gsonGroup2);
}

private static GsonGroup createGsonGroup(String number, String fullPath, String description) {
GsonGroup gsonGroup = mock(GsonGroup.class);
when(gsonGroup.getId()).thenReturn(number);
when(gsonGroup.getFullPath()).thenReturn(fullPath);
when(gsonGroup.getDescription()).thenReturn(description);
return gsonGroup;
}

private static String getResponseContent(String path) throws IOException {
return IOUtils.toString(GitlabApplicationClientTest.class.getResourceAsStream(path), StandardCharsets.UTF_8);
}

}

+ 1
- 3
server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabGlobalSettingsValidatorTest.java View File

@@ -21,8 +21,6 @@ package org.sonar.alm.client.gitlab;

import org.junit.BeforeClass;
import org.junit.Test;
import org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.api.config.internal.Encryption;
import org.sonar.api.config.internal.Settings;
import org.sonar.db.alm.setting.AlmSettingDto;
@@ -37,7 +35,7 @@ public class GitlabGlobalSettingsValidatorTest {
private static final Encryption encryption = mock(Encryption.class);
private static final Settings settings = mock(Settings.class);

private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class);
private final GitlabApplicationClient gitlabHttpClient = mock(GitlabApplicationClient.class);

private final GitlabGlobalSettingsValidator underTest = new GitlabGlobalSettingsValidator(gitlabHttpClient, settings);


+ 76
- 0
server/sonar-alm-client/src/test/resources/org/sonar/alm/client/gitlab/groups-full-response.json View File

@@ -0,0 +1,76 @@
[
{
"id": 56232243,
"web_url": "https://gitlab.com/groups/sonarsource/cfamily",
"name": "CFamily",
"path": "cfamily",
"description": "this is a long description",
"visibility": "public",
"share_with_group_lock": false,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"project_creation_level": "maintainer",
"auto_devops_enabled": null,
"subgroup_creation_level": "owner",
"emails_disabled": false,
"emails_enabled": true,
"mentions_disabled": null,
"lfs_enabled": false,
"default_branch_protection": 2,
"default_branch_protection_defaults": {
"allowed_to_push": [
{
"access_level": 30
}
],
"allow_force_push": true,
"allowed_to_merge": [
{
"access_level": 30
}
]
},
"avatar_url": null,
"request_access_enabled": false,
"full_name": "SonarSource / CFamily",
"full_path": "sonarsource/cfamily",
"created_at": "2022-08-02T06:56:14.451Z",
"parent_id": 6164984,
"shared_runners_setting": "enabled",
"ldap_cn": null,
"ldap_access": null,
"marked_for_deletion_on": null,
"wiki_access_level": "enabled"
},
{
"id": 78902256,
"web_url": "https://gitlab.com/groups/sonarsource/sonarqube/mmf-3052-ant1",
"name": "MMF-3052-Ant1",
"path": "mmf-3052-ant1",
"description": "",
"visibility": "private",
"share_with_group_lock": true,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"project_creation_level": "developer",
"auto_devops_enabled": null,
"subgroup_creation_level": "maintainer",
"emails_disabled": false,
"emails_enabled": true,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
"default_branch_protection_defaults": {},
"avatar_url": null,
"request_access_enabled": true,
"full_name": "SonarSource / SonarQube / MMF-3052-Ant1",
"full_path": "sonarsource/sonarqube/mmf-3052-ant1",
"created_at": "2023-11-29T10:34:43.382Z",
"parent_id": 67918039,
"shared_runners_setting": "enabled",
"ldap_cn": null,
"ldap_access": null,
"marked_for_deletion_on": null,
"wiki_access_level": "enabled"
}
]

+ 17
- 3
server/sonar-auth-gitlab/src/main/java/org/sonar/auth/gitlab/GsonGroup.java View File

@@ -31,22 +31,36 @@ import java.util.List;
*/
public class GsonGroup {

@SerializedName("id")
private String id;
@SerializedName("full_path")
private String fullPath;
@SerializedName("description")
private String description;

public GsonGroup() {
// http://stackoverflow.com/a/18645370/229031
this("");
this("", "", "");
}

GsonGroup(String fullPath) {
private GsonGroup(String id, String fullPath, String description) {
this.id = id;
this.fullPath = fullPath;
this.description = description;
}

String getFullPath() {
public String getId() {
return id;
}

public String getFullPath() {
return fullPath;
}

public String getDescription() {
return description;
}

static List<GsonGroup> parse(String json) {
Type collectionType = new TypeToken<Collection<GsonGroup>>() {
}.getType();

+ 3
- 1
server/sonar-auth-gitlab/src/test/java/org/sonar/auth/gitlab/GsonGroupTest.java View File

@@ -33,7 +33,7 @@ public class GsonGroupTest {
"\"web_url\": \"https://gitlab.com/groups/my-awesome-group/my-project\",\n" +
"\"name\": \"my-project\",\n" +
"\"path\": \"my-project\",\n" +
"\"description\": \"\",\n" +
"\"description\": \"toto\",\n" +
"\"visibility\": \"private\",\n" +
"\"lfs_enabled\": true,\n" +
"\"avatar_url\": null,\n" +
@@ -47,6 +47,8 @@ public class GsonGroupTest {

assertThat(groups).isNotNull();
assertThat(groups.size()).isOne();
assertThat(groups.get(0).getId()).isEqualTo("123456789");
assertThat(groups.get(0).getFullPath()).isEqualTo("my-awesome-group/my-project");
assertThat(groups.get(0).getDescription()).isEqualTo("toto");
}
}

+ 5
- 5
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/CheckPatActionIT.java View File

@@ -24,7 +24,7 @@ import org.junit.Test;
import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient;
import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.alm.client.gitlab.GitlabApplicationClient;
import org.sonar.api.server.ws.WebService;
import org.sonar.db.DbTester;
import org.sonar.db.alm.pat.AlmPatDto;
@@ -60,9 +60,9 @@ public class CheckPatActionIT {
private final AzureDevOpsHttpClient azureDevOpsPrHttpClient = mock(AzureDevOpsHttpClient.class);
private final BitbucketCloudRestClient bitbucketCloudRestClient = mock(BitbucketCloudRestClient.class);
private final BitbucketServerRestClient bitbucketServerRestClient = mock(BitbucketServerRestClient.class);
private final GitlabHttpClient gitlabPrHttpClient = mock(GitlabHttpClient.class);
private final GitlabApplicationClient gitlabApplicationClient = mock(GitlabApplicationClient.class);
private final WsActionTester ws = new WsActionTester(new CheckPatAction(db.getDbClient(), userSession, azureDevOpsPrHttpClient,
bitbucketCloudRestClient, bitbucketServerRestClient, gitlabPrHttpClient));
bitbucketCloudRestClient, bitbucketServerRestClient, gitlabApplicationClient));

@Test
public void check_pat_for_github() {
@@ -134,7 +134,7 @@ public class CheckPatActionIT {
.execute();

assertThat(almSetting.getUrl()).isNotNull();
verify(gitlabPrHttpClient).searchProjects(almSetting.getUrl(), PAT_SECRET, null, null, null);
verify(gitlabApplicationClient).searchProjects(almSetting.getUrl(), PAT_SECRET, null, null, null);
}

@Test
@@ -175,7 +175,7 @@ public class CheckPatActionIT {

@Test
public void fail_when_personal_access_token_is_invalid_for_gitlab() {
when(gitlabPrHttpClient.searchProjects(any(), any(), any(), any(), any()))
when(gitlabApplicationClient.searchProjects(any(), any(), any(), any(), any()))
.thenThrow(new IllegalArgumentException("Invalid personal access token"));
UserDto user = db.users().insertUser();
userSession.logIn(user).addPermission(PROVISION_PROJECTS);

+ 11
- 11
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionIT.java View File

@@ -26,7 +26,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.alm.client.gitlab.GitLabBranch;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.alm.client.gitlab.GitlabApplicationClient;
import org.sonar.alm.client.gitlab.Project;
import org.sonar.api.utils.System2;
import org.sonar.core.i18n.I18n;
@@ -94,14 +94,14 @@ public class ImportGitLabProjectActionIT {
mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestIndexers(), new SequenceUuidFactory(),
defaultBranchNameResolver, mock(PermissionUpdater.class), mock(PermissionService.class));

private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class);
private final GitlabApplicationClient gitlabApplicationClient = mock(GitlabApplicationClient.class);
private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
private NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider);
private final ImportGitLabProjectAction importGitLabProjectAction = new ImportGitLabProjectAction(
db.getDbClient(), userSession, projectDefaultVisibility, gitlabHttpClient, componentUpdater, importHelper, projectKeyGenerator, newCodeDefinitionResolver,
db.getDbClient(), userSession, projectDefaultVisibility, gitlabApplicationClient, componentUpdater, importHelper, projectKeyGenerator, newCodeDefinitionResolver,
defaultBranchNameResolver);
private final WsActionTester ws = new WsActionTester(importGitLabProjectAction);

@@ -125,7 +125,7 @@ public class ImportGitLabProjectActionIT {
.setParam(PARAM_NEW_CODE_DEFINITION_VALUE, "30")
.executeProtobuf(Projects.CreateWsResponse.class);

verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L);
verify(gitlabApplicationClient).getProject(almSetting.getUrl(), "PAT", 12345L);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
@@ -179,8 +179,8 @@ public class ImportGitLabProjectActionIT {
.setParam("gitlabProjectId", "12345")
.executeProtobuf(Projects.CreateWsResponse.class);

verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L);
verify(gitlabHttpClient).getBranches(almSetting.getUrl(), "PAT", 12345L);
verify(gitlabApplicationClient).getProject(almSetting.getUrl(), "PAT", 12345L);
verify(gitlabApplicationClient).getBranches(almSetting.getUrl(), "PAT", 12345L);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
@@ -205,8 +205,8 @@ public class ImportGitLabProjectActionIT {
.setParam("gitlabProjectId", "12345")
.executeProtobuf(Projects.CreateWsResponse.class);

verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L);
verify(gitlabHttpClient).getBranches(almSetting.getUrl(), "PAT", 12345L);
verify(gitlabApplicationClient).getProject(almSetting.getUrl(), "PAT", 12345L);
verify(gitlabApplicationClient).getBranches(almSetting.getUrl(), "PAT", 12345L);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
@@ -231,7 +231,7 @@ public class ImportGitLabProjectActionIT {
.setParam("gitlabProjectId", "12345")
.executeProtobuf(Projects.CreateWsResponse.class);

verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L);
verify(gitlabApplicationClient).getProject(almSetting.getUrl(), "PAT", 12345L);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
@@ -344,8 +344,8 @@ public class ImportGitLabProjectActionIT {

private Project mockGitlabProject(List<GitLabBranch> master) {
Project project = new Project(randomAlphanumeric(5), randomAlphanumeric(5));
when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project);
when(gitlabHttpClient.getBranches(any(), any(), any())).thenReturn(master);
when(gitlabApplicationClient.getProject(any(), any(), any())).thenReturn(project);
when(gitlabApplicationClient.getBranches(any(), any(), any())).thenReturn(master);
when(projectKeyGenerator.generateUniqueProjectKey(project.getPathWithNamespace())).thenReturn(PROJECT_KEY_NAME);
return project;
}

+ 7
- 7
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposActionIT.java View File

@@ -23,7 +23,7 @@ import java.util.Arrays;
import java.util.LinkedList;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.alm.client.gitlab.GitlabApplicationClient;
import org.sonar.alm.client.gitlab.Project;
import org.sonar.alm.client.gitlab.ProjectList;
import org.sonar.api.server.ws.WebService;
@@ -59,9 +59,9 @@ public class SearchGitlabReposActionIT {
@Rule
public DbTester db = DbTester.create();

private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class);
private final GitlabApplicationClient gitlabApplicationClient = mock(GitlabApplicationClient.class);
private final WsActionTester ws = new WsActionTester(new SearchGitlabReposAction(db.getDbClient(), userSession,
gitlabHttpClient));
gitlabApplicationClient));

@Test
public void list_gitlab_repos() {
@@ -69,7 +69,7 @@ public class SearchGitlabReposActionIT {
Project gitlabProject2 = new Project(2L, "repoName2", "path1 / repoName2", "repo-slug-2", "path-1/repo-slug-2", "url-2");
Project gitlabProject3 = new Project(3L, "repoName3", "repoName3 / repoName3", "repo-slug-3", "repo-slug-3/repo-slug-3", "url-3");
Project gitlabProject4 = new Project(4L, "repoName4", "repoName4 / repoName4 / repoName4", "repo-slug-4", "repo-slug-4/repo-slug-4/repo-slug-4", "url-4");
when(gitlabHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
when(gitlabApplicationClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
.thenReturn(
new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3, gitlabProject4), 1, 10, 4));

@@ -112,7 +112,7 @@ public class SearchGitlabReposActionIT {
Project gitlabProject2 = new Project(2L, "repoName2", "path1 / repoName2", "repo-slug-2", "path-1/repo-slug-2", "url-2");
Project gitlabProject3 = new Project(3L, "repoName3", "repoName3 / repoName3", "repo-slug-3", "repo-slug-3/repo-slug-3", "url-3");
Project gitlabProject4 = new Project(4L, "repoName4", "repoName4 / repoName4 / repoName4", "repo-slug-4", "repo-slug-4/repo-slug-4/repo-slug-4", "url-4");
when(gitlabHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
when(gitlabApplicationClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
.thenReturn(
new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3, gitlabProject4), 1, 10, 4));

@@ -160,7 +160,7 @@ public class SearchGitlabReposActionIT {

@Test
public void return_empty_list_when_no_gitlab_projects() {
when(gitlabHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt())).thenReturn(new ProjectList(new LinkedList<>(), 1, 10, 0));
when(gitlabApplicationClient.searchProjects(any(), any(), any(), anyInt(), anyInt())).thenReturn(new ProjectList(new LinkedList<>(), 1, 10, 0));
UserDto user = db.users().insertUser();
userSession.logIn(user).addPermission(PROVISION_PROJECTS);
AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting();
@@ -238,7 +238,7 @@ public class SearchGitlabReposActionIT {
"https://example.gitlab.com/group/gitlab-repo-name-2");
Project gitlabProject3 = new Project(3L, "Gitlab repo name 3", "Group / Gitlab repo name 3", "gitlab-repo-name-3", "group/gitlab-repo-name-3",
"https://example.gitlab.com/group/gitlab-repo-name-3");
when(gitlabHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
when(gitlabApplicationClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
.thenReturn(
new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3), 1, 3, 10));


+ 5
- 5
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/CheckPatAction.java View File

@@ -22,7 +22,7 @@ package org.sonar.server.almintegration.ws;
import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient;
import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.alm.client.gitlab.GitlabApplicationClient;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
@@ -50,19 +50,19 @@ public class CheckPatAction implements AlmIntegrationsWsAction {
private final AzureDevOpsHttpClient azureDevOpsHttpClient;
private final BitbucketCloudRestClient bitbucketCloudRestClient;
private final BitbucketServerRestClient bitbucketServerRestClient;
private final GitlabHttpClient gitlabHttpClient;
private final GitlabApplicationClient gitlabApplicationClient;

public CheckPatAction(DbClient dbClient, UserSession userSession,
AzureDevOpsHttpClient azureDevOpsHttpClient,
BitbucketCloudRestClient bitbucketCloudRestClient,
BitbucketServerRestClient bitbucketServerRestClient,
GitlabHttpClient gitlabHttpClient) {
GitlabApplicationClient gitlabApplicationClient) {
this.dbClient = dbClient;
this.userSession = userSession;
this.azureDevOpsHttpClient = azureDevOpsHttpClient;
this.bitbucketCloudRestClient = bitbucketCloudRestClient;
this.bitbucketServerRestClient = bitbucketServerRestClient;
this.gitlabHttpClient = gitlabHttpClient;
this.gitlabApplicationClient = gitlabApplicationClient;
}

@Override
@@ -113,7 +113,7 @@ public class CheckPatAction implements AlmIntegrationsWsAction {
requireNonNull(almPatDto.getPersonalAccessToken(), PAT_CANNOT_BE_NULL));
break;
case GITLAB:
gitlabHttpClient.searchProjects(
gitlabApplicationClient.searchProjects(
requireNonNull(almSettingDto.getUrl(), URL_CANNOT_BE_NULL),
requireNonNull(almPatDto.getPersonalAccessToken(), PAT_CANNOT_BE_NULL),
null, null, null);

+ 6
- 6
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java View File

@@ -23,7 +23,7 @@ import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.sonar.alm.client.gitlab.GitLabBranch;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.alm.client.gitlab.GitlabApplicationClient;
import org.sonar.alm.client.gitlab.Project;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
@@ -70,7 +70,7 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
private final DbClient dbClient;
private final UserSession userSession;
private final ProjectDefaultVisibility projectDefaultVisibility;
private final GitlabHttpClient gitlabHttpClient;
private final GitlabApplicationClient gitlabApplicationClient;
private final ComponentUpdater componentUpdater;
private final ImportHelper importHelper;
private final ProjectKeyGenerator projectKeyGenerator;
@@ -79,13 +79,13 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {

@Inject
public ImportGitLabProjectAction(DbClient dbClient, UserSession userSession,
ProjectDefaultVisibility projectDefaultVisibility, GitlabHttpClient gitlabHttpClient,
ProjectDefaultVisibility projectDefaultVisibility, GitlabApplicationClient gitlabApplicationClient,
ComponentUpdater componentUpdater, ImportHelper importHelper, ProjectKeyGenerator projectKeyGenerator, NewCodeDefinitionResolver newCodeDefinitionResolver,
DefaultBranchNameResolver defaultBranchNameResolver) {
this.dbClient = dbClient;
this.userSession = userSession;
this.projectDefaultVisibility = projectDefaultVisibility;
this.gitlabHttpClient = gitlabHttpClient;
this.gitlabApplicationClient = gitlabApplicationClient;
this.componentUpdater = componentUpdater;
this.importHelper = importHelper;
this.projectKeyGenerator = projectKeyGenerator;
@@ -139,7 +139,7 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
long gitlabProjectId = request.mandatoryParamAsLong(PARAM_GITLAB_PROJECT_ID);

String gitlabUrl = requireNonNull(almSettingDto.getUrl(), "DevOps Platform gitlabUrl cannot be null");
Project gitlabProject = gitlabHttpClient.getProject(gitlabUrl, pat, gitlabProjectId);
Project gitlabProject = gitlabApplicationClient.getProject(gitlabUrl, pat, gitlabProjectId);

Optional<String> almMainBranchName = getAlmDefaultBranch(pat, gitlabProjectId, gitlabUrl);

@@ -169,7 +169,7 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
}

private Optional<String> getAlmDefaultBranch(String pat, long gitlabProjectId, String gitlabUrl) {
Optional<GitLabBranch> almMainBranch = gitlabHttpClient.getBranches(gitlabUrl, pat, gitlabProjectId).stream().filter(GitLabBranch::isDefault).findFirst();
Optional<GitLabBranch> almMainBranch = gitlabApplicationClient.getBranches(gitlabUrl, pat, gitlabProjectId).stream().filter(GitLabBranch::isDefault).findFirst();
return almMainBranch.map(GitLabBranch::getName);
}


+ 5
- 5
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposAction.java View File

@@ -26,7 +26,7 @@ import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.alm.client.gitlab.GitlabApplicationClient;
import org.sonar.alm.client.gitlab.Project;
import org.sonar.alm.client.gitlab.ProjectList;
import org.sonar.api.server.ws.Request;
@@ -58,12 +58,12 @@ public class SearchGitlabReposAction implements AlmIntegrationsWsAction {

private final DbClient dbClient;
private final UserSession userSession;
private final GitlabHttpClient gitlabHttpClient;
private final GitlabApplicationClient gitlabApplicationClient;

public SearchGitlabReposAction(DbClient dbClient, UserSession userSession, GitlabHttpClient gitlabHttpClient) {
public SearchGitlabReposAction(DbClient dbClient, UserSession userSession, GitlabApplicationClient gitlabApplicationClient) {
this.dbClient = dbClient;
this.userSession = userSession;
this.gitlabHttpClient = gitlabHttpClient;
this.gitlabApplicationClient = gitlabApplicationClient;
}

@Override
@@ -113,7 +113,7 @@ public class SearchGitlabReposAction implements AlmIntegrationsWsAction {
String personalAccessToken = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found"));
String gitlabUrl = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null");

ProjectList gitlabProjectList = gitlabHttpClient
ProjectList gitlabProjectList = gitlabApplicationClient
.searchProjects(gitlabUrl, personalAccessToken, projectName, pageNumber, pageSize);

Map<String, ProjectKeyName> sqProjectsKeyByGitlabProjectId = getSqProjectsKeyByGitlabProjectId(dbSession, almSettingDto, gitlabProjectList);

+ 11
- 5
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -33,11 +33,14 @@ import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.alm.client.github.GithubHeaders;
import org.sonar.alm.client.github.GithubPaginatedHttpClient;
import org.sonar.alm.client.github.GithubPermissionConverter;
import org.sonar.alm.client.github.RatioBasedRateLimitChecker;
import org.sonar.alm.client.RatioBasedRateLimitChecker;
import org.sonar.alm.client.github.config.GithubProvisioningConfigValidator;
import org.sonar.alm.client.github.security.GithubAppSecurityImpl;
import org.sonar.alm.client.gitlab.GitlabApplicationHttpClient;
import org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator;
import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.alm.client.gitlab.GitlabHeaders;
import org.sonar.alm.client.gitlab.GitlabApplicationClient;
import org.sonar.alm.client.gitlab.GitlabPaginatedHttpClient;
import org.sonar.api.resources.ResourceTypes;
import org.sonar.api.server.rule.RulesDefinitionXmlLoader;
import org.sonar.auth.bitbucket.BitbucketModule;
@@ -555,22 +558,25 @@ public class PlatformLevel4 extends PlatformLevel {
ProjectKeyGenerator.class,
RatioBasedRateLimitChecker.class,
GithubAppSecurityImpl.class,
GithubApplicationClientImpl.class,
GithubPaginatedHttpClient.class,
GithubHeaders.class,
GithubApplicationHttpClient.class,
GithubPaginatedHttpClient.class,
GithubApplicationClientImpl.class,
GithubProvisioningConfigValidator.class,
GithubProvisioningWs.class,
GithubProjectCreatorFactory.class,
GithubPermissionConverter.class,
BitbucketCloudRestClientConfiguration.class,
BitbucketServerRestClient.class,
GitlabHttpClient.class,
AzureDevOpsHttpClient.class,
new AlmIntegrationsWSModule(),
BitbucketCloudValidator.class,
BitbucketServerSettingsValidator.class,
GithubGlobalSettingsValidator.class,
GitlabHeaders.class,
GitlabApplicationHttpClient.class,
GitlabPaginatedHttpClient.class,
GitlabApplicationClient.class,
GitlabGlobalSettingsValidator.class,
AzureDevOpsValidator.class,


Loading…
Cancel
Save