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')
--- /dev/null
+/*
+ * 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.io.IOException;
+import java.util.Optional;
+import org.sonar.alm.client.github.security.AccessToken;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+@ComputeEngineSide
+public interface ApplicationHttpClient {
+ /**
+ * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}.
+ */
+ GetResponse get(String appUrl, AccessToken token, String endPoint) throws IOException;
+
+ /**
+ * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}.
+ * No log if there is an issue during the call.
+ */
+ GetResponse getSilent(String appUrl, AccessToken token, String endPoint) throws IOException;
+
+ /**
+ * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK} or
+ * {@link java.net.HttpURLConnection#HTTP_CREATED CREATED}.
+ */
+ Response post(String appUrl, AccessToken token, String endPoint) throws IOException;
+
+ /**
+ * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK} or
+ * {@link java.net.HttpURLConnection#HTTP_CREATED CREATED}.
+ *
+ * Content type will be application/json; charset=utf-8
+ */
+ Response post(String appUrl, AccessToken token, String endPoint, String json) throws IOException;
+
+ /**
+ * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}.
+ *
+ * Content type will be application/json; charset=utf-8
+ */
+ Response patch(String appUrl, AccessToken token, String endPoint, String json) throws IOException;
+
+ /**
+ * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}.
+ *
+ * Content type will be application/json; charset=utf-8
+ *
+ */
+ Response delete(String appUrl, AccessToken token, String endPoint) throws IOException;
+
+ record RateLimit(int remaining, int limit, long reset) {
+ }
+ interface Response {
+
+ /**
+ * @return the HTTP code of the response.
+ */
+ int getCode();
+
+ /**
+ * @return the content of the response if the response had an HTTP code for which we expect a content for the current
+ * HTTP method (see {@link #get(String, AccessToken, String)} and {@link #post(String, AccessToken, String)}).
+ */
+ Optional<String> getContent();
+
+ RateLimit getRateLimit();
+ }
+
+ interface GetResponse extends Response {
+ Optional<String> getNextEndPoint();
+ }
+
+}
--- /dev/null
+/*
+ * 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.Optional;
+
+public interface DevopsPlatformHeaders {
+ Optional<String> getApiVersionHeader();
+
+ Optional<String> getApiVersion();
+
+ String getRateLimitRemainingHeader();
+
+ String getRateLimitLimitHeader();
+
+ String getRateLimitResetHeader();
+
+ String getAuthorizationHeader();
+}
--- /dev/null
+/*
+ * 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.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import okhttp3.FormBody;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.ResponseBody;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.alm.client.github.security.AccessToken;
+import org.sonarqube.ws.client.OkHttpClientBuilder;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.net.HttpURLConnection.HTTP_ACCEPTED;
+import static java.net.HttpURLConnection.HTTP_CREATED;
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+import static java.util.Optional.ofNullable;
+
+public abstract class GenericApplicationHttpClient implements ApplicationHttpClient {
+
+ private static final Logger LOG = LoggerFactory.getLogger(GenericApplicationHttpClient.class);
+ private static final Pattern NEXT_LINK_PATTERN = Pattern.compile("<([^<]+)>; rel=\"next\"");
+
+ private final DevopsPlatformHeaders devopsPlatformHeaders;
+ private final OkHttpClient client;
+
+ protected GenericApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
+ this.devopsPlatformHeaders = devopsPlatformHeaders;
+ client = new OkHttpClientBuilder()
+ .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
+ .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
+ .setFollowRedirects(false)
+ .build();
+ }
+
+ @Override
+ public GetResponse get(String appUrl, AccessToken token, String endPoint) throws IOException {
+ return get(appUrl, token, endPoint, true);
+ }
+
+ @Override
+ public GetResponse getSilent(String appUrl, AccessToken token, String endPoint) throws IOException {
+ return get(appUrl, token, endPoint, false);
+ }
+
+ private GetResponse get(String appUrl, AccessToken token, String endPoint, boolean withLog) throws IOException {
+ validateEndPoint(endPoint);
+ try (okhttp3.Response response = client.newCall(newGetRequest(appUrl, token, endPoint)).execute()) {
+ int responseCode = response.code();
+ RateLimit rateLimit = readRateLimit(response);
+ if (responseCode != HTTP_OK) {
+ String content = StringUtils.trimToNull(attemptReadContent(response));
+ if (withLog) {
+ LOG.warn("GET response did not have expected HTTP code (was {}): {}", responseCode, content);
+ }
+ return new GetResponseImpl(responseCode, content, null, rateLimit);
+ }
+ return new GetResponseImpl(responseCode, readContent(response.body()).orElse(null), readNextEndPoint(response), rateLimit);
+ }
+ }
+
+ private static void validateEndPoint(String endPoint) {
+ checkArgument(endPoint.startsWith("/") || endPoint.startsWith("http") || endPoint.isEmpty(),
+ "endpoint must start with '/' or 'http'");
+ }
+
+ private Request newGetRequest(String appUrl, AccessToken token, String endPoint) {
+ return newRequestBuilder(appUrl, token, endPoint).get().build();
+ }
+
+ @Override
+ public Response post(String appUrl, AccessToken token, String endPoint) throws IOException {
+ return doPost(appUrl, token, endPoint, new FormBody.Builder().build());
+ }
+
+ @Override
+ public Response post(String appUrl, AccessToken token, String endPoint, String json) throws IOException {
+ RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
+ return doPost(appUrl, token, endPoint, body);
+ }
+
+ @Override
+ public Response patch(String appUrl, AccessToken token, String endPoint, String json) throws IOException {
+ RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
+ return doPatch(appUrl, token, endPoint, body);
+ }
+
+ @Override
+ public Response delete(String appUrl, AccessToken token, String endPoint) throws IOException {
+ validateEndPoint(endPoint);
+
+ try (okhttp3.Response response = client.newCall(newDeleteRequest(appUrl, token, endPoint)).execute()) {
+ int responseCode = response.code();
+ RateLimit rateLimit = readRateLimit(response);
+ if (responseCode != HTTP_NO_CONTENT) {
+ String content = attemptReadContent(response);
+ LOG.warn("DELETE response did not have expected HTTP code (was {}): {}", responseCode, content);
+ return new ResponseImpl(responseCode, content, rateLimit);
+ }
+ return new ResponseImpl(responseCode, null, rateLimit);
+ }
+ }
+
+ private Request newDeleteRequest(String appUrl, AccessToken token, String endPoint) {
+ return newRequestBuilder(appUrl, token, endPoint).delete().build();
+ }
+
+ private Response doPost(String appUrl, @Nullable AccessToken token, String endPoint, RequestBody body) throws IOException {
+ validateEndPoint(endPoint);
+
+ try (okhttp3.Response response = client.newCall(newPostRequest(appUrl, token, endPoint, body)).execute()) {
+ int responseCode = response.code();
+ RateLimit rateLimit = readRateLimit(response);
+ if (responseCode == HTTP_OK || responseCode == HTTP_CREATED || responseCode == HTTP_ACCEPTED) {
+ return new ResponseImpl(responseCode, readContent(response.body()).orElse(null), rateLimit);
+ } else if (responseCode == HTTP_NO_CONTENT) {
+ return new ResponseImpl(responseCode, null, rateLimit);
+ }
+ String content = attemptReadContent(response);
+ LOG.warn("POST response did not have expected HTTP code (was {}): {}", responseCode, content);
+ return new ResponseImpl(responseCode, content, rateLimit);
+ }
+ }
+
+ private Response doPatch(String appUrl, AccessToken token, String endPoint, RequestBody body) throws IOException {
+ validateEndPoint(endPoint);
+
+ try (okhttp3.Response response = client.newCall(newPatchRequest(token, appUrl, endPoint, body)).execute()) {
+ int responseCode = response.code();
+ RateLimit rateLimit = readRateLimit(response);
+ if (responseCode == HTTP_OK) {
+ return new ResponseImpl(responseCode, readContent(response.body()).orElse(null), rateLimit);
+ } else if (responseCode == HTTP_NO_CONTENT) {
+ return new ResponseImpl(responseCode, null, rateLimit);
+ }
+ String content = attemptReadContent(response);
+ LOG.warn("PATCH response did not have expected HTTP code (was {}): {}", responseCode, content);
+ return new ResponseImpl(responseCode, content, rateLimit);
+ }
+ }
+
+ private Request newPostRequest(String appUrl, @Nullable AccessToken token, String endPoint, RequestBody body) {
+ return newRequestBuilder(appUrl, token, endPoint).post(body).build();
+ }
+
+ private Request newPatchRequest(AccessToken token, String appUrl, String endPoint, RequestBody body) {
+ return newRequestBuilder(appUrl, token, endPoint).patch(body).build();
+ }
+
+ 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.getValue());
+ devopsPlatformHeaders.getApiVersion().ifPresent(apiVersion ->
+ url.addHeader(devopsPlatformHeaders.getApiVersionHeader().orElseThrow(), apiVersion)
+ );
+ }
+ return url;
+ }
+
+ private static String toAbsoluteEndPoint(String host, String endPoint) {
+ if (endPoint.startsWith("http")) {
+ return endPoint;
+ }
+ try {
+ return new URL(host + endPoint).toExternalForm();
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException(String.format("%s is not a valid url", host + endPoint));
+ }
+ }
+
+ private static String attemptReadContent(okhttp3.Response response) {
+ try {
+ return readContent(response.body()).orElse(null);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static Optional<String> readContent(@Nullable ResponseBody body) throws IOException {
+ if (body == null) {
+ return empty();
+ }
+ try {
+ return of(body.string());
+ } finally {
+ body.close();
+ }
+ }
+
+ @CheckForNull
+ private static String readNextEndPoint(okhttp3.Response response) {
+ String links = Optional.ofNullable(response.headers().get("link")).orElse("");
+ Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(links);
+ if (!nextLinkMatcher.find()) {
+ return null;
+ }
+ String nextUrl = nextLinkMatcher.group(1);
+ if (response.request().url().toString().equals(nextUrl)) {
+ return null;
+ }
+ return nextUrl;
+ }
+
+ @CheckForNull
+ private RateLimit readRateLimit(okhttp3.Response response) {
+ Integer remaining = headerValueOrNull(response, devopsPlatformHeaders.getRateLimitRemainingHeader(), Integer::valueOf);
+ Integer limit = headerValueOrNull(response, devopsPlatformHeaders.getRateLimitLimitHeader(), Integer::valueOf);
+ Long reset = headerValueOrNull(response, devopsPlatformHeaders.getRateLimitResetHeader(), Long::valueOf);
+ if (remaining == null || limit == null || reset == null) {
+ return null;
+ }
+ return new RateLimit(remaining, limit, reset);
+ }
+
+ @CheckForNull
+ private static <T> T headerValueOrNull(okhttp3.Response response, String header, Function<String, T> mapper) {
+ return ofNullable(response.headers().get(header)).map(mapper::apply).orElse(null);
+ }
+
+ private static class ResponseImpl implements Response {
+ private final int code;
+ private final String content;
+
+ private final RateLimit rateLimit;
+
+ private ResponseImpl(int code, @Nullable String content, @Nullable RateLimit rateLimit) {
+ this.code = code;
+ this.content = content;
+ this.rateLimit = rateLimit;
+ }
+
+ @Override
+ public int getCode() {
+ return code;
+ }
+
+ @Override
+ public Optional<String> getContent() {
+ return ofNullable(content);
+ }
+
+ @Override
+ @CheckForNull
+ public RateLimit getRateLimit() {
+ return rateLimit;
+ }
+
+ }
+
+ private static final class GetResponseImpl extends ResponseImpl implements GetResponse {
+ private final String nextEndPoint;
+
+ private GetResponseImpl(int code, @Nullable String content, @Nullable String nextEndPoint, @Nullable RateLimit rateLimit) {
+ super(code, content, rateLimit);
+ this.nextEndPoint = nextEndPoint;
+ }
+
+ @Override
+ public Optional<String> getNextEndPoint() {
+ return ofNullable(nextEndPoint);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.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);
+}
--- /dev/null
+/*
+ * 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 com.google.common.annotations.VisibleForTesting;
+import org.kohsuke.github.GHRateLimit;
+import org.kohsuke.github.RateLimitChecker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.server.ServerSide;
+
+@ComputeEngineSide
+@ServerSide
+public class RatioBasedRateLimitChecker extends RateLimitChecker {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RatioBasedRateLimitChecker.class);
+
+ @VisibleForTesting
+ 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;
+
+ public boolean checkRateLimit(ApplicationHttpClient.RateLimit rateLimitRecord) throws InterruptedException {
+ int limit = rateLimitRecord.limit();
+ int apiCallsUsed = limit - rateLimitRecord.remaining();
+ double percentageOfCallsUsed = computePercentageOfCallsUsed(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());
+ return sleepUntilReset(rateLimit);
+ }
+ return false;
+ }
+
+ private static double computePercentageOfCallsUsed(int used, int limit) {
+ return (double) used * 100 / limit;
+ }
+}
+++ /dev/null
-/*
- * 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.github;
-
-import java.io.IOException;
-import java.util.Optional;
-import org.sonar.alm.client.github.security.AccessToken;
-import org.sonar.api.ce.ComputeEngineSide;
-import org.sonar.api.server.ServerSide;
-
-@ServerSide
-@ComputeEngineSide
-public interface ApplicationHttpClient {
- /**
- * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}.
- */
- GetResponse get(String appUrl, AccessToken token, String endPoint) throws IOException;
-
- /**
- * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}.
- * No log if there is an issue during the call.
- */
- GetResponse getSilent(String appUrl, AccessToken token, String endPoint) throws IOException;
-
- /**
- * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK} or
- * {@link java.net.HttpURLConnection#HTTP_CREATED CREATED}.
- */
- Response post(String appUrl, AccessToken token, String endPoint) throws IOException;
-
- /**
- * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK} or
- * {@link java.net.HttpURLConnection#HTTP_CREATED CREATED}.
- *
- * Content type will be application/json; charset=utf-8
- */
- Response post(String appUrl, AccessToken token, String endPoint, String json) throws IOException;
-
- /**
- * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}.
- *
- * Content type will be application/json; charset=utf-8
- */
- Response patch(String appUrl, AccessToken token, String endPoint, String json) throws IOException;
-
- /**
- * Content of the response is populated if response's HTTP code is {@link java.net.HttpURLConnection#HTTP_OK OK}.
- *
- * Content type will be application/json; charset=utf-8
- *
- */
- Response delete(String appUrl, AccessToken token, String endPoint) throws IOException;
-
- record RateLimit(int remaining, int limit, long reset) {
- }
- interface Response {
-
- /**
- * @return the HTTP code of the response.
- */
- int getCode();
-
- /**
- * @return the content of the response if the response had an HTTP code for which we expect a content for the current
- * HTTP method (see {@link #get(String, AccessToken, String)} and {@link #post(String, AccessToken, String)}).
- */
- Optional<String> getContent();
-
- RateLimit getRateLimit();
- }
-
- interface GetResponse extends Response {
- Optional<String> getNextEndPoint();
- }
-
-}
+++ /dev/null
-/*
- * 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.github;
-
-import java.util.Optional;
-
-public interface DevopsPlatformHeaders {
- Optional<String> getApiVersionHeader();
-
- Optional<String> getApiVersion();
-
- String getRateLimitRemainingHeader();
-
- String getRateLimitLimitHeader();
-
- String getRateLimitResetHeader();
-
- String getAuthorizationHeader();
-}
+++ /dev/null
-/*
- * 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.github;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Optional;
-import java.util.function.Function;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import okhttp3.FormBody;
-import okhttp3.MediaType;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-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;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static java.net.HttpURLConnection.HTTP_ACCEPTED;
-import static java.net.HttpURLConnection.HTTP_CREATED;
-import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.util.Optional.empty;
-import static java.util.Optional.of;
-import static java.util.Optional.ofNullable;
-
-public abstract class GenericApplicationHttpClient implements ApplicationHttpClient {
-
- private static final Logger LOG = LoggerFactory.getLogger(GenericApplicationHttpClient.class);
- private static final Pattern NEXT_LINK_PATTERN = Pattern.compile("<([^<]+)>; rel=\"next\"");
-
- private final DevopsPlatformHeaders devopsPlatformHeaders;
- private final OkHttpClient client;
-
- public GenericApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
- this.devopsPlatformHeaders = devopsPlatformHeaders;
- client = new OkHttpClientBuilder()
- .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
- .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
- .setFollowRedirects(false)
- .build();
- }
-
- @Override
- public GetResponse get(String appUrl, AccessToken token, String endPoint) throws IOException {
- return get(appUrl, token, endPoint, true);
- }
-
- @Override
- public GetResponse getSilent(String appUrl, AccessToken token, String endPoint) throws IOException {
- return get(appUrl, token, endPoint, false);
- }
-
- private GetResponse get(String appUrl, AccessToken token, String endPoint, boolean withLog) throws IOException {
- validateEndPoint(endPoint);
- try (okhttp3.Response response = client.newCall(newGetRequest(appUrl, token, endPoint)).execute()) {
- int responseCode = response.code();
- RateLimit rateLimit = readRateLimit(response);
- if (responseCode != HTTP_OK) {
- String content = StringUtils.trimToNull(attemptReadContent(response));
- if (withLog) {
- LOG.warn("GET response did not have expected HTTP code (was {}): {}", responseCode, content);
- }
- return new GetResponseImpl(responseCode, content, null, rateLimit);
- }
- return new GetResponseImpl(responseCode, readContent(response.body()).orElse(null), readNextEndPoint(response), rateLimit);
- }
- }
-
- private static void validateEndPoint(String endPoint) {
- checkArgument(endPoint.startsWith("/") || endPoint.startsWith("http") || endPoint.isEmpty(),
- "endpoint must start with '/' or 'http'");
- }
-
- private Request newGetRequest(String appUrl, AccessToken token, String endPoint) {
- return newRequestBuilder(appUrl, token, endPoint).get().build();
- }
-
- @Override
- public Response post(String appUrl, AccessToken token, String endPoint) throws IOException {
- return doPost(appUrl, token, endPoint, new FormBody.Builder().build());
- }
-
- @Override
- public Response post(String appUrl, AccessToken token, String endPoint, String json) throws IOException {
- RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
- return doPost(appUrl, token, endPoint, body);
- }
-
- @Override
- public Response patch(String appUrl, AccessToken token, String endPoint, String json) throws IOException {
- RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
- return doPatch(appUrl, token, endPoint, body);
- }
-
- @Override
- public Response delete(String appUrl, AccessToken token, String endPoint) throws IOException {
- validateEndPoint(endPoint);
-
- try (okhttp3.Response response = client.newCall(newDeleteRequest(appUrl, token, endPoint)).execute()) {
- int responseCode = response.code();
- RateLimit rateLimit = readRateLimit(response);
- if (responseCode != HTTP_NO_CONTENT) {
- String content = attemptReadContent(response);
- LOG.warn("DELETE response did not have expected HTTP code (was {}): {}", responseCode, content);
- return new ResponseImpl(responseCode, content, rateLimit);
- }
- return new ResponseImpl(responseCode, null, rateLimit);
- }
- }
-
- private Request newDeleteRequest(String appUrl, AccessToken token, String endPoint) {
- return newRequestBuilder(appUrl, token, endPoint).delete().build();
- }
-
- private Response doPost(String appUrl, @Nullable AccessToken token, String endPoint, RequestBody body) throws IOException {
- validateEndPoint(endPoint);
-
- try (okhttp3.Response response = client.newCall(newPostRequest(appUrl, token, endPoint, body)).execute()) {
- int responseCode = response.code();
- RateLimit rateLimit = readRateLimit(response);
- if (responseCode == HTTP_OK || responseCode == HTTP_CREATED || responseCode == HTTP_ACCEPTED) {
- return new ResponseImpl(responseCode, readContent(response.body()).orElse(null), rateLimit);
- } else if (responseCode == HTTP_NO_CONTENT) {
- return new ResponseImpl(responseCode, null, rateLimit);
- }
- String content = attemptReadContent(response);
- LOG.warn("POST response did not have expected HTTP code (was {}): {}", responseCode, content);
- return new ResponseImpl(responseCode, content, rateLimit);
- }
- }
-
- private Response doPatch(String appUrl, AccessToken token, String endPoint, RequestBody body) throws IOException {
- validateEndPoint(endPoint);
-
- try (okhttp3.Response response = client.newCall(newPatchRequest(token, appUrl, endPoint, body)).execute()) {
- int responseCode = response.code();
- RateLimit rateLimit = readRateLimit(response);
- if (responseCode == HTTP_OK) {
- return new ResponseImpl(responseCode, readContent(response.body()).orElse(null), rateLimit);
- } else if (responseCode == HTTP_NO_CONTENT) {
- return new ResponseImpl(responseCode, null, rateLimit);
- }
- String content = attemptReadContent(response);
- LOG.warn("PATCH response did not have expected HTTP code (was {}): {}", responseCode, content);
- return new ResponseImpl(responseCode, content, rateLimit);
- }
- }
-
- private Request newPostRequest(String appUrl, @Nullable AccessToken token, String endPoint, RequestBody body) {
- return newRequestBuilder(appUrl, token, endPoint).post(body).build();
- }
-
- private Request newPatchRequest(AccessToken token, String appUrl, String endPoint, RequestBody body) {
- return newRequestBuilder(appUrl, token, endPoint).patch(body).build();
- }
-
- 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);
- devopsPlatformHeaders.getApiVersion().ifPresent(apiVersion ->
- url.addHeader(devopsPlatformHeaders.getApiVersionHeader().orElseThrow(), apiVersion)
- );
- }
- return url;
- }
-
- private static String toAbsoluteEndPoint(String host, String endPoint) {
- if (endPoint.startsWith("http")) {
- return endPoint;
- }
- try {
- return new URL(host + endPoint).toExternalForm();
- } catch (MalformedURLException e) {
- throw new IllegalArgumentException(String.format("%s is not a valid url", host + endPoint));
- }
- }
-
- private static String attemptReadContent(okhttp3.Response response) {
- try {
- return readContent(response.body()).orElse(null);
- } catch (IOException e) {
- return null;
- }
- }
-
- private static Optional<String> readContent(@Nullable ResponseBody body) throws IOException {
- if (body == null) {
- return empty();
- }
- try {
- return of(body.string());
- } finally {
- body.close();
- }
- }
-
- @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;
- }
-
- Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(links);
- if (!nextLinkMatcher.find()) {
- return null;
- }
-
- return nextLinkMatcher.group(1);
- }
-
- @CheckForNull
- private RateLimit readRateLimit(okhttp3.Response response) {
- Integer remaining = headerValueOrNull(response, devopsPlatformHeaders.getRateLimitRemainingHeader(), Integer::valueOf);
- Integer limit = headerValueOrNull(response, devopsPlatformHeaders.getRateLimitLimitHeader(), Integer::valueOf);
- Long reset = headerValueOrNull(response, devopsPlatformHeaders.getRateLimitResetHeader(), Long::valueOf);
- if (remaining == null || limit == null || reset == null) {
- return null;
- }
- return new RateLimit(remaining, limit, reset);
- }
-
- @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);
- }
-
- private static class ResponseImpl implements Response {
- private final int code;
- private final String content;
-
- private final RateLimit rateLimit;
-
- private ResponseImpl(int code, @Nullable String content, @Nullable RateLimit rateLimit) {
- this.code = code;
- this.content = content;
- this.rateLimit = rateLimit;
- }
-
- @Override
- public int getCode() {
- return code;
- }
-
- @Override
- public Optional<String> getContent() {
- return ofNullable(content);
- }
-
- @Override
- @CheckForNull
- public RateLimit getRateLimit() {
- return rateLimit;
- }
-
- }
-
- private static final class GetResponseImpl extends ResponseImpl implements GetResponse {
- private final String nextEndPoint;
-
- private GetResponseImpl(int code, @Nullable String content, @Nullable String nextEndPoint, @Nullable RateLimit rateLimit) {
- super(code, content, rateLimit);
- this.nextEndPoint = nextEndPoint;
- }
-
- @Override
- public Optional<String> getNextEndPoint() {
- return ofNullable(nextEndPoint);
- }
- }
-}
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;
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;
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);
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");
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()) {
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);
}
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;
@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)
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(""));
}
// 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);
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(""));
}
}
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);
- }
- }
}
*/
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;
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;
*/
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;
- }
}
+++ /dev/null
-/*
- * 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.github;
-
-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;
-}
+++ /dev/null
-/*
- * 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.github;
-
-import com.google.common.annotations.VisibleForTesting;
-import org.kohsuke.github.GHRateLimit;
-import org.kohsuke.github.RateLimitChecker;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.api.ce.ComputeEngineSide;
-import org.sonar.api.server.ServerSide;
-
-@ComputeEngineSide
-@ServerSide
-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. "
- + "{} out of {} calls were used.";
-
- private static final int MAX_PERCENTAGE_OF_CALLS_FOR_PROVISIONING = 90;
-
- public boolean checkRateLimit(ApplicationHttpClient.RateLimit rateLimitRecord) throws InterruptedException {
- 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);
- 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());
- return sleepUntilReset(rateLimit);
- }
- return false;
- }
-
- private static double computePercentageOfCallsUsed(int used, int limit) {
- return (double) used * 100 / limit;
- }
-}
--- /dev/null
+/*
+ * 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 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;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.logging.log4j.util.Strings;
+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;
+
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+@ServerSide
+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();
+
+ protected static final String PRIVATE_TOKEN = "Private-Token";
+ protected final OkHttpClient client;
+
+ private final GitlabPaginatedHttpClient gitlabPaginatedHttpClient;
+
+ public GitlabApplicationClient(GitlabPaginatedHttpClient gitlabPaginatedHttpClient, TimeoutConfiguration timeoutConfiguration) {
+ this.gitlabPaginatedHttpClient = gitlabPaginatedHttpClient;
+ client = new OkHttpClientBuilder()
+ .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
+ .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
+ .setFollowRedirects(false)
+ .build();
+ }
+
+ public void checkReadPermission(@Nullable String gitlabUrl, @Nullable String personalAccessToken) {
+ checkProjectAccess(gitlabUrl, personalAccessToken, "Could not validate GitLab read permission. Got an unexpected answer.");
+ }
+
+ public void checkUrl(@Nullable String gitlabUrl) {
+ checkProjectAccess(gitlabUrl, null, "Could not validate GitLab url. Got an unexpected answer.");
+ }
+
+ private void checkProjectAccess(@Nullable String gitlabUrl, @Nullable String personalAccessToken, String errorMessage) {
+ String url = String.format("%s/projects", gitlabUrl);
+
+ LOG.debug(String.format("get projects : [%s]", url));
+ Request.Builder builder = new Request.Builder()
+ .url(url)
+ .get();
+
+ if (personalAccessToken != null) {
+ builder.addHeader(PRIVATE_TOKEN, personalAccessToken);
+ }
+
+ Request request = builder.build();
+
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response, errorMessage);
+ Project.parseJsonArray(response.body().string());
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException("Could not parse GitLab answer to verify read permission. Got a non-json payload as result.");
+ } catch (IOException e) {
+ logException(url, e);
+ throw new IllegalArgumentException(errorMessage);
+ }
+ }
+
+ private static void logException(String url, IOException e) {
+ LOG.info(String.format("Gitlab API call to [%s] failed with error message : [%s]", url, e.getMessage()), e);
+ }
+
+ public void checkToken(String gitlabUrl, String personalAccessToken) {
+ String url = String.format("%s/user", gitlabUrl);
+
+ LOG.debug(String.format("get current user : [%s]", url));
+ Request.Builder builder = new Request.Builder()
+ .addHeader(PRIVATE_TOKEN, personalAccessToken)
+ .url(url)
+ .get();
+
+ Request request = builder.build();
+
+ String errorMessage = "Could not validate GitLab token. Got an unexpected answer.";
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response, errorMessage);
+ GsonId.parseOne(response.body().string());
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException("Could not parse GitLab answer to verify token. Got a non-json payload as result.");
+ } catch (IOException e) {
+ logException(url, e);
+ throw new IllegalArgumentException(errorMessage);
+ }
+ }
+
+ public void checkWritePermission(String gitlabUrl, String personalAccessToken) {
+ String url = String.format("%s/markdown", gitlabUrl);
+
+ LOG.debug(String.format("verify write permission by formating some markdown : [%s]", url));
+ Request.Builder builder = new Request.Builder()
+ .url(url)
+ .addHeader(PRIVATE_TOKEN, personalAccessToken)
+ .addHeader("Content-Type", MediaTypes.JSON)
+ .post(RequestBody.create("{\"text\":\"validating write permission\"}".getBytes(UTF_8)));
+
+ Request request = builder.build();
+
+ String errorMessage = "Could not validate GitLab write permission. Got an unexpected answer.";
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response, errorMessage);
+ GsonMarkdown.parseOne(response.body().string());
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException("Could not parse GitLab answer to verify write permission. Got a non-json payload as result.");
+ } catch (IOException e) {
+ logException(url, e);
+ throw new IllegalArgumentException(errorMessage);
+ }
+
+ }
+
+ private static String urlEncode(String value) {
+ try {
+ return URLEncoder.encode(value, UTF_8.toString());
+ } catch (UnsupportedEncodingException ex) {
+ throw new IllegalStateException(ex.getCause());
+ }
+ }
+
+ protected static void checkResponseIsSuccessful(Response response) throws IOException {
+ checkResponseIsSuccessful(response, "GitLab Merge Request did not happen, please check your configuration");
+ }
+
+ protected static void checkResponseIsSuccessful(Response response, String errorMessage) throws IOException {
+ if (!response.isSuccessful()) {
+ String body = response.body().string();
+ LOG.error(String.format("Gitlab API call to [%s] failed with %s http code. gitlab response content : [%s]", response.request().url().toString(), response.code(), body));
+ if (isTokenRevoked(response, body)) {
+ throw new GitlabServerException(response.code(), "Your GitLab token was revoked");
+ } else if (isTokenExpired(response, body)) {
+ throw new GitlabServerException(response.code(), "Your GitLab token is expired");
+ } else if (isInsufficientScope(response, body)) {
+ throw new GitlabServerException(response.code(), "Your GitLab token has insufficient scope");
+ } else if (response.code() == HTTP_UNAUTHORIZED) {
+ throw new GitlabServerException(response.code(), "Invalid personal access token");
+ } else if (response.isRedirect()) {
+ throw new GitlabServerException(response.code(), "Request was redirected, please provide the correct URL");
+ } else {
+ throw new GitlabServerException(response.code(), errorMessage);
+ }
+ }
+ }
+
+ private static boolean isTokenRevoked(Response response, String body) {
+ if (response.code() == HTTP_UNAUTHORIZED) {
+ try {
+ Optional<GsonError> gitlabError = GsonError.parseOne(body);
+ return gitlabError.map(GsonError::getErrorDescription).map(description -> description.contains("Token was revoked")).orElse(false);
+ } catch (JsonParseException e) {
+ // nothing to do
+ }
+ }
+ return false;
+ }
+
+ private static boolean isTokenExpired(Response response, String body) {
+ if (response.code() == HTTP_UNAUTHORIZED) {
+ try {
+ Optional<GsonError> gitlabError = GsonError.parseOne(body);
+ return gitlabError.map(GsonError::getErrorDescription).map(description -> description.contains("Token is expired")).orElse(false);
+ } catch (JsonParseException e) {
+ // nothing to do
+ }
+ }
+ return false;
+ }
+
+ private static boolean isInsufficientScope(Response response, String body) {
+ if (response.code() == HTTP_FORBIDDEN) {
+ try {
+ Optional<GsonError> gitlabError = GsonError.parseOne(body);
+ return gitlabError.map(GsonError::getError).map("insufficient_scope"::equals).orElse(false);
+ } catch (JsonParseException e) {
+ // nothing to do
+ }
+ }
+ return false;
+ }
+
+ public Project getProject(String gitlabUrl, String pat, Long gitlabProjectId) {
+ String url = String.format("%s/projects/%s", gitlabUrl, gitlabProjectId);
+ LOG.debug(String.format("get project : [%s]", url));
+ Request request = new Request.Builder()
+ .addHeader(PRIVATE_TOKEN, pat)
+ .get()
+ .url(url)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response);
+ String body = response.body().string();
+ LOG.trace(String.format("loading project payload result : [%s]", body));
+ return new GsonBuilder().create().fromJson(body, Project.class);
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
+ } catch (IOException e) {
+ logException(url, e);
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ }
+
+ //
+ // This method is used to check if a user has REPORTER level access to the project, which is a requirement for PR decoration.
+ // As of June 9, 2021 there is no better way to do this check and still support GitLab 11.7.
+ //
+ public Optional<Project> getReporterLevelAccessProject(String gitlabUrl, String pat, Long gitlabProjectId) {
+ String url = String.format("%s/projects?min_access_level=20&id_after=%s&id_before=%s", gitlabUrl, gitlabProjectId - 1,
+ gitlabProjectId + 1);
+ LOG.debug(String.format("get project : [%s]", url));
+ Request request = new Request.Builder()
+ .addHeader(PRIVATE_TOKEN, pat)
+ .get()
+ .url(url)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response);
+ String body = response.body().string();
+ LOG.trace(String.format("loading project payload result : [%s]", body));
+
+ List<Project> projects = Project.parseJsonArray(body);
+ if (projects.isEmpty()) {
+ return Optional.empty();
+ } else {
+ return Optional.of(projects.get(0));
+ }
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
+ } catch (IOException e) {
+ logException(url, e);
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ }
+
+ public List<GitLabBranch> getBranches(String gitlabUrl, String pat, Long gitlabProjectId) {
+ String url = String.format("%s/projects/%s/repository/branches", gitlabUrl, gitlabProjectId);
+ LOG.debug(String.format("get branches : [%s]", url));
+ Request request = new Request.Builder()
+ .addHeader(PRIVATE_TOKEN, pat)
+ .get()
+ .url(url)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response);
+ String body = response.body().string();
+ LOG.trace(String.format("loading branches payload result : [%s]", body));
+ return Arrays.asList(new GsonBuilder().create().fromJson(body, GitLabBranch[].class));
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException("Could not parse GitLab answer to retrieve project branches. Got a non-json payload as result.");
+ } catch (IOException e) {
+ logException(url, e);
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ }
+
+ public ProjectList searchProjects(String gitlabUrl, String personalAccessToken, @Nullable String projectName,
+ @Nullable Integer pageNumber, @Nullable Integer pageSize) {
+ String url = String.format("%s/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%s%s%s",
+ gitlabUrl,
+ projectName == null ? "" : urlEncode(projectName),
+ pageNumber == null ? "" : String.format("&page=%d", pageNumber),
+ pageSize == null ? "" : String.format("&per_page=%d", pageSize)
+ );
+
+ LOG.debug(String.format("get projects : [%s]", url));
+ Request request = new Request.Builder()
+ .addHeader(PRIVATE_TOKEN, personalAccessToken)
+ .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");
+ } else {
+ try {
+ return Integer.parseInt(header);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Could not parse pagination number", e);
+ }
+ }
+ }
+
+ 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
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) {
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);
}
}
--- /dev/null
+/*
+ * 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";
+ }
+}
+++ /dev/null
-/*
- * 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 com.google.gson.GsonBuilder;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonSyntaxException;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import javax.annotation.Nullable;
-import okhttp3.Headers;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import org.apache.logging.log4j.util.Strings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.alm.client.TimeoutConfiguration;
-import org.sonar.api.server.ServerSide;
-import org.sonarqube.ws.MediaTypes;
-import org.sonarqube.ws.client.OkHttpClientBuilder;
-
-import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
-import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-@ServerSide
-public class GitlabHttpClient {
-
- 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) {
- client = new OkHttpClientBuilder()
- .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
- .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
- .setFollowRedirects(false)
- .build();
- }
-
- public void checkReadPermission(@Nullable String gitlabUrl, @Nullable String personalAccessToken) {
- checkProjectAccess(gitlabUrl, personalAccessToken, "Could not validate GitLab read permission. Got an unexpected answer.");
- }
-
- public void checkUrl(@Nullable String gitlabUrl) {
- checkProjectAccess(gitlabUrl, null, "Could not validate GitLab url. Got an unexpected answer.");
- }
-
- private void checkProjectAccess(@Nullable String gitlabUrl, @Nullable String personalAccessToken, String errorMessage) {
- String url = String.format("%s/projects", gitlabUrl);
-
- LOG.debug(String.format("get projects : [%s]", url));
- Request.Builder builder = new Request.Builder()
- .url(url)
- .get();
-
- if (personalAccessToken != null) {
- builder.addHeader(PRIVATE_TOKEN, personalAccessToken);
- }
-
- Request request = builder.build();
-
- try (Response response = client.newCall(request).execute()) {
- checkResponseIsSuccessful(response, errorMessage);
- Project.parseJsonArray(response.body().string());
- } catch (JsonSyntaxException e) {
- throw new IllegalArgumentException("Could not parse GitLab answer to verify read permission. Got a non-json payload as result.");
- } catch (IOException e) {
- logException(url, e);
- throw new IllegalArgumentException(errorMessage);
- }
- }
-
- private static void logException(String url, IOException e) {
- LOG.info(String.format("Gitlab API call to [%s] failed with error message : [%s]", url, e.getMessage()), e);
- }
-
- public void checkToken(String gitlabUrl, String personalAccessToken) {
- String url = String.format("%s/user", gitlabUrl);
-
- LOG.debug(String.format("get current user : [%s]", url));
- Request.Builder builder = new Request.Builder()
- .addHeader(PRIVATE_TOKEN, personalAccessToken)
- .url(url)
- .get();
-
- Request request = builder.build();
-
- String errorMessage = "Could not validate GitLab token. Got an unexpected answer.";
- try (Response response = client.newCall(request).execute()) {
- checkResponseIsSuccessful(response, errorMessage);
- GsonId.parseOne(response.body().string());
- } catch (JsonSyntaxException e) {
- throw new IllegalArgumentException("Could not parse GitLab answer to verify token. Got a non-json payload as result.");
- } catch (IOException e) {
- logException(url, e);
- throw new IllegalArgumentException(errorMessage);
- }
- }
-
- public void checkWritePermission(String gitlabUrl, String personalAccessToken) {
- String url = String.format("%s/markdown", gitlabUrl);
-
- LOG.debug(String.format("verify write permission by formating some markdown : [%s]", url));
- Request.Builder builder = new Request.Builder()
- .url(url)
- .addHeader(PRIVATE_TOKEN, personalAccessToken)
- .addHeader("Content-Type", MediaTypes.JSON)
- .post(RequestBody.create("{\"text\":\"validating write permission\"}".getBytes(UTF_8)));
-
- Request request = builder.build();
-
- String errorMessage = "Could not validate GitLab write permission. Got an unexpected answer.";
- try (Response response = client.newCall(request).execute()) {
- checkResponseIsSuccessful(response, errorMessage);
- GsonMarkdown.parseOne(response.body().string());
- } catch (JsonSyntaxException e) {
- throw new IllegalArgumentException("Could not parse GitLab answer to verify write permission. Got a non-json payload as result.");
- } catch (IOException e) {
- logException(url, e);
- throw new IllegalArgumentException(errorMessage);
- }
-
- }
-
- private static String urlEncode(String value) {
- try {
- return URLEncoder.encode(value, UTF_8.toString());
- } catch (UnsupportedEncodingException ex) {
- throw new IllegalStateException(ex.getCause());
- }
- }
-
- protected static void checkResponseIsSuccessful(Response response) throws IOException {
- checkResponseIsSuccessful(response, "GitLab Merge Request did not happen, please check your configuration");
- }
-
- protected static void checkResponseIsSuccessful(Response response, String errorMessage) throws IOException {
- if (!response.isSuccessful()) {
- String body = response.body().string();
- LOG.error(String.format("Gitlab API call to [%s] failed with %s http code. gitlab response content : [%s]", response.request().url().toString(), response.code(), body));
- if (isTokenRevoked(response, body)) {
- throw new GitlabServerException(response.code(), "Your GitLab token was revoked");
- } else if (isTokenExpired(response, body)) {
- throw new GitlabServerException(response.code(), "Your GitLab token is expired");
- } else if (isInsufficientScope(response, body)) {
- throw new GitlabServerException(response.code(), "Your GitLab token has insufficient scope");
- } else if (response.code() == HTTP_UNAUTHORIZED) {
- throw new GitlabServerException(response.code(), "Invalid personal access token");
- } else if (response.isRedirect()) {
- throw new GitlabServerException(response.code(), "Request was redirected, please provide the correct URL");
- } else {
- throw new GitlabServerException(response.code(), errorMessage);
- }
- }
- }
-
- private static boolean isTokenRevoked(Response response, String body) {
- if (response.code() == HTTP_UNAUTHORIZED) {
- try {
- Optional<GsonError> gitlabError = GsonError.parseOne(body);
- return gitlabError.map(GsonError::getErrorDescription).map(description -> description.contains("Token was revoked")).orElse(false);
- } catch (JsonParseException e) {
- // nothing to do
- }
- }
- return false;
- }
-
- private static boolean isTokenExpired(Response response, String body) {
- if (response.code() == HTTP_UNAUTHORIZED) {
- try {
- Optional<GsonError> gitlabError = GsonError.parseOne(body);
- return gitlabError.map(GsonError::getErrorDescription).map(description -> description.contains("Token is expired")).orElse(false);
- } catch (JsonParseException e) {
- // nothing to do
- }
- }
- return false;
- }
-
- private static boolean isInsufficientScope(Response response, String body) {
- if (response.code() == HTTP_FORBIDDEN) {
- try {
- Optional<GsonError> gitlabError = GsonError.parseOne(body);
- return gitlabError.map(GsonError::getError).map("insufficient_scope"::equals).orElse(false);
- } catch (JsonParseException e) {
- // nothing to do
- }
- }
- return false;
- }
-
- public Project getProject(String gitlabUrl, String pat, Long gitlabProjectId) {
- String url = String.format("%s/projects/%s", gitlabUrl, gitlabProjectId);
- LOG.debug(String.format("get project : [%s]", url));
- Request request = new Request.Builder()
- .addHeader(PRIVATE_TOKEN, pat)
- .get()
- .url(url)
- .build();
-
- try (Response response = client.newCall(request).execute()) {
- checkResponseIsSuccessful(response);
- String body = response.body().string();
- LOG.trace(String.format("loading project payload result : [%s]", body));
- return new GsonBuilder().create().fromJson(body, Project.class);
- } catch (JsonSyntaxException e) {
- throw new IllegalArgumentException("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
- } catch (IOException e) {
- logException(url, e);
- throw new IllegalStateException(e.getMessage(), e);
- }
- }
-
- //
- // This method is used to check if a user has REPORTER level access to the project, which is a requirement for PR decoration.
- // As of June 9, 2021 there is no better way to do this check and still support GitLab 11.7.
- //
- public Optional<Project> getReporterLevelAccessProject(String gitlabUrl, String pat, Long gitlabProjectId) {
- String url = String.format("%s/projects?min_access_level=20&id_after=%s&id_before=%s", gitlabUrl, gitlabProjectId - 1,
- gitlabProjectId + 1);
- LOG.debug(String.format("get project : [%s]", url));
- Request request = new Request.Builder()
- .addHeader(PRIVATE_TOKEN, pat)
- .get()
- .url(url)
- .build();
-
- try (Response response = client.newCall(request).execute()) {
- checkResponseIsSuccessful(response);
- String body = response.body().string();
- LOG.trace(String.format("loading project payload result : [%s]", body));
-
- List<Project> projects = Project.parseJsonArray(body);
- if (projects.isEmpty()) {
- return Optional.empty();
- } else {
- return Optional.of(projects.get(0));
- }
- } catch (JsonSyntaxException e) {
- throw new IllegalArgumentException("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
- } catch (IOException e) {
- logException(url, e);
- throw new IllegalStateException(e.getMessage(), e);
- }
- }
-
- public List<GitLabBranch> getBranches(String gitlabUrl, String pat, Long gitlabProjectId) {
- String url = String.format("%s/projects/%s/repository/branches", gitlabUrl, gitlabProjectId);
- LOG.debug(String.format("get branches : [%s]", url));
- Request request = new Request.Builder()
- .addHeader(PRIVATE_TOKEN, pat)
- .get()
- .url(url)
- .build();
-
- try (Response response = client.newCall(request).execute()) {
- checkResponseIsSuccessful(response);
- String body = response.body().string();
- LOG.trace(String.format("loading branches payload result : [%s]", body));
- return Arrays.asList(new GsonBuilder().create().fromJson(body, GitLabBranch[].class));
- } catch (JsonSyntaxException e) {
- throw new IllegalArgumentException("Could not parse GitLab answer to retrieve project branches. Got a non-json payload as result.");
- } catch (IOException e) {
- logException(url, e);
- throw new IllegalStateException(e.getMessage(), e);
- }
- }
-
- public ProjectList searchProjects(String gitlabUrl, String personalAccessToken, @Nullable String projectName,
- @Nullable Integer pageNumber, @Nullable Integer pageSize) {
- String url = String.format("%s/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%s%s%s",
- gitlabUrl,
- projectName == null ? "" : urlEncode(projectName),
- pageNumber == null ? "" : String.format("&page=%d", pageNumber),
- pageSize == null ? "" : String.format("&per_page=%d", pageSize)
- );
-
- LOG.debug(String.format("get projects : [%s]", url));
- Request request = new Request.Builder()
- .addHeader(PRIVATE_TOKEN, personalAccessToken)
- .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);
- }
- }
-
- /*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");
- } else {
- try {
- return Integer.parseInt(header);
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Could not parse pagination number", e);
- }
- }
- }
-
-}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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 com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.slf4j.event.Level;
+import org.sonar.alm.client.github.security.AccessToken;
+import org.sonar.api.testfixtures.log.LogTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+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.ApplicationHttpClient.GetResponse;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GenericPaginatedHttpClientImplTest {
+
+ private static final String APP_URL = "https://github.com/";
+
+ private static final String ENDPOINT = "/test-endpoint";
+
+ private static final Type STRING_LIST_TYPE = TypeToken.getParameterized(List.class, String.class).getType();
+
+ private Gson gson = new Gson();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Mock
+ private AccessToken accessToken;
+
+ @Mock
+ RatioBasedRateLimitChecker rateLimitChecker;
+
+ @Mock
+ ApplicationHttpClient appHttpClient;
+
+ @InjectMocks
+ 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 {
+
+ GetResponse response = mockResponseWithoutPagination("[\"result1\", \"result2\"]");
+ when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response);
+
+ List<String> results = underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE));
+
+ assertThat(results)
+ .containsExactly("result1", "result2");
+ }
+
+ @Test
+ public void get_whenEndpointAlreadyContainsPathParameter_shouldAddANewParameter() throws IOException {
+ ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
+
+ GetResponse response = mockResponseWithoutPagination("[\"result1\", \"result2\"]");
+ when(appHttpClient.get(eq(APP_URL), eq(accessToken), urlCaptor.capture())).thenReturn(response);
+
+ underTest.get(APP_URL, accessToken, ENDPOINT + "?alreadyExistingArg=2", result -> gson.fromJson(result, STRING_LIST_TYPE));
+
+ assertThat(urlCaptor.getValue()).isEqualTo(ENDPOINT + "?alreadyExistingArg=2&per_page=100");
+ }
+
+ private static GetResponse mockResponseWithoutPagination(String content) {
+ GetResponse response = mock(GetResponse.class);
+ when(response.getCode()).thenReturn(200);
+ when(response.getContent()).thenReturn(Optional.of(content));
+ return response;
+ }
+
+ @Test
+ public void get_whenPaginationAndRateLimiting_returnsResponseFromAllPages() throws IOException, InterruptedException {
+ GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
+ GetResponse response2 = mockResponseWithoutPagination("[\"result3\"]");
+ when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
+ when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
+
+ List<String> results = underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE));
+
+ assertThat(results)
+ .containsExactly("result1", "result2", "result3");
+
+ ArgumentCaptor<ApplicationHttpClient.RateLimit> rateLimitRecordCaptor = ArgumentCaptor.forClass(ApplicationHttpClient.RateLimit.class);
+ verify(rateLimitChecker).checkRateLimit(rateLimitRecordCaptor.capture());
+ ApplicationHttpClient.RateLimit rateLimitRecord = rateLimitRecordCaptor.getValue();
+ assertThat(rateLimitRecord.limit()).isEqualTo(10);
+ assertThat(rateLimitRecord.remaining()).isEqualTo(1);
+ assertThat(rateLimitRecord.reset()).isZero();
+ }
+
+ private static GetResponse mockResponseWithPaginationAndRateLimit(String content, String nextEndpoint) {
+ GetResponse response = mockResponseWithoutPagination(content);
+ when(response.getCode()).thenReturn(200);
+ when(response.getNextEndPoint()).thenReturn(Optional.of(nextEndpoint));
+ when(response.getRateLimit()).thenReturn(new ApplicationHttpClient.RateLimit(1, 10, 0L));
+ return response;
+ }
+
+ @Test
+ public void get_whenGitHubReturnsNonSuccessCode_shouldThrow() throws IOException {
+ GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
+ GetResponse response2 = mockFailedResponse("failed");
+ when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
+ when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
+
+ assertThatIllegalStateException()
+ .isThrownBy(() -> underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE)))
+ .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) {
+ GetResponse response = mock(GetResponse.class);
+ when(response.getCode()).thenReturn(400);
+ when(response.getContent()).thenReturn(Optional.of(content));
+ return response;
+ }
+
+ @Test
+ public void getRepositoryTeams_whenRateLimitCheckerThrowsInterruptedException_shouldSucceed() throws IOException, InterruptedException {
+ GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
+ GetResponse response2 = mockResponseWithoutPagination("[\"result3\"]");
+ when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
+ when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
+ doThrow(new InterruptedException("interrupted")).when(rateLimitChecker).checkRateLimit(any(ApplicationHttpClient.RateLimit.class));
+
+ assertThatNoException()
+ .isThrownBy(() -> underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE)));
+
+ assertThat(logTester.logs()).hasSize(1);
+ 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.");
+ }
+}
--- /dev/null
+/*
+ * 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 com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTester;
+
+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.RatioBasedRateLimitChecker.RATE_RATIO_EXCEEDED_MESSAGE;
+
+@RunWith(DataProviderRunner.class)
+public class RatioBasedRateLimitCheckerTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+ private static final long MILLIS_BEFORE_RESET = 100L;
+ RatioBasedRateLimitChecker ratioBasedRateLimitChecker = new RatioBasedRateLimitChecker();
+
+ @DataProvider
+ public static Object[][] rates() {
+ return new Object[][] {
+ {10000, 100000, false},
+ {10000, 10000, false},
+ {10000, 9999, false},
+ {10000, 9900, false},
+ {10000, 1001, false},
+ {10000, 1000, true},
+ {10000, 500, true},
+ {10000, 0, true},
+ };
+ }
+
+ @Test
+ @UseDataProvider("rates")
+ public void checkRateLimit(int limit, int remaining, boolean rateLimitShouldBeExceeded) throws InterruptedException {
+ ApplicationHttpClient.RateLimit record = mock();
+ when(record.limit()).thenReturn(limit);
+ when(record.remaining()).thenReturn(remaining);
+ when(record.reset()).thenReturn(System.currentTimeMillis() / 1000 + 1);
+
+ long start = System.currentTimeMillis();
+ boolean result = ratioBasedRateLimitChecker.checkRateLimit(record);
+ long stop = System.currentTimeMillis();
+ long totalTime = stop - start;
+
+ if (rateLimitShouldBeExceeded) {
+ assertThat(result).isTrue();
+ assertThat(stop).isGreaterThanOrEqualTo(record.reset());
+ assertThat(logTester.logs(Level.WARN)).contains(
+ format(RATE_RATIO_EXCEEDED_MESSAGE.replaceAll("\\{\\}", "%s"), limit - remaining, limit));
+ } else {
+ assertThat(result).isFalse();
+ assertThat(totalTime).isLessThan(MILLIS_BEFORE_RESET);
+ assertThat(logTester.logs(Level.WARN)).isEmpty();
+ }
+ }
+}
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;
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 {
logTester.clear();
}
- private class TestApplicationHttpClient extends GenericApplicationHttpClient {
+ private static class TestApplicationHttpClient extends GenericApplicationHttpClient {
public TestApplicationHttpClient(DevopsPlatformHeaders devopsPlatformHeaders, TimeoutConfiguration timeoutConfiguration) {
super(devopsPlatformHeaders, timeoutConfiguration);
}
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);
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\""},
};
}
@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();
}
- 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();
@Test
public void delete_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
- testRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint));
+ testRateLimitHeader(() -> underTest.delete(appUrl, accessToken, randomEndPoint), false);
}
@Test
public void patch_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
- testRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"));
+ testRateLimitHeader(() -> underTest.patch(appUrl, accessToken, randomEndPoint, "body"), false);
}
@Test
@Test
public void post_whenRateLimitHeadersArePresent_returnsRateLimit() throws Exception {
- testRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint));
+ testRateLimitHeader(() -> underTest.post(appUrl, accessToken, randomEndPoint), false);
}
@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;
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 {
@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;
@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();
}
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)
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)
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)
+ " }\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)
+ " }\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();
}
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\": {" +
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();
@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");
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);
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))
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))
+ " \"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);
+ " ]\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);
}
@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
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))
+ " \"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);
+ " ]\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);
+ " ]\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() {
@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");
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");
+ " }"
+ "}";
- 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() {
@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();
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
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
}
@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() {
}
@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 {
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() {
" \"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);
}
+++ /dev/null
-/*
- * 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.github;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import java.io.IOException;
-import java.lang.reflect.Type;
-import java.util.List;
-import java.util.Optional;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.slf4j.event.Level;
-import org.sonar.alm.client.github.security.AccessToken;
-import org.sonar.api.testfixtures.log.LogTester;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
-import static org.assertj.core.api.Assertions.assertThatNoException;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-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;
-
-@RunWith(MockitoJUnitRunner.class)
-public class GithubPaginatedHttpClientImplTest {
-
- private static final String APP_URL = "https://github.com/";
-
- private static final String ENDPOINT = "/test-endpoint";
-
- private static final Type STRING_LIST_TYPE = TypeToken.getParameterized(List.class, String.class).getType();
-
- private Gson gson = new Gson();
-
- @Rule
- public LogTester logTester = new LogTester();
-
- @Mock
- private AccessToken accessToken;
-
- @Mock
- RatioBasedRateLimitChecker rateLimitChecker;
-
- @Mock
- ApplicationHttpClient appHttpClient;
-
- @InjectMocks
- private GithubPaginatedHttpClient underTest;
-
- @Test
- public void get_whenNoPagination_ReturnsCorrectResponse() throws IOException {
-
- GetResponse response = mockResponseWithoutPagination("[\"result1\", \"result2\"]");
- when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response);
-
- List<String> results = underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE));
-
- assertThat(results)
- .containsExactly("result1", "result2");
- }
-
- @Test
- public void get_whenEndpointAlreadyContainsPathParameter_shouldAddANewParameter() throws IOException {
- ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
-
- GetResponse response = mockResponseWithoutPagination("[\"result1\", \"result2\"]");
- when(appHttpClient.get(eq(APP_URL), eq(accessToken), urlCaptor.capture())).thenReturn(response);
-
- underTest.get(APP_URL, accessToken, ENDPOINT + "?alreadyExistingArg=2", result -> gson.fromJson(result, STRING_LIST_TYPE));
-
- assertThat(urlCaptor.getValue()).isEqualTo(ENDPOINT + "?alreadyExistingArg=2&per_page=100");
- }
-
- private static GetResponse mockResponseWithoutPagination(String content) {
- GetResponse response = mock(GetResponse.class);
- when(response.getCode()).thenReturn(200);
- when(response.getContent()).thenReturn(Optional.of(content));
- return response;
- }
-
- @Test
- public void get_whenPaginationAndRateLimiting_returnsResponseFromAllPages() throws IOException, InterruptedException {
- GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
- GetResponse response2 = mockResponseWithoutPagination("[\"result3\"]");
- when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
- when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
-
- List<String> results = underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE));
-
- assertThat(results)
- .containsExactly("result1", "result2", "result3");
-
- ArgumentCaptor<ApplicationHttpClient.RateLimit> rateLimitRecordCaptor = ArgumentCaptor.forClass(ApplicationHttpClient.RateLimit.class);
- verify(rateLimitChecker).checkRateLimit(rateLimitRecordCaptor.capture());
- ApplicationHttpClient.RateLimit rateLimitRecord = rateLimitRecordCaptor.getValue();
- assertThat(rateLimitRecord.limit()).isEqualTo(10);
- assertThat(rateLimitRecord.remaining()).isEqualTo(1);
- assertThat(rateLimitRecord.reset()).isZero();
- }
-
- private static GetResponse mockResponseWithPaginationAndRateLimit(String content, String nextEndpoint) {
- GetResponse response = mockResponseWithoutPagination(content);
- when(response.getCode()).thenReturn(200);
- when(response.getNextEndPoint()).thenReturn(Optional.of(nextEndpoint));
- when(response.getRateLimit()).thenReturn(new ApplicationHttpClient.RateLimit(1, 10, 0L));
- return response;
- }
-
- @Test
- public void get_whenGitHubReturnsNonSuccessCode_shouldThrow() throws IOException {
- GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
- GetResponse response2 = mockFailedResponse("failed");
- when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
- when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
-
- 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.");
- }
-
- private static GetResponse mockFailedResponse(String content) {
- GetResponse response = mock(GetResponse.class);
- when(response.getCode()).thenReturn(400);
- when(response.getContent()).thenReturn(Optional.of(content));
- return response;
- }
-
- @Test
- public void getRepositoryTeams_whenRateLimitCheckerThrowsInterruptedException_shouldSucceed() throws IOException, InterruptedException {
- GetResponse response1 = mockResponseWithPaginationAndRateLimit("[\"result1\", \"result2\"]", "/next-endpoint");
- GetResponse response2 = mockResponseWithoutPagination("[\"result3\"]");
- when(appHttpClient.get(APP_URL, accessToken, ENDPOINT + "?per_page=100")).thenReturn(response1);
- when(appHttpClient.get(APP_URL, accessToken, "/next-endpoint")).thenReturn(response2);
- doThrow(new InterruptedException("interrupted")).when(rateLimitChecker).checkRateLimit(any(ApplicationHttpClient.RateLimit.class));
-
- assertThatNoException()
- .isThrownBy(() -> underTest.get(APP_URL, accessToken, ENDPOINT, result -> gson.fromJson(result, STRING_LIST_TYPE)));
-
- assertThat(logTester.logs()).hasSize(1);
- assertThat(logTester.logs(Level.WARN))
- .containsExactly("Thread interrupted: interrupted");
- }
-}
+++ /dev/null
-/*
- * 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.github;
-
-import com.tngtech.java.junit.dataprovider.DataProvider;
-import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.slf4j.event.Level;
-import org.sonar.api.testfixtures.log.LogTester;
-
-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;
-
-@RunWith(DataProviderRunner.class)
-public class RatioBasedRateLimitCheckerTest {
-
- @Rule
- public LogTester logTester = new LogTester();
- private static final long MILLIS_BEFORE_RESET = 100L;
- RatioBasedRateLimitChecker ratioBasedRateLimitChecker = new RatioBasedRateLimitChecker();
-
- @DataProvider
- public static Object[][] rates() {
- return new Object[][] {
- {10000, 100000, false},
- {10000, 10000, false},
- {10000, 9999, false},
- {10000, 9900, false},
- {10000, 1001, false},
- {10000, 1000, true},
- {10000, 500, true},
- {10000, 0, true},
- };
- }
-
- @Test
- @UseDataProvider("rates")
- public void checkRateLimit(int limit, int remaining, boolean rateLimitShouldBeExceeded) throws InterruptedException {
- ApplicationHttpClient.RateLimit record = mock();
- when(record.limit()).thenReturn(limit);
- when(record.remaining()).thenReturn(remaining);
- when(record.reset()).thenReturn(System.currentTimeMillis() / 1000 + 1);
-
- long start = System.currentTimeMillis();
- boolean result = ratioBasedRateLimitChecker.checkRateLimit(record);
- long stop = System.currentTimeMillis();
- long totalTime = stop - start;
-
- if (rateLimitShouldBeExceeded) {
- assertThat(result).isTrue();
- assertThat(stop).isGreaterThanOrEqualTo(record.reset());
- assertThat(logTester.logs(Level.WARN)).contains(
- format(RATE_RATIO_EXCEEDED_MESSAGE.replaceAll("\\{\\}", "%s"), limit - remaining, limit));
- } else {
- assertThat(result).isFalse();
- assertThat(totalTime).isLessThan(MILLIS_BEFORE_RESET);
- assertThat(logTester.logs(Level.WARN)).isEmpty();
- }
- }
-}
--- /dev/null
+/*
+ * 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.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 GitlabApplicationClientTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+
+ private GitlabPaginatedHttpClient gitlabPaginatedHttpClient = mock();
+
+ private final MockWebServer server = new MockWebServer();
+ private GitlabApplicationClient underTest;
+ private String gitlabUrl;
+
+ @Before
+ public void prepare() throws IOException {
+ server.start();
+ String urlWithEndingSlash = server.url("").toString();
+ gitlabUrl = urlWithEndingSlash.substring(0, urlWithEndingSlash.length() - 1);
+
+ TimeoutConfiguration timeoutConfiguration = new ConstantTimeoutConfiguration(10_000);
+ underTest = new GitlabApplicationClient(gitlabPaginatedHttpClient, timeoutConfiguration);
+ }
+
+ @After
+ public void stopServer() throws IOException {
+ server.shutdown();
+ }
+
+ @Test
+ public void should_throw_IllegalArgumentException_when_token_is_revoked() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(401)
+ .setBody("{\"error\":\"invalid_token\",\"error_description\":\"Token was revoked. You have to re-authorize from the user.\"}");
+ server.enqueue(response);
+
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Your GitLab token was revoked");
+ }
+
+ @Test
+ public void should_throw_IllegalArgumentException_when_token_insufficient_scope() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(403)
+ .setBody("{\"error\":\"insufficient_scope\"," +
+ "\"error_description\":\"The request requires higher privileges than provided by the access token.\"," +
+ "\"scope\":\"api read_api\"}");
+ server.enqueue(response);
+
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Your GitLab token has insufficient scope");
+ }
+
+ @Test
+ public void should_throw_IllegalArgumentException_when_invalide_json_in_401_response() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(401)
+ .setBody("error in pat");
+ server.enqueue(response);
+
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid personal access token");
+ }
+
+ @Test
+ public void should_throw_IllegalArgumentException_when_redirected() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(308);
+ server.enqueue(response);
+
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Request was redirected, please provide the correct URL");
+ }
+
+ @Test
+ public void get_project() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(200)
+ .setBody("{\n"
+ + " \"id\": 12345,\n"
+ + " \"name\": \"SonarQube example 1\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
+ + " \"path\": \"sonarqube-example-1\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
+ + " }");
+ server.enqueue(response);
+
+ assertThat(underTest.getProject(gitlabUrl, "pat", 12345L))
+ .extracting(Project::getId, Project::getName)
+ .containsExactly(12345L, "SonarQube example 1");
+ }
+
+ @Test
+ public void get_project_fail_if_non_json_payload() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(200)
+ .setBody("non json payload");
+ server.enqueue(response);
+
+ assertThatThrownBy(() -> underTest.getProject(gitlabUrl, "pat", 12345L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
+ }
+
+ @Test
+ public void get_branches() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[{\n"
+ + " \"name\": \"main\",\n"
+ + " \"default\": true\n"
+ + "},{\n"
+ + " \"name\": \"other\",\n"
+ + " \"default\": false\n"
+ + "}]");
+ server.enqueue(response);
+
+ assertThat(underTest.getBranches(gitlabUrl, "pat", 12345L))
+ .extracting(GitLabBranch::getName, GitLabBranch::isDefault)
+ .containsExactly(
+ tuple("main", true),
+ tuple("other", false)
+ );
+ }
+
+ @Test
+ public void get_branches_fail_if_non_json_payload() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(200)
+ .setBody("non json payload");
+ server.enqueue(response);
+
+ String instanceUrl = gitlabUrl;
+ assertThatThrownBy(() -> underTest.getBranches(instanceUrl, "pat", 12345L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not parse GitLab answer to retrieve project branches. Got a non-json payload as result.");
+ }
+
+ @Test
+ public void get_branches_fail_if_exception() throws IOException {
+ server.shutdown();
+
+ String instanceUrl = gitlabUrl;
+ assertThatThrownBy(() -> underTest.getBranches(instanceUrl, "pat", 12345L))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Failed to connect to");
+ }
+
+ @Test
+ public void search_projects() throws InterruptedException {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[\n"
+ + " {\n"
+ + " \"id\": 1,\n"
+ + " \"name\": \"SonarQube example 1\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
+ + " \"path\": \"sonarqube-example-1\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"id\": 2,\n"
+ + " \"name\": \"SonarQube example 2\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 2\",\n"
+ + " \"path\": \"sonarqube-example-2\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-2\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"id\": 3,\n"
+ + " \"name\": \"SonarQube example 3\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 3\",\n"
+ + " \"path\": \"sonarqube-example-3\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-3\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-3\"\n"
+ + " }\n"
+ + "]");
+ projects.addHeader("X-Page", 1);
+ projects.addHeader("X-Per-Page", 10);
+ projects.addHeader("X-Total", 3);
+ server.enqueue(projects);
+
+ ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
+
+ assertThat(projectList.getPageNumber()).isOne();
+ assertThat(projectList.getPageSize()).isEqualTo(10);
+ assertThat(projectList.getTotal()).isEqualTo(3);
+
+ assertThat(projectList.getProjects()).hasSize(3);
+ assertThat(projectList.getProjects()).extracting(
+ Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
+ tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
+ "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"),
+ tuple(2L, "SonarQube example 2", "SonarSource / SonarQube / SonarQube example 2", "sonarqube-example-2", "sonarsource/sonarqube/sonarqube-example-2",
+ "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2"),
+ tuple(3L, "SonarQube example 3", "SonarSource / SonarQube / SonarQube example 3", "sonarqube-example-3", "sonarsource/sonarqube/sonarqube-example-3",
+ "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-3"));
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+ assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void search_projects_dont_fail_if_no_x_total() throws InterruptedException {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[\n"
+ + " {\n"
+ + " \"id\": 1,\n"
+ + " \"name\": \"SonarQube example 1\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
+ + " \"path\": \"sonarqube-example-1\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
+ + " }"
+ + "]");
+ projects.addHeader("X-Page", 1);
+ projects.addHeader("X-Per-Page", 10);
+ server.enqueue(projects);
+
+ ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
+
+ assertThat(projectList.getPageNumber()).isOne();
+ assertThat(projectList.getPageSize()).isEqualTo(10);
+ assertThat(projectList.getTotal()).isNull();
+
+ assertThat(projectList.getProjects()).hasSize(1);
+ assertThat(projectList.getProjects()).extracting(
+ Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
+ tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
+ "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"));
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+ assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void search_projects_with_case_insensitive_pagination_headers() throws InterruptedException {
+ MockResponse projects1 = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[\n"
+ + " {\n"
+ + " \"id\": 1,\n"
+ + " \"name\": \"SonarQube example 1\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
+ + " \"path\": \"sonarqube-example-1\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
+ + " }"
+ + "]");
+ projects1.addHeader("x-page", 1);
+ projects1.addHeader("x-Per-page", 1);
+ projects1.addHeader("X-Total", 2);
+ server.enqueue(projects1);
+
+ ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
+
+ assertThat(projectList.getPageNumber()).isOne();
+ assertThat(projectList.getPageSize()).isOne();
+ assertThat(projectList.getTotal()).isEqualTo(2);
+
+ assertThat(projectList.getProjects()).hasSize(1);
+ assertThat(projectList.getProjects()).extracting(
+ Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
+ tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
+ "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"));
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+ assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void search_projects_projectName_param_should_be_encoded() throws InterruptedException {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[]");
+ projects.addHeader("X-Page", 1);
+ projects.addHeader("X-Per-Page", 10);
+ projects.addHeader("X-Total", 0);
+ server.enqueue(projects);
+
+ ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "&page=<script>alert('nasty')</script>", 1, 10);
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+ assertThat(projectList.getProjects()).isEmpty();
+ assertThat(gitlabUrlCall).isEqualTo(
+ server.url("")
+ + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%26page%3D%3Cscript%3Ealert%28%27nasty%27%29%3C%2Fscript%3E&page=1&per_page=10");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void search_projects_projectName_param_null_should_pass_empty_string() throws InterruptedException {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[]");
+ projects.addHeader("X-Page", 1);
+ projects.addHeader("X-Per-Page", 10);
+ projects.addHeader("X-Total", 0);
+ server.enqueue(projects);
+
+ ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", null, 1, 10);
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+ assertThat(projectList.getProjects()).isEmpty();
+ assertThat(gitlabUrlCall).isEqualTo(
+ server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=&page=1&per_page=10");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void get_project_details() throws InterruptedException {
+ MockResponse projectResponse = new MockResponse()
+ .setResponseCode(200)
+ .setBody("{"
+ + " \"id\": 1234,"
+ + " \"name\": \"SonarQube example 2\","
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 2\","
+ + " \"path\": \"sonarqube-example-2\","
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-2\","
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2\""
+ + "}");
+
+ server.enqueue(projectResponse);
+
+ Project project = underTest.getProject(gitlabUrl, "pat", 1234L);
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+
+ assertThat(project).isNotNull();
+
+ assertThat(gitlabUrlCall).isEqualTo(
+ server.url("") + "projects/1234");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void get_reporter_level_access_project() throws InterruptedException {
+ MockResponse projectResponse = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[{"
+ + " \"id\": 1234,"
+ + " \"name\": \"SonarQube example 2\","
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 2\","
+ + " \"path\": \"sonarqube-example-2\","
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-2\","
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2\""
+ + "}]");
+
+ server.enqueue(projectResponse);
+
+ Optional<Project> project = underTest.getReporterLevelAccessProject(gitlabUrl, "pat", 1234L);
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+
+ assertThat(project).isNotNull();
+
+ assertThat(gitlabUrlCall).isEqualTo(
+ server.url("") + "projects?min_access_level=20&id_after=1233&id_before=1235");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void search_projects_fail_if_could_not_parse_pagination_number() {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[ ]");
+ projects.addHeader("X-Page", "bad-page-number");
+ projects.addHeader("X-Per-Page", "bad-per-page-number");
+ projects.addHeader("X-Total", "bad-total-number");
+ server.enqueue(projects);
+
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not parse pagination number");
+ }
+
+ @Test
+ public void search_projects_fail_if_pagination_data_not_returned() {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[ ]");
+ server.enqueue(projects);
+
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Pagination data from GitLab response is missing");
+ }
+
+ @Test
+ public void throws_ISE_when_get_projects_not_http_200() {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(500)
+ .setBody("test");
+ server.enqueue(projects);
+
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not get projects from GitLab instance");
+ }
+
+ @Test
+ public void fail_check_read_permission_with_unexpected_io_exception_with_detailed_log() throws IOException {
+ server.shutdown();
+
+ assertThatThrownBy(() -> underTest.checkReadPermission(gitlabUrl, "token"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not validate GitLab read permission. Got an unexpected answer.");
+ assertThat(logTester.logs(Level.INFO).get(0))
+ .contains("Gitlab API call to [" + server.url("/projects") + "] " +
+ "failed with error message : [Failed to connect to " + server.getHostName());
+ }
+
+ @Test
+ public void fail_check_token_with_unexpected_io_exception_with_detailed_log() throws IOException {
+ server.shutdown();
+
+ assertThatThrownBy(() -> underTest.checkToken(gitlabUrl, "token"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not validate GitLab token. Got an unexpected answer.");
+ assertThat(logTester.logs(Level.INFO).get(0))
+ .contains("Gitlab API call to [" + server.url("user") + "] " +
+ "failed with error message : [Failed to connect to " + server.getHostName());
+ }
+
+ @Test
+ public void fail_check_write_permission_with_unexpected_io_exception_with_detailed_log() throws IOException {
+ server.shutdown();
+
+ assertThatThrownBy(() -> underTest.checkWritePermission(gitlabUrl, "token"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not validate GitLab write permission. Got an unexpected answer.");
+ assertThat(logTester.logs(Level.INFO).get(0))
+ .contains("Gitlab API call to [" + server.url("/markdown") + "] " +
+ "failed with error message : [Failed to connect to " + server.getHostName());
+ }
+
+ @Test
+ public void fail_get_project_with_unexpected_io_exception_with_detailed_log() throws IOException {
+ server.shutdown();
+
+ assertThatThrownBy(() -> underTest.getProject(gitlabUrl, "token", 0L))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Failed to connect to");
+ assertThat(logTester.logs(Level.INFO).get(0))
+ .contains("Gitlab API call to [" + server.url("/projects/0") + "] " +
+ "failed with error message : [Failed to connect to " + server.getHostName());
+ }
+
+ @Test
+ public void fail_get_branches_with_unexpected_io_exception_with_detailed_log() throws IOException {
+ server.shutdown();
+
+ assertThatThrownBy(() -> underTest.getBranches(gitlabUrl, "token", 0L))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Failed to connect to " + server.getHostName());
+ assertThat(logTester.logs(Level.INFO).get(0))
+ .contains("Gitlab API call to [" + server.url("/projects/0/repository/branches") + "] " +
+ "failed with error message : [Failed to connect to " + server.getHostName());
+ }
+
+ @Test
+ public void fail_search_projects_with_unexpected_io_exception_with_detailed_log() throws IOException {
+ server.shutdown();
+
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "token", null, 1, 1))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Failed to connect to");
+ assertThat(logTester.logs(Level.INFO).get(0))
+ .contains(
+ "Gitlab API call to [" + server.url("/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=&page=1&per_page=1")
+ + "] " +
+ "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);
+ }
+
+}
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;
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);
+++ /dev/null
-/*
- * 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.io.IOException;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import okhttp3.mockwebserver.MockResponse;
-import okhttp3.mockwebserver.MockWebServer;
-import okhttp3.mockwebserver.RecordedRequest;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-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 static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.Assertions.tuple;
-
-public class GitlabHttpClientTest {
-
- @Rule
- public LogTester logTester = new LogTester();
-
- private final MockWebServer server = new MockWebServer();
- private GitlabHttpClient underTest;
- private String gitlabUrl;
-
- @Before
- public void prepare() throws IOException {
- server.start();
- String urlWithEndingSlash = server.url("").toString();
- gitlabUrl = urlWithEndingSlash.substring(0, urlWithEndingSlash.length() - 1);
-
- TimeoutConfiguration timeoutConfiguration = new ConstantTimeoutConfiguration(10_000);
- underTest = new GitlabHttpClient(timeoutConfiguration);
- }
-
- @After
- public void stopServer() throws IOException {
- server.shutdown();
- }
-
- @Test
- public void should_throw_IllegalArgumentException_when_token_is_revoked() {
- MockResponse response = new MockResponse()
- .setResponseCode(401)
- .setBody("{\"error\":\"invalid_token\",\"error_description\":\"Token was revoked. You have to re-authorize from the user.\"}");
- server.enqueue(response);
-
- assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Your GitLab token was revoked");
- }
-
- @Test
- public void should_throw_IllegalArgumentException_when_token_insufficient_scope() {
- MockResponse response = new MockResponse()
- .setResponseCode(403)
- .setBody("{\"error\":\"insufficient_scope\"," +
- "\"error_description\":\"The request requires higher privileges than provided by the access token.\"," +
- "\"scope\":\"api read_api\"}");
- server.enqueue(response);
-
- assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Your GitLab token has insufficient scope");
- }
-
- @Test
- public void should_throw_IllegalArgumentException_when_invalide_json_in_401_response() {
- MockResponse response = new MockResponse()
- .setResponseCode(401)
- .setBody("error in pat");
- server.enqueue(response);
-
- assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Invalid personal access token");
- }
-
- @Test
- public void should_throw_IllegalArgumentException_when_redirected() {
- MockResponse response = new MockResponse()
- .setResponseCode(308);
- server.enqueue(response);
-
- assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Request was redirected, please provide the correct URL");
- }
-
- @Test
- public void get_project() {
- MockResponse response = new MockResponse()
- .setResponseCode(200)
- .setBody("{\n"
- + " \"id\": 12345,\n"
- + " \"name\": \"SonarQube example 1\",\n"
- + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
- + " \"path\": \"sonarqube-example-1\",\n"
- + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
- + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
- + " }");
- server.enqueue(response);
-
- assertThat(underTest.getProject(gitlabUrl, "pat", 12345L))
- .extracting(Project::getId, Project::getName)
- .containsExactly(12345L, "SonarQube example 1");
- }
-
- @Test
- public void get_project_fail_if_non_json_payload() {
- MockResponse response = new MockResponse()
- .setResponseCode(200)
- .setBody("non json payload");
- server.enqueue(response);
-
- assertThatThrownBy(() -> underTest.getProject(gitlabUrl, "pat", 12345L))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
- }
-
- @Test
- public void get_branches() {
- MockResponse response = new MockResponse()
- .setResponseCode(200)
- .setBody("[{\n"
- + " \"name\": \"main\",\n"
- + " \"default\": true\n"
- + "},{\n"
- + " \"name\": \"other\",\n"
- + " \"default\": false\n"
- + "}]");
- server.enqueue(response);
-
- assertThat(underTest.getBranches(gitlabUrl, "pat", 12345L))
- .extracting(GitLabBranch::getName, GitLabBranch::isDefault)
- .containsExactly(
- tuple("main", true),
- tuple("other", false)
- );
- }
-
- @Test
- public void get_branches_fail_if_non_json_payload() {
- MockResponse response = new MockResponse()
- .setResponseCode(200)
- .setBody("non json payload");
- server.enqueue(response);
-
- String instanceUrl = gitlabUrl;
- assertThatThrownBy(() -> underTest.getBranches(instanceUrl, "pat", 12345L))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Could not parse GitLab answer to retrieve project branches. Got a non-json payload as result.");
- }
-
- @Test
- public void get_branches_fail_if_exception() throws IOException {
- server.shutdown();
-
- String instanceUrl = gitlabUrl;
- assertThatThrownBy(() -> underTest.getBranches(instanceUrl, "pat", 12345L))
- .isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Failed to connect to");
- }
-
- @Test
- public void search_projects() throws InterruptedException {
- MockResponse projects = new MockResponse()
- .setResponseCode(200)
- .setBody("[\n"
- + " {\n"
- + " \"id\": 1,\n"
- + " \"name\": \"SonarQube example 1\",\n"
- + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
- + " \"path\": \"sonarqube-example-1\",\n"
- + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
- + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
- + " },\n"
- + " {\n"
- + " \"id\": 2,\n"
- + " \"name\": \"SonarQube example 2\",\n"
- + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 2\",\n"
- + " \"path\": \"sonarqube-example-2\",\n"
- + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-2\",\n"
- + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2\"\n"
- + " },\n"
- + " {\n"
- + " \"id\": 3,\n"
- + " \"name\": \"SonarQube example 3\",\n"
- + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 3\",\n"
- + " \"path\": \"sonarqube-example-3\",\n"
- + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-3\",\n"
- + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-3\"\n"
- + " }\n"
- + "]");
- projects.addHeader("X-Page", 1);
- projects.addHeader("X-Per-Page", 10);
- projects.addHeader("X-Total", 3);
- server.enqueue(projects);
-
- ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
-
- assertThat(projectList.getPageNumber()).isOne();
- assertThat(projectList.getPageSize()).isEqualTo(10);
- assertThat(projectList.getTotal()).isEqualTo(3);
-
- assertThat(projectList.getProjects()).hasSize(3);
- assertThat(projectList.getProjects()).extracting(
- Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
- tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
- "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"),
- tuple(2L, "SonarQube example 2", "SonarSource / SonarQube / SonarQube example 2", "sonarqube-example-2", "sonarsource/sonarqube/sonarqube-example-2",
- "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2"),
- tuple(3L, "SonarQube example 3", "SonarSource / SonarQube / SonarQube example 3", "sonarqube-example-3", "sonarsource/sonarqube/sonarqube-example-3",
- "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-3"));
-
- RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
- String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
- assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
- assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
- }
-
- @Test
- public void search_projects_dont_fail_if_no_x_total() throws InterruptedException {
- MockResponse projects = new MockResponse()
- .setResponseCode(200)
- .setBody("[\n"
- + " {\n"
- + " \"id\": 1,\n"
- + " \"name\": \"SonarQube example 1\",\n"
- + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
- + " \"path\": \"sonarqube-example-1\",\n"
- + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
- + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
- + " }"
- + "]");
- projects.addHeader("X-Page", 1);
- projects.addHeader("X-Per-Page", 10);
- server.enqueue(projects);
-
- ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
-
- assertThat(projectList.getPageNumber()).isOne();
- assertThat(projectList.getPageSize()).isEqualTo(10);
- assertThat(projectList.getTotal()).isNull();
-
- assertThat(projectList.getProjects()).hasSize(1);
- assertThat(projectList.getProjects()).extracting(
- Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
- tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
- "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"));
-
- RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
- String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
- assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
- assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
- }
-
- @Test
- public void search_projects_with_case_insensitive_pagination_headers() throws InterruptedException {
- MockResponse projects1 = new MockResponse()
- .setResponseCode(200)
- .setBody("[\n"
- + " {\n"
- + " \"id\": 1,\n"
- + " \"name\": \"SonarQube example 1\",\n"
- + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
- + " \"path\": \"sonarqube-example-1\",\n"
- + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
- + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
- + " }"
- + "]");
- projects1.addHeader("x-page", 1);
- projects1.addHeader("x-Per-page", 1);
- projects1.addHeader("X-Total", 2);
- server.enqueue(projects1);
-
- ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
-
- assertThat(projectList.getPageNumber()).isOne();
- assertThat(projectList.getPageSize()).isOne();
- assertThat(projectList.getTotal()).isEqualTo(2);
-
- assertThat(projectList.getProjects()).hasSize(1);
- assertThat(projectList.getProjects()).extracting(
- Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
- tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
- "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"));
-
- RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
- String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
- assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
- assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
- }
-
- @Test
- public void search_projects_projectName_param_should_be_encoded() throws InterruptedException {
- MockResponse projects = new MockResponse()
- .setResponseCode(200)
- .setBody("[]");
- projects.addHeader("X-Page", 1);
- projects.addHeader("X-Per-Page", 10);
- projects.addHeader("X-Total", 0);
- server.enqueue(projects);
-
- ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "&page=<script>alert('nasty')</script>", 1, 10);
-
- RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
- String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
- assertThat(projectList.getProjects()).isEmpty();
- assertThat(gitlabUrlCall).isEqualTo(
- server.url("")
- + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%26page%3D%3Cscript%3Ealert%28%27nasty%27%29%3C%2Fscript%3E&page=1&per_page=10");
- assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
- }
-
- @Test
- public void search_projects_projectName_param_null_should_pass_empty_string() throws InterruptedException {
- MockResponse projects = new MockResponse()
- .setResponseCode(200)
- .setBody("[]");
- projects.addHeader("X-Page", 1);
- projects.addHeader("X-Per-Page", 10);
- projects.addHeader("X-Total", 0);
- server.enqueue(projects);
-
- ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", null, 1, 10);
-
- RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
- String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
- assertThat(projectList.getProjects()).isEmpty();
- assertThat(gitlabUrlCall).isEqualTo(
- server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=&page=1&per_page=10");
- assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
- }
-
- @Test
- public void get_project_details() throws InterruptedException {
- MockResponse projectResponse = new MockResponse()
- .setResponseCode(200)
- .setBody("{"
- + " \"id\": 1234,"
- + " \"name\": \"SonarQube example 2\","
- + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 2\","
- + " \"path\": \"sonarqube-example-2\","
- + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-2\","
- + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2\""
- + "}");
-
- server.enqueue(projectResponse);
-
- Project project = underTest.getProject(gitlabUrl, "pat", 1234L);
-
- RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
- String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
-
- assertThat(project).isNotNull();
-
- assertThat(gitlabUrlCall).isEqualTo(
- server.url("") + "projects/1234");
- assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
- }
-
- @Test
- public void get_reporter_level_access_project() throws InterruptedException {
- MockResponse projectResponse = new MockResponse()
- .setResponseCode(200)
- .setBody("[{"
- + " \"id\": 1234,"
- + " \"name\": \"SonarQube example 2\","
- + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 2\","
- + " \"path\": \"sonarqube-example-2\","
- + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-2\","
- + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2\""
- + "}]");
-
- server.enqueue(projectResponse);
-
- Optional<Project> project = underTest.getReporterLevelAccessProject(gitlabUrl, "pat", 1234L);
-
- RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
- String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
-
- assertThat(project).isNotNull();
-
- assertThat(gitlabUrlCall).isEqualTo(
- server.url("") + "projects?min_access_level=20&id_after=1233&id_before=1235");
- assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
- }
-
- @Test
- public void search_projects_fail_if_could_not_parse_pagination_number() {
- MockResponse projects = new MockResponse()
- .setResponseCode(200)
- .setBody("[ ]");
- projects.addHeader("X-Page", "bad-page-number");
- projects.addHeader("X-Per-Page", "bad-per-page-number");
- projects.addHeader("X-Total", "bad-total-number");
- server.enqueue(projects);
-
- assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Could not parse pagination number");
- }
-
- @Test
- public void search_projects_fail_if_pagination_data_not_returned() {
- MockResponse projects = new MockResponse()
- .setResponseCode(200)
- .setBody("[ ]");
- server.enqueue(projects);
-
- assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Pagination data from GitLab response is missing");
- }
-
- @Test
- public void throws_ISE_when_get_projects_not_http_200() {
- MockResponse projects = new MockResponse()
- .setResponseCode(500)
- .setBody("test");
- server.enqueue(projects);
-
- assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Could not get projects from GitLab instance");
- }
-
- @Test
- public void fail_check_read_permission_with_unexpected_io_exception_with_detailed_log() throws IOException {
- server.shutdown();
-
- assertThatThrownBy(() -> underTest.checkReadPermission(gitlabUrl, "token"))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Could not validate GitLab read permission. Got an unexpected answer.");
- assertThat(logTester.logs(Level.INFO).get(0))
- .contains("Gitlab API call to [" + server.url("/projects") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
- }
-
- @Test
- public void fail_check_token_with_unexpected_io_exception_with_detailed_log() throws IOException {
- server.shutdown();
-
- assertThatThrownBy(() -> underTest.checkToken(gitlabUrl, "token"))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Could not validate GitLab token. Got an unexpected answer.");
- assertThat(logTester.logs(Level.INFO).get(0))
- .contains("Gitlab API call to [" + server.url("user") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
- }
-
- @Test
- public void fail_check_write_permission_with_unexpected_io_exception_with_detailed_log() throws IOException {
- server.shutdown();
-
- assertThatThrownBy(() -> underTest.checkWritePermission(gitlabUrl, "token"))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Could not validate GitLab write permission. Got an unexpected answer.");
- assertThat(logTester.logs(Level.INFO).get(0))
- .contains("Gitlab API call to [" + server.url("/markdown") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
- }
-
- @Test
- public void fail_get_project_with_unexpected_io_exception_with_detailed_log() throws IOException {
- server.shutdown();
-
- assertThatThrownBy(() -> underTest.getProject(gitlabUrl, "token", 0L))
- .isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Failed to connect to");
- assertThat(logTester.logs(Level.INFO).get(0))
- .contains("Gitlab API call to [" + server.url("/projects/0") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
- }
-
- @Test
- public void fail_get_branches_with_unexpected_io_exception_with_detailed_log() throws IOException {
- server.shutdown();
-
- assertThatThrownBy(() -> underTest.getBranches(gitlabUrl, "token", 0L))
- .isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Failed to connect to " + server.getHostName());
- assertThat(logTester.logs(Level.INFO).get(0))
- .contains("Gitlab API call to [" + server.url("/projects/0/repository/branches") + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
- }
-
- @Test
- public void fail_search_projects_with_unexpected_io_exception_with_detailed_log() throws IOException {
- server.shutdown();
-
- assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "token", null, 1, 1))
- .isInstanceOf(IllegalStateException.class)
- .hasMessageContaining("Failed to connect to");
- assertThat(logTester.logs(Level.INFO).get(0))
- .contains(
- "Gitlab API call to [" + server.url("/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=&page=1&per_page=1")
- + "] " +
- "failed with error message : [Failed to connect to " + server.getHostName());
- }
-}
--- /dev/null
+[
+ {
+ "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"
+ }
+]
*/
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();
"\"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" +
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");
}
}
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;
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() {
.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
@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);
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;
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);
.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);
.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);
.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);
.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);
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;
}
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;
@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() {
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));
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));
@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();
"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));
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;
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
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);
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;
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;
@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;
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);
}
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);
}
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;
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
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);
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;
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,