@@ -1,5 +1,18 @@ | |||
description = 'SonarQube :: ALM integrations :: Clients' | |||
dependencies { | |||
testCompile group: 'junit', name: 'junit' | |||
compile project(path: ':sonar-plugin-api', configuration: 'shadow') | |||
compile project(':sonar-ws') | |||
compile 'com.google.code.gson:gson' | |||
compile 'com.google.guava:guava' | |||
compile 'com.squareup.okhttp3:okhttp' | |||
testCompile project(':sonar-plugin-api-impl') | |||
testCompile 'junit:junit' | |||
testCompile 'com.tngtech.java:junit-dataprovider' | |||
testCompile 'org.assertj:assertj-core' | |||
testCompile 'org.assertj:assertj-guava' | |||
testCompile 'org.mockito:mockito-core' | |||
testCompile 'com.squareup.okhttp3:mockwebserver' | |||
} |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.server.ServerSide; | |||
/** | |||
* Holds the configuration of timeouts when connecting to ALMs. | |||
*/ | |||
@ServerSide | |||
@ComputeEngineSide | |||
public interface TimeoutConfiguration { | |||
/** | |||
* @return connect timeout in milliseconds | |||
*/ | |||
long getConnectTimeout(); | |||
/** | |||
* @return read timeout in milliseconds | |||
*/ | |||
long getReadTimeout(); | |||
} |
@@ -0,0 +1,64 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.OptionalLong; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.utils.log.Loggers; | |||
/** | |||
* Implementation of {@link TimeoutConfiguration} reading values from configuration properties. | |||
*/ | |||
public class TimeoutConfigurationImpl implements TimeoutConfiguration { | |||
private static final String CONNECT_TIMEOUT_PROPERTY = "sonar.alm.timeout.connect"; | |||
private static final String READ_TIMEOUT_PROPERTY = "sonar.alm.timeout.read"; | |||
private static final long DEFAULT_TIMEOUT = 30_000; | |||
private final Configuration configuration; | |||
public TimeoutConfigurationImpl(Configuration configuration) { | |||
this.configuration = configuration; | |||
} | |||
@Override | |||
public long getConnectTimeout() { | |||
return safelyParseLongValue(CONNECT_TIMEOUT_PROPERTY).orElse(DEFAULT_TIMEOUT); | |||
} | |||
private OptionalLong safelyParseLongValue(String property) { | |||
return configuration.get(property) | |||
.map(value -> { | |||
try { | |||
return OptionalLong.of(Long.parseLong(value)); | |||
} catch (NumberFormatException e) { | |||
Loggers.get(TimeoutConfigurationImpl.class) | |||
.warn("Value of property {} can not be parsed to a long: {}", property, value); | |||
return OptionalLong.empty(); | |||
} | |||
}) | |||
.orElse(OptionalLong.empty()); | |||
} | |||
@Override | |||
public long getReadTimeout() { | |||
return safelyParseLongValue(READ_TIMEOUT_PROPERTY).orElse(DEFAULT_TIMEOUT); | |||
} | |||
} |
@@ -0,0 +1,183 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.List; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import okhttp3.OkHttpClient; | |||
import okhttp3.Request; | |||
import okhttp3.Response; | |||
import org.sonar.alm.client.TimeoutConfiguration; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
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 = Loggers.get(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()) | |||
.build(); | |||
} | |||
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 IllegalArgumentException("Your GitLab token was revoked"); | |||
} else if (isTokenExpired(response, body)) { | |||
throw new IllegalArgumentException("Your GitLab token is expired"); | |||
} else if (isInsufficientScope(response, body)) { | |||
throw new IllegalArgumentException("Your GitLab token has insufficient scope"); | |||
} else if (response.code() == HTTP_UNAUTHORIZED) { | |||
throw new IllegalArgumentException("Invalid personal access token"); | |||
} else { | |||
throw new IllegalArgumentException(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) { | |||
throw new IllegalStateException(e.getMessage(), e); | |||
} | |||
} | |||
public ProjectList searchProjects(String gitlabUrl, String personalAccessToken, @Nullable String projectName, | |||
int pageNumber, int pageSize) { | |||
String url = String.format("%s/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%s&page=%d&per_page=%d", | |||
gitlabUrl, projectName == null ? "" : urlEncode(projectName), pageNumber, 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()) { | |||
checkResponseIsSuccessful(response, "Could not get projects from GitLab instance"); | |||
List<Project> projectList = Project.parseJsonArray(response.body().string()); | |||
int returnedPageNumber = parseAndGetIntegerHeader(response.header("X-Page")); | |||
int returnedPageSize = parseAndGetIntegerHeader(response.header("X-Per-Page")); | |||
int totalProjects = parseAndGetIntegerHeader(response.header("X-Total")); | |||
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) { | |||
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); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,62 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.annotations.SerializedName; | |||
import java.util.Optional; | |||
public class GsonError { | |||
@SerializedName("error") | |||
private final String error; | |||
@SerializedName("error_description") | |||
private final String errorDescription; | |||
@SerializedName("message") | |||
private final String message; | |||
public GsonError() { | |||
this("", "", ""); | |||
} | |||
public GsonError(String error, String errorDescription, String message) { | |||
this.error = error; | |||
this.errorDescription = errorDescription; | |||
this.message = message; | |||
} | |||
public static Optional<GsonError> parseOne(String json) { | |||
Gson gson = new Gson(); | |||
return Optional.ofNullable(gson.fromJson(json, GsonError.class)); | |||
} | |||
public String getError() { | |||
return error; | |||
} | |||
public String getErrorDescription() { | |||
return errorDescription; | |||
} | |||
public String getMessage() { | |||
return message; | |||
} | |||
} |
@@ -0,0 +1,105 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.annotations.SerializedName; | |||
import com.google.gson.reflect.TypeToken; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
public class Project { | |||
// https://docs.gitlab.com/ee/api/projects.html#get-single-project | |||
// https://docs.gitlab.com/ee/api/projects.html#list-all-projects | |||
@SerializedName("id") | |||
private long id; | |||
@SerializedName("name") | |||
private final String name; | |||
@SerializedName("name_with_namespace") | |||
private String nameWithNamespace; | |||
@SerializedName("path") | |||
private String path; | |||
@SerializedName("path_with_namespace") | |||
private final String pathWithNamespace; | |||
@SerializedName("web_url") | |||
private String webUrl; | |||
public Project(String name, String pathWithNamespace) { | |||
this.name = name; | |||
this.pathWithNamespace = pathWithNamespace; | |||
} | |||
public Project() { | |||
// http://stackoverflow.com/a/18645370/229031 | |||
this(0, "", "", "", "", ""); | |||
} | |||
public Project(long id, String name, String nameWithNamespace, String path, String pathWithNamespace, | |||
String webUrl) { | |||
this.id = id; | |||
this.name = name; | |||
this.nameWithNamespace = nameWithNamespace; | |||
this.path = path; | |||
this.pathWithNamespace = pathWithNamespace; | |||
this.webUrl = webUrl; | |||
} | |||
public static Project parseJson(String json) { | |||
Gson gson = new Gson(); | |||
return gson.fromJson(json, Project.class); | |||
} | |||
public static List<Project> parseJsonArray(String json) { | |||
Gson gson = new Gson(); | |||
return gson.fromJson(json, new TypeToken<LinkedList<Project>>() { | |||
}.getType()); | |||
} | |||
public long getId() { | |||
return id; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
public String getNameWithNamespace() { | |||
return nameWithNamespace; | |||
} | |||
public String getPath() { | |||
return path; | |||
} | |||
public String getPathWithNamespace() { | |||
return pathWithNamespace; | |||
} | |||
public String getWebUrl() { | |||
return webUrl; | |||
} | |||
} |
@@ -0,0 +1,53 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.List; | |||
public class ProjectList { | |||
private final List<Project> projects; | |||
private final int pageNumber; | |||
private final int pageSize; | |||
private final int total; | |||
public ProjectList(List<Project> projects, int pageNumber, int pageSize, int total) { | |||
this.projects = projects; | |||
this.pageNumber = pageNumber; | |||
this.pageSize = pageSize; | |||
this.total = total; | |||
} | |||
public List<Project> getProjects() { | |||
return projects; | |||
} | |||
public int getPageNumber() { | |||
return pageNumber; | |||
} | |||
public int getPageSize() { | |||
return pageSize; | |||
} | |||
public int getTotal() { | |||
return total; | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.alm.client.gitlab; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,38 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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; | |||
public class ConstantTimeoutConfiguration implements TimeoutConfiguration { | |||
private final long timeout; | |||
public ConstantTimeoutConfiguration(long timeout) { | |||
this.timeout = timeout; | |||
} | |||
@Override | |||
public long getConnectTimeout() { | |||
return timeout; | |||
} | |||
@Override | |||
public long getReadTimeout() { | |||
return timeout; | |||
} | |||
} |
@@ -0,0 +1,87 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 java.util.Random; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
@RunWith(DataProviderRunner.class) | |||
public class TimeoutConfigurationImplTest { | |||
private final MapSettings settings = new MapSettings(); | |||
private final TimeoutConfigurationImpl underTest = new TimeoutConfigurationImpl(settings.asConfig()); | |||
@Test | |||
public void getConnectTimeout_returns_10000_when_property_is_not_defined() { | |||
assertThat(underTest.getConnectTimeout()).isEqualTo(30_000L); | |||
} | |||
@Test | |||
@UseDataProvider("notALongPropertyValues") | |||
public void getConnectTimeout_returns_10000_when_property_is_not_a_long(String notALong) { | |||
settings.setProperty("sonar.alm.timeout.connect", notALong); | |||
assertThat(underTest.getConnectTimeout()).isEqualTo(30_000L); | |||
} | |||
@Test | |||
public void getConnectTimeout_returns_value_of_property() { | |||
long expected = new Random().nextInt(9_456_789); | |||
settings.setProperty("sonar.alm.timeout.connect", expected); | |||
assertThat(underTest.getConnectTimeout()).isEqualTo(expected); | |||
} | |||
@Test | |||
public void getReadTimeout_returns_10000_when_property_is_not_defined() { | |||
assertThat(underTest.getReadTimeout()).isEqualTo(30_000L); | |||
} | |||
@Test | |||
@UseDataProvider("notALongPropertyValues") | |||
public void getReadTimeout_returns_10000_when_property_is_not_a_long(String notALong) { | |||
settings.setProperty("sonar.alm.timeout.read", notALong); | |||
assertThat(underTest.getReadTimeout()).isEqualTo(30_000L); | |||
} | |||
@Test | |||
public void getReadTimeout_returns_value_of_property() { | |||
long expected = new Random().nextInt(9_456_789); | |||
settings.setProperty("sonar.alm.timeout.read", expected); | |||
assertThat(underTest.getReadTimeout()).isEqualTo(expected); | |||
} | |||
@DataProvider | |||
public static Object[][] notALongPropertyValues() { | |||
return new Object[][] { | |||
{"foo"}, | |||
{""}, | |||
{"12.5"} | |||
}; | |||
} | |||
} |
@@ -0,0 +1,270 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.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.Test; | |||
import org.sonar.alm.client.ConstantTimeoutConfiguration; | |||
import org.sonar.alm.client.TimeoutConfiguration; | |||
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 { | |||
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); | |||
String gitlabUrl = this.gitlabUrl; | |||
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); | |||
String gitlabUrl = this.gitlabUrl; | |||
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); | |||
String gitlabUrl = this.gitlabUrl; | |||
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("Invalid personal access token"); | |||
} | |||
@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); | |||
String instanceUrl = gitlabUrl; | |||
assertThatThrownBy(() -> underTest.getProject(instanceUrl, "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 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()).isEqualTo(1); | |||
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_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 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); | |||
String gitlabInstanceUrl = gitlabUrl; | |||
assertThatThrownBy(() -> underTest.searchProjects(gitlabInstanceUrl, "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); | |||
String gitlabInstanceUrl = gitlabUrl; | |||
assertThatThrownBy(() -> underTest.searchProjects(gitlabInstanceUrl, "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); | |||
String gitlabUrl = this.gitlabUrl; | |||
assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("Could not get projects from GitLab instance"); | |||
} | |||
} |
@@ -20,12 +20,15 @@ | |||
package org.sonar.server.almintegration.ws; | |||
import org.sonar.core.platform.Module; | |||
import org.sonar.server.almintegration.ws.gitlab.ImportGitLabProjectAction; | |||
import org.sonar.server.almintegration.ws.gitlab.SearchGitlabReposAction; | |||
public class AlmIntegrationsWSModule extends Module { | |||
@Override | |||
protected void configureModule() { | |||
// TODO:: move alm_integrations actions here | |||
add( | |||
ImportGitLabProjectAction.class, | |||
SearchGitlabReposAction.class, | |||
AlmIntegrationsWs.class); | |||
} | |||
} |
@@ -0,0 +1,76 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.project.Visibility; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Projects.CreateWsResponse.Project; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
import static org.sonarqube.ws.Projects.CreateWsResponse; | |||
import static org.sonarqube.ws.Projects.CreateWsResponse.newBuilder; | |||
@ServerSide | |||
public class ImportHelper { | |||
public static final String PARAM_ALM_SETTING = "almSetting"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
public ImportHelper(DbClient dbClient, UserSession userSession) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
} | |||
public void checkProvisionProjectPermission() { | |||
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); | |||
} | |||
public AlmSettingDto getAlmSetting(Request request) { | |||
String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
return dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) | |||
.orElseThrow(() -> new NotFoundException(String.format("ALM Setting '%s' not found", almSettingKey))); | |||
} | |||
} | |||
public String getUserUuid() { | |||
return requireNonNull(userSession.getUuid(), "User UUID cannot be null"); | |||
} | |||
public static CreateWsResponse toCreateResponse(ComponentDto componentDto) { | |||
return newBuilder() | |||
.setProject(Project.newBuilder() | |||
.setKey(componentDto.getDbKey()) | |||
.setName(componentDto.name()) | |||
.setQualifier(componentDto.qualifier()) | |||
.setVisibility(Visibility.getLabel(componentDto.isPrivate()))) | |||
.build(); | |||
} | |||
} |
@@ -0,0 +1,149 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.gitlab; | |||
import com.google.common.annotations.VisibleForTesting; | |||
import java.util.Optional; | |||
import org.sonar.alm.client.gitlab.GitlabHttpClient; | |||
import org.sonar.alm.client.gitlab.Project; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.alm.pat.AlmPatDto; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.alm.setting.ProjectAlmSettingDto; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; | |||
import org.sonar.server.almintegration.ws.ImportHelper; | |||
import org.sonar.server.component.ComponentUpdater; | |||
import org.sonar.server.project.ProjectDefaultVisibility; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Projects.CreateWsResponse; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.api.resources.Qualifiers.PROJECT; | |||
import static org.sonar.server.component.NewComponent.newComponentBuilder; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class ImportGitLabProjectAction implements AlmIntegrationsWsAction { | |||
public static final String PARAM_GITLAB_PROJECT_ID = "gitlabProjectId"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final ProjectDefaultVisibility projectDefaultVisibility; | |||
private final GitlabHttpClient gitlabHttpClient; | |||
private final ComponentUpdater componentUpdater; | |||
private final UuidFactory uuidFactory; | |||
private final ImportHelper importHelper; | |||
public ImportGitLabProjectAction(DbClient dbClient, UserSession userSession, | |||
ProjectDefaultVisibility projectDefaultVisibility, GitlabHttpClient gitlabHttpClient, | |||
ComponentUpdater componentUpdater, UuidFactory uuidFactory, ImportHelper importHelper) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.projectDefaultVisibility = projectDefaultVisibility; | |||
this.gitlabHttpClient = gitlabHttpClient; | |||
this.componentUpdater = componentUpdater; | |||
this.uuidFactory = uuidFactory; | |||
this.importHelper = importHelper; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("import_gitlab_project") | |||
.setDescription("Import a GitLab project to SonarQube, creating a new project and configuring MR decoration<br/>" + | |||
"Requires the 'Create Projects' permission") | |||
.setPost(true) | |||
.setSince("8.5") | |||
.setHandler(this); | |||
action.createParam(ImportHelper.PARAM_ALM_SETTING) | |||
.setRequired(true) | |||
.setDescription("ALM setting key"); | |||
action.createParam(PARAM_GITLAB_PROJECT_ID) | |||
.setRequired(true) | |||
.setDescription("GitLab project ID"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) throws Exception { | |||
CreateWsResponse createResponse = doHandle(request); | |||
writeProtobuf(createResponse, request, response); | |||
} | |||
private CreateWsResponse doHandle(Request request) { | |||
importHelper.checkProvisionProjectPermission(); | |||
AlmSettingDto almSettingDto = importHelper.getAlmSetting(request); | |||
String userUuid = importHelper.getUserUuid(); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto); | |||
String pat = almPatDto.map(AlmPatDto::getPersonalAccessToken) | |||
.orElseThrow(() -> new IllegalArgumentException(String.format("personal access token for '%s' is missing", almSettingDto.getKey()))); | |||
long gitlabProjectId = request.mandatoryParamAsLong(PARAM_GITLAB_PROJECT_ID); | |||
String url = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null"); | |||
Project gitlabProject = gitlabHttpClient.getProject(url, pat, gitlabProjectId); | |||
ComponentDto componentDto = createProject(dbSession, gitlabProject); | |||
populateMRSetting(dbSession, gitlabProjectId, componentDto, almSettingDto); | |||
return ImportHelper.toCreateResponse(componentDto); | |||
} | |||
} | |||
private void populateMRSetting(DbSession dbSession, Long gitlabProjectId, ComponentDto componentDto, AlmSettingDto almSettingDto) { | |||
dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, new ProjectAlmSettingDto() | |||
.setProjectUuid(componentDto.projectUuid()) | |||
.setAlmSettingUuid(almSettingDto.getUuid()) | |||
.setAlmRepo(gitlabProjectId.toString()) | |||
.setAlmSlug(null) | |||
.setMonorepo(false)); | |||
dbSession.commit(); | |||
} | |||
private ComponentDto createProject(DbSession dbSession, Project gitlabProject) { | |||
boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate(); | |||
String sqProjectKey = generateProjectKey(gitlabProject.getPathWithNamespace(), uuidFactory.create()); | |||
return componentUpdater.create(dbSession, newComponentBuilder() | |||
.setKey(sqProjectKey) | |||
.setName(gitlabProject.getName()) | |||
.setPrivate(visibility) | |||
.setQualifier(PROJECT) | |||
.build(), | |||
userSession.getUuid()); | |||
} | |||
@VisibleForTesting | |||
String generateProjectKey(String pathWithNamespace, String uuid) { | |||
String sqProjectKey = pathWithNamespace + "_" + uuid; | |||
if (sqProjectKey.length() > 250) { | |||
sqProjectKey = sqProjectKey.substring(sqProjectKey.length() - 250); | |||
} | |||
return sqProjectKey.replace("/", "_"); | |||
} | |||
} |
@@ -0,0 +1,197 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.gitlab; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.BinaryOperator; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import org.sonar.alm.client.gitlab.GitlabHttpClient; | |||
import org.sonar.alm.client.gitlab.Project; | |||
import org.sonar.alm.client.gitlab.ProjectList; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.alm.pat.AlmPatDto; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.alm.setting.ProjectAlmSettingDto; | |||
import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.AlmIntegrations; | |||
import org.sonarqube.ws.AlmIntegrations.GitlabRepository; | |||
import org.sonarqube.ws.Common.Paging; | |||
import static java.util.Objects.requireNonNull; | |||
import static java.util.stream.Collectors.toList; | |||
import static java.util.stream.Collectors.toSet; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class SearchGitlabReposAction implements AlmIntegrationsWsAction { | |||
private static final String PARAM_ALM_SETTING = "almSetting"; | |||
private static final String PARAM_PROJECT_NAME = "projectName"; | |||
private static final int DEFAULT_PAGE_SIZE = 20; | |||
private static final int MAX_PAGE_SIZE = 500; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final GitlabHttpClient gitlabHttpClient; | |||
public SearchGitlabReposAction(DbClient dbClient, UserSession userSession, GitlabHttpClient gitlabHttpClient) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.gitlabHttpClient = gitlabHttpClient; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("search_gitlab_repos") | |||
.setDescription("Search the GitLab projects.<br/>" + | |||
"Requires the 'Create Projects' permission") | |||
.setPost(false) | |||
.setSince("8.5") | |||
.setHandler(this); | |||
action.createParam(PARAM_ALM_SETTING) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("ALM setting key"); | |||
action.createParam(PARAM_PROJECT_NAME) | |||
.setRequired(false) | |||
.setMaximumLength(200) | |||
.setDescription("Project name filter"); | |||
action.addPagingParams(DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE); | |||
action.setResponseExample(getClass().getResource("search_gitlab_repos.json")); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
AlmIntegrations.SearchGitlabReposWsResponse wsResponse = doHandle(request); | |||
writeProtobuf(wsResponse, request, response); | |||
} | |||
private AlmIntegrations.SearchGitlabReposWsResponse doHandle(Request request) { | |||
String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); | |||
String projectName = request.param(PARAM_PROJECT_NAME); | |||
int pageNumber = request.mandatoryParamAsInt("p"); | |||
int pageSize = request.mandatoryParamAsInt("ps"); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); | |||
String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null"); | |||
AlmSettingDto almSettingDto = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) | |||
.orElseThrow(() -> new NotFoundException(String.format("ALM Setting '%s' not found", almSettingKey))); | |||
Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto); | |||
String personalAccessToken = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found")); | |||
String gitlabUrl = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null"); | |||
ProjectList gitlabProjectList = gitlabHttpClient | |||
.searchProjects(gitlabUrl, personalAccessToken, projectName, pageNumber, pageSize); | |||
Map<String, ProjectKeyName> sqProjectsKeyByGitlabProjectId = getSqProjectsKeyByGitlabProjectId(dbSession, almSettingDto, gitlabProjectList); | |||
List<GitlabRepository> gitlabRepositories = gitlabProjectList.getProjects().stream() | |||
.map(project -> toGitlabRepository(project, sqProjectsKeyByGitlabProjectId)) | |||
.collect(toList()); | |||
return AlmIntegrations.SearchGitlabReposWsResponse.newBuilder() | |||
.addAllRepositories(gitlabRepositories) | |||
.setPaging(Paging.newBuilder() | |||
.setPageIndex(gitlabProjectList.getPageNumber()) | |||
.setPageSize(gitlabProjectList.getPageSize()) | |||
.setTotal(gitlabProjectList.getTotal()) | |||
.build()) | |||
.build(); | |||
} | |||
} | |||
private Map<String, ProjectKeyName> getSqProjectsKeyByGitlabProjectId(DbSession dbSession, AlmSettingDto almSettingDto, | |||
ProjectList gitlabProjectList) { | |||
Set<String> gitlabProjectIds = gitlabProjectList.getProjects().stream().map(Project::getId).map(String::valueOf) | |||
.collect(toSet()); | |||
Map<String, ProjectAlmSettingDto> projectAlmSettingDtos = dbClient.projectAlmSettingDao() | |||
.selectByAlmSettingAndRepos(dbSession, almSettingDto, gitlabProjectIds) | |||
.stream().collect(Collectors.toMap(ProjectAlmSettingDto::getProjectUuid, Function.identity())); | |||
return dbClient.projectDao().selectByUuids(dbSession, projectAlmSettingDtos.keySet()) | |||
.stream() | |||
.collect(Collectors.toMap(projectDto -> projectAlmSettingDtos.get(projectDto.getUuid()).getAlmRepo(), | |||
p -> new ProjectKeyName(p.getKey(), p.getName()), resolveNameCollisionOperatorByNaturalOrder())); | |||
} | |||
private static BinaryOperator<ProjectKeyName> resolveNameCollisionOperatorByNaturalOrder() { | |||
return (a, b) -> b.key.compareTo(a.key) > 0 ? a : b; | |||
} | |||
private static GitlabRepository toGitlabRepository(Project project, Map<String, ProjectKeyName> sqProjectsKeyByGitlabProjectId) { | |||
String name = project.getName(); | |||
String pathName = removeLastOccurrenceOfString(project.getNameWithNamespace(), " / " + name); | |||
String slug = project.getPath(); | |||
String pathSlug = removeLastOccurrenceOfString(project.getPathWithNamespace(), "/" + slug); | |||
GitlabRepository.Builder builder = GitlabRepository.newBuilder() | |||
.setId(project.getId()) | |||
.setName(name) | |||
.setPathName(pathName) | |||
.setSlug(slug) | |||
.setPathSlug(pathSlug) | |||
.setUrl(project.getWebUrl()); | |||
String projectIdAsString = String.valueOf(project.getId()); | |||
Optional.ofNullable(sqProjectsKeyByGitlabProjectId.get(projectIdAsString)) | |||
.ifPresent(p -> builder | |||
.setSqProjectKey(p.key) | |||
.setSqProjectName(p.name)); | |||
return builder.build(); | |||
} | |||
private static String removeLastOccurrenceOfString(String string, String stringToRemove) { | |||
StringBuilder resultString = new StringBuilder(string); | |||
int index = resultString.lastIndexOf(stringToRemove); | |||
if (index > -1) { | |||
resultString.delete(index, string.length() + index); | |||
} | |||
return resultString.toString(); | |||
} | |||
static class ProjectKeyName { | |||
String key; | |||
String name; | |||
ProjectKeyName(String key, String name) { | |||
this.key = key; | |||
this.name = name; | |||
} | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.almintegration.ws.gitlab; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.almintegration.ws; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,33 @@ | |||
{ | |||
"paging": { | |||
"pageIndex": 1, | |||
"pageSize": 3, | |||
"total": 10 | |||
}, | |||
"repositories": [ | |||
{ | |||
"id": 1, | |||
"name": "Gitlab repo name 1", | |||
"pathName": "Group", | |||
"slug": "gitlab-repo-name-1", | |||
"pathSlug": "group", | |||
"url": "https://example.gitlab.com/group/gitlab-repo-name-1" | |||
}, | |||
{ | |||
"id": 2, | |||
"name": "Gitlab repo name 2", | |||
"pathName": "Group", | |||
"slug": "gitlab-repo-name-2", | |||
"pathSlug": "group", | |||
"url": "https://example.gitlab.com/group/gitlab-repo-name-2" | |||
}, | |||
{ | |||
"id": 3, | |||
"name": "Gitlab repo name 3", | |||
"pathName": "Group", | |||
"slug": "gitlab-repo-name-3", | |||
"pathSlug": "group", | |||
"url": "https://example.gitlab.com/group/gitlab-repo-name-3" | |||
} | |||
] | |||
} |
@@ -0,0 +1,36 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws; | |||
import org.junit.Test; | |||
import org.sonar.core.platform.ComponentContainer; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class AlmIntegrationsWSModuleTest { | |||
@Test | |||
public void verify_count_of_added_components() { | |||
ComponentContainer container = new ComponentContainer(); | |||
new AlmIntegrationsWSModule().configure(container); | |||
assertThat(container.size()).isPositive(); | |||
} | |||
} |
@@ -0,0 +1,61 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws; | |||
import java.util.Collections; | |||
import org.junit.Test; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class AlmIntegrationsWsTest { | |||
private final AlmIntegrationsWs underTest = new AlmIntegrationsWs(Collections.singletonList(new AlmIntegrationsWsAction() { | |||
@Override | |||
public void handle(Request request, Response response) { | |||
// nothing to do | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
controller.createAction("foo") | |||
.setHandler((request, response) -> { | |||
throw new UnsupportedOperationException("not implemented"); | |||
}); | |||
} | |||
})); | |||
@Test | |||
public void define_ws() { | |||
WebService.Context context = new WebService.Context(); | |||
underTest.define(context); | |||
WebService.Controller controller = context.controller("api/alm_integrations"); | |||
assertThat(controller).isNotNull(); | |||
assertThat(controller.description()).isNotEmpty(); | |||
assertThat(controller.actions()).hasSize(1); | |||
WebService.Action fooAction = controller.action("foo"); | |||
assertThat(fooAction).isNotNull(); | |||
assertThat(fooAction.handler()).isNotNull(); | |||
} | |||
} |
@@ -0,0 +1,148 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.gitlab; | |||
import java.util.Optional; | |||
import java.util.stream.IntStream; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.alm.client.gitlab.GitlabHttpClient; | |||
import org.sonar.alm.client.gitlab.Project; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.i18n.I18n; | |||
import org.sonar.core.util.SequenceUuidFactory; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.almintegration.ws.ImportHelper; | |||
import org.sonar.server.component.ComponentUpdater; | |||
import org.sonar.server.es.TestProjectIndexers; | |||
import org.sonar.server.favorite.FavoriteUpdater; | |||
import org.sonar.server.permission.PermissionTemplateService; | |||
import org.sonar.server.project.ProjectDefaultVisibility; | |||
import org.sonar.server.project.Visibility; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.Projects; | |||
import static java.util.stream.Collectors.joining; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
import static org.sonar.server.tester.UserSessionRule.standalone; | |||
public class ImportGitLabProjectActionTest { | |||
private final System2 system2 = mock(System2.class); | |||
@Rule | |||
public UserSessionRule userSession = standalone(); | |||
@Rule | |||
public DbTester db = DbTester.create(system2); | |||
private final ComponentUpdater componentUpdater = new ComponentUpdater(db.getDbClient(), mock(I18n.class), System2.INSTANCE, | |||
mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestProjectIndexers(), new SequenceUuidFactory()); | |||
private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class); | |||
private final UuidFactory uuidFactory = mock(UuidFactory.class); | |||
private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession); | |||
private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class); | |||
private final ImportGitLabProjectAction importGitLabProjectAction = new ImportGitLabProjectAction( | |||
db.getDbClient(), userSession, projectDefaultVisibility, gitlabHttpClient, componentUpdater, uuidFactory, importHelper); | |||
private final WsActionTester ws = new WsActionTester(importGitLabProjectAction); | |||
@Before | |||
public void before() { | |||
when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE); | |||
} | |||
@Test | |||
public void import_project() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setUserUuid(user.getUuid()); | |||
dto.setPersonalAccessToken("PAT"); | |||
}); | |||
Project project = getGitlabProject(); | |||
when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project); | |||
when(uuidFactory.create()).thenReturn("uuid"); | |||
Projects.CreateWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.setParam("gitlabProjectId", "12345") | |||
.executeProtobuf(Projects.CreateWsResponse.class); | |||
verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L); | |||
Projects.CreateWsResponse.Project result = response.getProject(); | |||
assertThat(result.getKey()).isEqualTo(project.getPathWithNamespace() + "_uuid"); | |||
assertThat(result.getName()).isEqualTo(project.getName()); | |||
Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey()); | |||
assertThat(projectDto).isPresent(); | |||
assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent(); | |||
} | |||
@Test | |||
public void generate_project_key_less_than_250() { | |||
String name = "abcdeert"; | |||
assertThat(importGitLabProjectAction.generateProjectKey(name, "uuid")).isEqualTo("abcdeert_uuid"); | |||
} | |||
@Test | |||
public void generate_project_key_equal_250() { | |||
String name = IntStream.range(0, 245).mapToObj(i -> "a").collect(joining()); | |||
String projectKey = importGitLabProjectAction.generateProjectKey(name, "uuid"); | |||
assertThat(projectKey) | |||
.hasSize(250) | |||
.isEqualTo(name + "_uuid"); | |||
} | |||
@Test | |||
public void generate_project_key_more_than_250() { | |||
String name = IntStream.range(0, 250).mapToObj(i -> "a").collect(joining()); | |||
String projectKey = importGitLabProjectAction.generateProjectKey(name, "uuid"); | |||
assertThat(projectKey) | |||
.hasSize(250) | |||
.isEqualTo(name.substring(5) + "_uuid"); | |||
} | |||
@Test | |||
public void generate_project_key_containing_slash() { | |||
String name = "a/b/c"; | |||
assertThat(importGitLabProjectAction.generateProjectKey(name, "uuid")).isEqualTo("a_b_c_uuid"); | |||
} | |||
private Project getGitlabProject() { | |||
return new Project(randomAlphanumeric(5), randomAlphanumeric(5)); | |||
} | |||
} |
@@ -0,0 +1,282 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.almintegration.ws.gitlab; | |||
import java.util.Arrays; | |||
import java.util.LinkedList; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.alm.client.gitlab.GitlabHttpClient; | |||
import org.sonar.alm.client.gitlab.Project; | |||
import org.sonar.alm.client.gitlab.ProjectList; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.alm.pat.AlmPatDto; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.AlmIntegrations; | |||
import org.sonarqube.ws.AlmIntegrations.GitlabRepository; | |||
import org.sonarqube.ws.AlmIntegrations.SearchGitlabReposWsResponse; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
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.anyInt; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
public class SearchGitlabReposActionTest { | |||
@Rule | |||
public UserSessionRule userSession = UserSessionRule.standalone(); | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class); | |||
private final WsActionTester ws = new WsActionTester(new SearchGitlabReposAction(db.getDbClient(), userSession, | |||
gitlabHttpClient)); | |||
@Test | |||
public void list_gitlab_repos() { | |||
Project gitlabProject1 = new Project(1L, "repoName1", "repoNamePath1", "repo-slug-1", "repo-path-slug-1", "url-1"); | |||
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())) | |||
.thenReturn( | |||
new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3, gitlabProject4), 1, 10, 4)); | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setUserUuid(user.getUuid()); | |||
dto.setPersonalAccessToken("some-pat"); | |||
}); | |||
ProjectDto projectDto = db.components().insertPrivateProjectDto(); | |||
db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto); | |||
AlmIntegrations.SearchGitlabReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchGitlabReposWsResponse.class); | |||
assertThat(response.getRepositoriesCount()).isEqualTo(4); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(GitlabRepository::getId, | |||
GitlabRepository::getName, | |||
GitlabRepository::getPathName, | |||
GitlabRepository::getSlug, | |||
GitlabRepository::getPathSlug, | |||
GitlabRepository::getUrl, | |||
GitlabRepository::hasSqProjectKey, | |||
GitlabRepository::hasSqProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple(1L, "repoName1", "repoNamePath1", "repo-slug-1", "repo-path-slug-1", "url-1", false, false), | |||
tuple(2L, "repoName2", "path1", "repo-slug-2", "path-1", "url-2", false, false), | |||
tuple(3L, "repoName3", "repoName3", "repo-slug-3", "repo-slug-3", "url-3", false, false), | |||
tuple(4L, "repoName4", "repoName4 / repoName4", "repo-slug-4", "repo-slug-4/repo-slug-4", "url-4", false, false)); | |||
} | |||
@Test | |||
public void list_gitlab_repos_some_projects_already_set_up() { | |||
Project gitlabProject1 = new Project(1L, "repoName1", "repoNamePath1", "repo-slug-1", "repo-path-slug-1", "url-1"); | |||
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())) | |||
.thenReturn( | |||
new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3, gitlabProject4), 1, 10, 4)); | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setUserUuid(user.getUuid()); | |||
dto.setPersonalAccessToken("some-pat"); | |||
}); | |||
ProjectDto projectDto1 = db.components().insertPrivateProjectDto(); | |||
db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto1); | |||
ProjectDto projectDto2 = db.components().insertPrivateProjectDto(); | |||
db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto2, projectAlmSettingDto -> projectAlmSettingDto.setAlmRepo("2")); | |||
ProjectDto projectDto3 = db.components().insertPrivateProjectDto(); | |||
db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto3, projectAlmSettingDto -> projectAlmSettingDto.setAlmRepo("3")); | |||
ProjectDto projectDto4 = db.components().insertPrivateProjectDto(); | |||
db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto4, projectAlmSettingDto -> projectAlmSettingDto.setAlmRepo("3")); | |||
AlmIntegrations.SearchGitlabReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchGitlabReposWsResponse.class); | |||
assertThat(response.getRepositoriesCount()).isEqualTo(4); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(GitlabRepository::getId, | |||
GitlabRepository::getName, | |||
GitlabRepository::getPathName, | |||
GitlabRepository::getSlug, | |||
GitlabRepository::getPathSlug, | |||
GitlabRepository::getUrl, | |||
GitlabRepository::getSqProjectKey, | |||
GitlabRepository::getSqProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple(1L, "repoName1", "repoNamePath1", "repo-slug-1", "repo-path-slug-1", "url-1", "", ""), | |||
tuple(2L, "repoName2", "path1", "repo-slug-2", "path-1", "url-2", projectDto2.getKey(), projectDto2.getName()), | |||
tuple(3L, "repoName3", "repoName3", "repo-slug-3", "repo-slug-3", "url-3", projectDto3.getKey(), projectDto3.getName()), | |||
tuple(4L, "repoName4", "repoName4 / repoName4", "repo-slug-4", "repo-slug-4/repo-slug-4", "url-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)); | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setUserUuid(user.getUuid()); | |||
}); | |||
ProjectDto projectDto = db.components().insertPrivateProjectDto(); | |||
db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto); | |||
AlmIntegrations.SearchGitlabReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchGitlabReposWsResponse.class); | |||
assertThat(response.getRepositoriesList()).isEmpty(); | |||
} | |||
@Test | |||
public void check_pat_is_missing() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("No personal access token found"); | |||
} | |||
@Test | |||
public void fail_when_alm_setting_not_found() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmPatDto almPatDto = newAlmPatDto(); | |||
db.getDbClient().almPatDao().insert(db.getSession(), almPatDto); | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", "testKey"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(NotFoundException.class) | |||
.hasMessage("ALM Setting 'testKey' not found"); | |||
} | |||
@Test | |||
public void fail_when_not_logged_in() { | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", "anyvalue"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(UnauthorizedException.class) | |||
.hasMessage("Authentication is required"); | |||
} | |||
@Test | |||
public void fail_when_no_creation_project_permission() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user); | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", "anyvalue"); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(ForbiddenException.class) | |||
.hasMessage("Insufficient privileges"); | |||
} | |||
@Test | |||
public void verify_response_example() { | |||
Project gitlabProject1 = new Project(1L, "Gitlab repo name 1", "Group / Gitlab repo name 1", "gitlab-repo-name-1", "group/gitlab-repo-name-1", | |||
"https://example.gitlab.com/group/gitlab-repo-name-1"); | |||
Project gitlabProject2 = new Project(2L, "Gitlab repo name 2", "Group / Gitlab repo name 2", "gitlab-repo-name-2", "group/gitlab-repo-name-2", | |||
"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())) | |||
.thenReturn( | |||
new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3), 1, 3, 10)); | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setUserUuid(user.getUuid()); | |||
dto.setPersonalAccessToken("some-pat"); | |||
}); | |||
ProjectDto projectDto = db.components().insertPrivateProjectDto(); | |||
db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto); | |||
WebService.Action def = ws.getDef(); | |||
String responseExample = def.responseExampleAsString(); | |||
assertThat(responseExample).isNotBlank(); | |||
ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.execute().assertJson( | |||
responseExample); | |||
} | |||
@Test | |||
public void definition() { | |||
WebService.Action def = ws.getDef(); | |||
assertThat(def.since()).isEqualTo("8.5"); | |||
assertThat(def.isPost()).isFalse(); | |||
assertThat(def.params()) | |||
.extracting(WebService.Param::key, WebService.Param::isRequired) | |||
.containsExactlyInAnyOrder( | |||
tuple("almSetting", true), | |||
tuple("p", false), | |||
tuple("ps", false), | |||
tuple("projectName", false)); | |||
} | |||
} |
@@ -20,6 +20,8 @@ | |||
package org.sonar.server.platform.platformlevel; | |||
import java.util.List; | |||
import org.sonar.alm.client.TimeoutConfigurationImpl; | |||
import org.sonar.alm.client.gitlab.GitlabHttpClient; | |||
import org.sonar.api.profiles.AnnotationProfileParser; | |||
import org.sonar.api.profiles.XMLProfileParser; | |||
import org.sonar.api.profiles.XMLProfileSerializer; | |||
@@ -40,6 +42,7 @@ import org.sonar.core.extension.CoreExtensionsInstaller; | |||
import org.sonar.core.platform.ComponentContainer; | |||
import org.sonar.core.platform.PlatformEditionProvider; | |||
import org.sonar.server.almintegration.ws.AlmIntegrationsWSModule; | |||
import org.sonar.server.almintegration.ws.ImportHelper; | |||
import org.sonar.server.almsettings.MultipleAlmFeatureProvider; | |||
import org.sonar.server.authentication.AuthenticationModule; | |||
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierNotificationHandler; | |||
@@ -486,6 +489,9 @@ public class PlatformLevel4 extends PlatformLevel { | |||
PluginsWs.class, | |||
// ALM integrations | |||
TimeoutConfigurationImpl.class, | |||
ImportHelper.class, | |||
GitlabHttpClient.class, | |||
AlmIntegrationsWSModule.class, | |||
// Branch |