Browse Source

SONAR-14371 Move gitlab http client to CE

tags/8.7.0.41497
Jacek 3 years ago
parent
commit
044247d489
23 changed files with 1977 additions and 2 deletions
  1. 14
    1
      server/sonar-alm-client/build.gradle
  2. 40
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfiguration.java
  3. 64
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfigurationImpl.java
  4. 183
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java
  5. 62
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GsonError.java
  6. 105
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/Project.java
  7. 53
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/ProjectList.java
  8. 23
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/package-info.java
  9. 38
    0
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/ConstantTimeoutConfiguration.java
  10. 87
    0
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/TimeoutConfigurationImplTest.java
  11. 270
    0
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java
  12. 4
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java
  13. 76
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ImportHelper.java
  14. 149
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java
  15. 197
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposAction.java
  16. 23
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/package-info.java
  17. 23
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/package-info.java
  18. 33
    0
      server/sonar-webserver-webapi/src/main/resources/org/sonar/server/almintegration/ws/gitlab/search_gitlab_repos.json
  19. 36
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModuleTest.java
  20. 61
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWsTest.java
  21. 148
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java
  22. 282
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposActionTest.java
  23. 6
    0
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java

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

@@ -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'

}

+ 40
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfiguration.java View File

@@ -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();
}

+ 64
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfigurationImpl.java View File

@@ -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);
}
}

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

@@ -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);
}
}
}

}

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

@@ -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;
}
}

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

@@ -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;
}

}

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

@@ -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;
}
}

+ 23
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/package-info.java View File

@@ -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;

+ 38
- 0
server/sonar-alm-client/src/test/java/org/sonar/alm/client/ConstantTimeoutConfiguration.java View File

@@ -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;
}
}

+ 87
- 0
server/sonar-alm-client/src/test/java/org/sonar/alm/client/TimeoutConfigurationImplTest.java View File

@@ -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"}
};
}
}

+ 270
- 0
server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java View File

@@ -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");
}
}

+ 4
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java View File

@@ -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);
}
}

+ 76
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ImportHelper.java View File

@@ -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();
}

}

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

@@ -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("/", "_");
}
}

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

@@ -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;
}
}
}

+ 23
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/package-info.java View File

@@ -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;

+ 23
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/package-info.java View File

@@ -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;

+ 33
- 0
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/almintegration/ws/gitlab/search_gitlab_repos.json View File

@@ -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"
}
]
}

+ 36
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModuleTest.java View File

@@ -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();
}

}

+ 61
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWsTest.java View File

@@ -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();
}
}

+ 148
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java View File

@@ -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));
}
}

+ 282
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposActionTest.java View File

@@ -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));
}

}

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

@@ -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

Loading…
Cancel
Save