@@ -6,6 +6,7 @@ dependencies { | |||
compile 'com.google.code.gson:gson' | |||
compile 'com.google.guava:guava' | |||
compile 'com.squareup.okhttp3:okhttp' | |||
compile 'commons-codec:commons-codec' | |||
testCompile project(':sonar-plugin-api-impl') | |||
@@ -0,0 +1,158 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.common.base.Strings; | |||
import com.google.gson.Gson; | |||
import com.google.gson.GsonBuilder; | |||
import com.google.gson.JsonSyntaxException; | |||
import java.io.IOException; | |||
import java.net.HttpURLConnection; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.function.Function; | |||
import javax.annotation.Nullable; | |||
import okhttp3.OkHttpClient; | |||
import okhttp3.Request; | |||
import okhttp3.RequestBody; | |||
import okhttp3.Response; | |||
import okhttp3.ResponseBody; | |||
import org.apache.commons.codec.binary.Base64; | |||
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 org.sonar.api.internal.apachecommons.lang.StringUtils.isBlank; | |||
import static org.sonar.api.internal.apachecommons.lang.StringUtils.substringBeforeLast; | |||
@ServerSide | |||
public class AzureDevOpsHttpClient { | |||
private static final Logger LOG = Loggers.get(AzureDevOpsHttpClient.class); | |||
protected static final String GET = "GET"; | |||
protected static final String UNABLE_TO_CONTACT_AZURE_SERVER = "Unable to contact Azure DevOps server"; | |||
protected final OkHttpClient client; | |||
public AzureDevOpsHttpClient(TimeoutConfiguration timeoutConfiguration) { | |||
client = new OkHttpClientBuilder() | |||
.setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout()) | |||
.setReadTimeoutMs(timeoutConfiguration.getReadTimeout()) | |||
.build(); | |||
} | |||
public GsonAzureProjectList getProjects(String serverUrl, String token) { | |||
String url = String.format("%s/_apis/projects", getTrimmedUrl(serverUrl)); | |||
LOG.debug(String.format("get projects : [%s]", url)); | |||
return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureProjectList.class)); | |||
} | |||
public GsonAzureRepoList getRepos(String serverUrl, String token, @Nullable String projectName) { | |||
String url; | |||
if (projectName != null && !projectName.isEmpty()) { | |||
url = String.format("%s/%s/_apis/git/repositories", getTrimmedUrl(serverUrl), projectName); | |||
} else { | |||
url = String.format("%s/_apis/git/repositories", getTrimmedUrl(serverUrl)); | |||
} | |||
LOG.debug(String.format("get repos : [%s]", url)); | |||
return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureRepoList.class)); | |||
} | |||
public GsonAzureRepo getRepo(String serverUrl, String token, String projectName, String repositoryName) { | |||
String url = String.format("%s/%s/_apis/git/repositories/%s", getTrimmedUrl(serverUrl), projectName, repositoryName); | |||
LOG.debug(String.format("get repo : [%s]", url)); | |||
return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureRepo.class)); | |||
} | |||
protected <G> G doGet(String token, String url, Function<Response, G> handler) { | |||
Request request = prepareRequestWithToken(token, GET, url, null); | |||
return doCall(request, handler); | |||
} | |||
protected <G> G doCall(Request request, Function<Response, G> handler) { | |||
try (Response response = client.newCall(request).execute()) { | |||
checkResponseIsSuccessful(response); | |||
return handler.apply(response); | |||
} catch (JsonSyntaxException e) { | |||
throw new IllegalArgumentException(UNABLE_TO_CONTACT_AZURE_SERVER + ", got an unexpected response", e); | |||
} catch (IOException e) { | |||
throw new IllegalArgumentException(UNABLE_TO_CONTACT_AZURE_SERVER, e); | |||
} | |||
} | |||
protected static Request prepareRequestWithToken(String token, String method, String url, @Nullable RequestBody body) { | |||
return new Request.Builder() | |||
.method(method, body) | |||
.url(url) | |||
.addHeader("Authorization", encodeToken("accessToken:" + token)) | |||
.build(); | |||
} | |||
protected static void checkResponseIsSuccessful(Response response) throws IOException { | |||
if (!response.isSuccessful()) { | |||
LOG.debug(UNABLE_TO_CONTACT_AZURE_SERVER + ": {} {}", response.request().url().toString(), response.code()); | |||
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { | |||
throw new IllegalArgumentException("Invalid personal access token"); | |||
} | |||
ResponseBody responseBody = response.body(); | |||
String body = responseBody == null ? "" : responseBody.string(); | |||
String errorMessage = generateErrorMessage(body, UNABLE_TO_CONTACT_AZURE_SERVER); | |||
LOG.info(String.format("Azure API call to [%s] failed with %s http code. Azure response content : [%s]", response.request().url().toString(), response.code(), body)); | |||
throw new IllegalArgumentException(errorMessage); | |||
} | |||
} | |||
protected static String generateErrorMessage(String body, String defaultMessage) { | |||
GsonAzureError gsonAzureError = null; | |||
try { | |||
gsonAzureError = buildGson().fromJson(body, GsonAzureError.class); | |||
} catch (JsonSyntaxException e) { | |||
// not a json payload, ignore the error | |||
} | |||
if (gsonAzureError != null && !Strings.isNullOrEmpty(gsonAzureError.message())) { | |||
return defaultMessage + " : " + gsonAzureError.message(); | |||
} else { | |||
return defaultMessage; | |||
} | |||
} | |||
protected static String getTrimmedUrl(String rawUrl) { | |||
if (isBlank(rawUrl)) { | |||
return rawUrl; | |||
} | |||
if (rawUrl.endsWith("/")) { | |||
return substringBeforeLast(rawUrl, "/"); | |||
} | |||
return rawUrl; | |||
} | |||
protected static String encodeToken(String token) { | |||
return String.format("BASIC %s", Base64.encodeBase64String(token.getBytes(StandardCharsets.UTF_8))); | |||
} | |||
protected static Gson buildGson() { | |||
return new GsonBuilder() | |||
.create(); | |||
} | |||
} |
@@ -0,0 +1,41 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.gson.annotations.SerializedName; | |||
import javax.annotation.Nullable; | |||
public class GsonAzureError { | |||
@SerializedName("message") | |||
private final String message; | |||
public GsonAzureError(@Nullable String message) { | |||
this.message = message; | |||
} | |||
public GsonAzureError() { | |||
// http://stackoverflow.com/a/18645370/229031 | |||
this(null); | |||
} | |||
public String message() { | |||
return message; | |||
} | |||
} |
@@ -0,0 +1,48 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.gson.annotations.SerializedName; | |||
public class GsonAzureProject { | |||
@SerializedName("name") | |||
private String name; | |||
@SerializedName("description") | |||
private String description; | |||
public GsonAzureProject() { | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
public GsonAzureProject(String name, String description) { | |||
this.name = name; | |||
this.description = description; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
public String getDescription() { | |||
return description; | |||
} | |||
} |
@@ -0,0 +1,56 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.gson.annotations.SerializedName; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
public class GsonAzureProjectList { | |||
@SerializedName("value") | |||
private List<GsonAzureProject> values; | |||
public GsonAzureProjectList() { | |||
// http://stackoverflow.com/a/18645370/229031 | |||
this(new ArrayList<>()); | |||
} | |||
public GsonAzureProjectList(List<GsonAzureProject> values) { | |||
this.values = values; | |||
} | |||
public List<GsonAzureProject> getValues() { | |||
return values; | |||
} | |||
public GsonAzureProjectList setValues(List<GsonAzureProject> values) { | |||
this.values = values; | |||
return this; | |||
} | |||
@Override | |||
public String toString() { | |||
return "{" + | |||
"values=" + values + | |||
'}'; | |||
} | |||
} |
@@ -0,0 +1,63 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.gson.annotations.SerializedName; | |||
public class GsonAzureRepo { | |||
@SerializedName("id") | |||
private String id; | |||
@SerializedName("name") | |||
private String name; | |||
@SerializedName("url") | |||
private String url; | |||
@SerializedName("project") | |||
private GsonAzureProject project; | |||
public GsonAzureRepo() { | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
public GsonAzureRepo(String id, String name, String url, GsonAzureProject project) { | |||
this.id = id; | |||
this.name = name; | |||
this.url = url; | |||
this.project = project; | |||
} | |||
public String getId() { | |||
return id; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
public String getUrl() { | |||
return url; | |||
} | |||
public GsonAzureProject getProject() { | |||
return project; | |||
} | |||
} |
@@ -0,0 +1,49 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.gson.Gson; | |||
import com.google.gson.annotations.SerializedName; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
public class GsonAzureRepoList { | |||
@SerializedName("value") | |||
private List<GsonAzureRepo> values; | |||
public GsonAzureRepoList() { | |||
// http://stackoverflow.com/a/18645370/229031 | |||
this(new ArrayList<>()); | |||
} | |||
public GsonAzureRepoList(List<GsonAzureRepo> values) { | |||
this.values = values; | |||
} | |||
static GsonAzureRepoList parse(String json) { | |||
return new Gson().fromJson(json, GsonAzureRepoList.class); | |||
} | |||
public List<GsonAzureRepo> getValues() { | |||
return values; | |||
} | |||
} |
@@ -0,0 +1,273 @@ | |||
/* | |||
* 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.azure; | |||
import okhttp3.mockwebserver.MockResponse; | |||
import okhttp3.mockwebserver.MockWebServer; | |||
import okhttp3.mockwebserver.RecordedRequest; | |||
import org.junit.After; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.alm.client.ConstantTimeoutConfiguration; | |||
import org.sonar.alm.client.TimeoutConfiguration; | |||
import org.sonar.api.utils.log.LogTester; | |||
import org.sonar.api.utils.log.LoggerLevel; | |||
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 javax.annotation.Nullable; | |||
import java.io.IOException; | |||
import java.util.concurrent.TimeUnit; | |||
public class AzureDevOpsHttpClientTest { | |||
public static final String UNABLE_TO_CONTACT_AZURE = "Unable to contact Azure DevOps server, got an unexpected response"; | |||
@Rule | |||
public LogTester logTester = new LogTester(); | |||
private static final String NON_JSON_PAYLOAD = "non json payload"; | |||
private final MockWebServer server = new MockWebServer(); | |||
private AzureDevOpsHttpClient underTest; | |||
@Before | |||
public void prepare() throws IOException { | |||
server.start(); | |||
TimeoutConfiguration timeoutConfiguration = new ConstantTimeoutConfiguration(10_000); | |||
underTest = new AzureDevOpsHttpClient(timeoutConfiguration); | |||
} | |||
@After | |||
public void stopServer() throws IOException { | |||
server.shutdown(); | |||
} | |||
@Test | |||
public void get_projects() throws InterruptedException { | |||
enqueueResponse(200, " { \"count\": 2,\n" + | |||
" \"value\": [\n" + | |||
" {\n" + | |||
" \"id\": \"3311cd05-3f00-4a5e-b47f-df94a9982b6e\",\n" + | |||
" \"name\": \"Project 1\",\n" + | |||
" \"description\": \"Project Description\",\n" + | |||
" \"url\": \"https://ado.sonarqube.com/DefaultCollection/_apis/projects/3311cd05-3f00-4a5e-b47f-df94a9982b6e\",\n" + | |||
" \"state\": \"wellFormed\",\n" + | |||
" \"revision\": 63,\n" + | |||
" \"visibility\": \"private\"\n" + | |||
" }," + | |||
"{\n" + | |||
" \"id\": \"3be0f34d-c931-4ff8-8d37-18a83663bd3c\",\n" + | |||
" \"name\": \"Project 2\",\n" + | |||
" \"url\": \"https://ado.sonarqube.com/DefaultCollection/_apis/projects/3be0f34d-c931-4ff8-8d37-18a83663bd3c\",\n" + | |||
" \"state\": \"wellFormed\",\n" + | |||
" \"revision\": 52,\n" + | |||
" \"visibility\": \"private\"\n" + | |||
" }]}"); | |||
GsonAzureProjectList projects = underTest.getProjects(server.url("").toString(), "token"); | |||
RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS); | |||
String azureDevOpsUrlCall = request.getRequestUrl().toString(); | |||
assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "_apis/projects"); | |||
assertThat(request.getMethod()).isEqualTo("GET"); | |||
assertThat(logTester.logs(LoggerLevel.DEBUG)).hasSize(1); | |||
assertThat(logTester.logs(LoggerLevel.DEBUG)) | |||
.contains("get projects : [" + server.url("").toString() + "_apis/projects]"); | |||
assertThat(projects.getValues()).hasSize(2); | |||
assertThat(projects.getValues()) | |||
.extracting(GsonAzureProject::getName, GsonAzureProject::getDescription) | |||
.containsExactly(tuple("Project 1", "Project Description"), tuple("Project 2", null)); | |||
} | |||
@Test | |||
public void get_projects_non_json_payload() { | |||
enqueueResponse(200, NON_JSON_PAYLOAD); | |||
assertThatThrownBy(() -> underTest.getProjects(server.url("").toString(), "token")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage(UNABLE_TO_CONTACT_AZURE); | |||
} | |||
@Test | |||
public void get_projects_with_invalid_pat() { | |||
enqueueResponse(401); | |||
assertThatThrownBy(() -> underTest.getProjects(server.url("").toString(), "invalid-token")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("Invalid personal access token"); | |||
} | |||
@Test | |||
public void get_projects_with_server_error() { | |||
enqueueResponse(500); | |||
assertThatThrownBy(() -> underTest.getProjects(server.url("").toString(), "token")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("Unable to contact Azure DevOps server"); | |||
} | |||
@Test | |||
public void get_repos_with_project_name() throws InterruptedException { | |||
enqueueResponse(200, "{\n" + | |||
" \"value\": [\n" + | |||
" {\n" + | |||
" \"id\": \"741248a4-285e-4a6d-af52-1a49d8070638\",\n" + | |||
" \"name\": \"Repository 1\",\n" + | |||
" \"url\": \"https://ado.sonarqube.com/repositories/\",\n" + | |||
" \"project\": {\n" + | |||
" \"id\": \"c88ddb32-ced8-420d-ab34-764133038b34\",\n" + | |||
" \"name\": \"projectName\",\n" + | |||
" \"url\": \"https://ado.sonarqube.com/DefaultCollection/_apis/projects/c88ddb32-ced8-420d-ab34-764133038b34\",\n" + | |||
" \"state\": \"wellFormed\",\n" + | |||
" \"revision\": 29,\n" + | |||
" \"visibility\": \"private\",\n" + | |||
" \"lastUpdateTime\": \"2020-11-11T09:38:03.3Z\"\n" + | |||
" },\n" + | |||
" \"size\": 0\n" + | |||
" }\n" + | |||
" ],\n" + | |||
" \"count\": 1\n" + | |||
"}"); | |||
GsonAzureRepoList repos = underTest.getRepos(server.url("").toString(), "token", "projectName"); | |||
RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS); | |||
String azureDevOpsUrlCall = request.getRequestUrl().toString(); | |||
assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "projectName/_apis/git/repositories"); | |||
assertThat(request.getMethod()).isEqualTo("GET"); | |||
assertThat(logTester.logs(LoggerLevel.DEBUG)).hasSize(1); | |||
assertThat(logTester.logs(LoggerLevel.DEBUG)) | |||
.contains("get repos : [" + server.url("").toString() + "projectName/_apis/git/repositories]"); | |||
assertThat(repos.getValues()).hasSize(1); | |||
assertThat(repos.getValues()) | |||
.extracting(GsonAzureRepo::getName, GsonAzureRepo::getUrl, r -> r.getProject().getName()) | |||
.containsExactly(tuple("Repository 1", "https://ado.sonarqube.com/repositories/", "projectName")); | |||
} | |||
@Test | |||
public void get_repos_without_project_name() throws InterruptedException { | |||
enqueueResponse(200, "{ \"value\": [], \"count\": 0 }"); | |||
GsonAzureRepoList repos = underTest.getRepos(server.url("").toString(), "token", null); | |||
RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS); | |||
String azureDevOpsUrlCall = request.getRequestUrl().toString(); | |||
assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "_apis/git/repositories"); | |||
assertThat(request.getMethod()).isEqualTo("GET"); | |||
assertThat(repos.getValues()).isEmpty(); | |||
} | |||
@Test | |||
public void get_repos_non_json_payload() { | |||
enqueueResponse(200, NON_JSON_PAYLOAD); | |||
assertThatThrownBy(() -> underTest.getRepos(server.url("").toString(), "token", null)) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage(UNABLE_TO_CONTACT_AZURE); | |||
} | |||
@Test | |||
public void get_repo() throws InterruptedException { | |||
enqueueResponse(200, "{ " + | |||
" \"id\": \"Repo-Id-1\",\n" + | |||
" \"name\": \"Repo-Name-1\",\n" + | |||
" \"url\": \"https://ado.sonarqube.com/DefaultCollection/Repo-Id-1\",\n" + | |||
" \"project\": {\n" + | |||
" \"id\": \"84ea9d51-0c8a-44ad-be92-b2af7fe2c299\",\n" + | |||
" \"name\": \"Project-Name\",\n" + | |||
" \"description\": \"Project's description\" \n" + | |||
" },\n" + | |||
" \"size\": 0" + | |||
"}"); | |||
GsonAzureRepo repo = underTest.getRepo(server.url("").toString(), "token", "Project-Name", "Repo-Name-1"); | |||
RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS); | |||
String azureDevOpsUrlCall = request.getRequestUrl().toString(); | |||
assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "Project-Name/_apis/git/repositories/Repo-Name-1"); | |||
assertThat(request.getMethod()).isEqualTo("GET"); | |||
assertThat(logTester.logs(LoggerLevel.DEBUG)).hasSize(1); | |||
assertThat(logTester.logs(LoggerLevel.DEBUG)) | |||
.contains("get repo : [" + server.url("").toString() + "Project-Name/_apis/git/repositories/Repo-Name-1]"); | |||
assertThat(repo.getId()).isEqualTo("Repo-Id-1"); | |||
assertThat(repo.getName()).isEqualTo("Repo-Name-1"); | |||
assertThat(repo.getUrl()).isEqualTo("https://ado.sonarqube.com/DefaultCollection/Repo-Id-1"); | |||
assertThat(repo.getProject().getName()).isEqualTo("Project-Name"); | |||
} | |||
@Test | |||
public void get_repo_non_json_payload() { | |||
enqueueResponse(200, NON_JSON_PAYLOAD); | |||
assertThatThrownBy(() -> underTest.getRepo(server.url("").toString(), "token", "projectName", "repoName")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage(UNABLE_TO_CONTACT_AZURE); | |||
} | |||
@Test | |||
public void get_repo_json_error_payload() { | |||
enqueueResponse(400, | |||
"{'message':'TF200016: The following project does not exist: projectName. Verify that the name of the project is correct and that the project exists on the specified Azure DevOps Server.'}"); | |||
assertThatThrownBy(() -> underTest.getRepo(server.url("").toString(), "token", "projectName", "repoName")) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage( | |||
"Unable to contact Azure DevOps server : TF200016: The following project does not exist: projectName. Verify that the name of the project is correct and that the project exists on the specified Azure DevOps Server."); | |||
} | |||
private void enqueueResponse(int responseCode) { | |||
enqueueResponse(responseCode, ""); | |||
} | |||
private void enqueueResponse(int responseCode, @Nullable String body) { | |||
server.enqueue(new MockResponse() | |||
.setHeader("Content-Type", "application/json;charset=UTF-8") | |||
.setResponseCode(responseCode) | |||
.setBody(body)); | |||
} | |||
@Test | |||
public void trim_url() { | |||
assertThat(AzureDevOpsHttpClient.getTrimmedUrl("http://localhost:4564/")) | |||
.isEqualTo("http://localhost:4564"); | |||
} | |||
@Test | |||
public void trim_url_without_ending_slash() { | |||
assertThat(AzureDevOpsHttpClient.getTrimmedUrl("http://localhost:4564")) | |||
.isEqualTo("http://localhost:4564"); | |||
} | |||
@Test | |||
public void trim_null_url() { | |||
assertThat(AzureDevOpsHttpClient.getTrimmedUrl(null)) | |||
.isNull(); | |||
} | |||
@Test | |||
public void trim_empty_url() { | |||
assertThat(AzureDevOpsHttpClient.getTrimmedUrl("")) | |||
.isEmpty(); | |||
} | |||
} |
@@ -20,6 +20,9 @@ | |||
package org.sonar.server.almintegration.ws; | |||
import org.sonar.core.platform.Module; | |||
import org.sonar.server.almintegration.ws.azure.ImportAzureProjectAction; | |||
import org.sonar.server.almintegration.ws.azure.ListAzureProjectsAction; | |||
import org.sonar.server.almintegration.ws.azure.SearchAzureReposAction; | |||
import org.sonar.server.almintegration.ws.bitbucketserver.ImportBitbucketServerProjectAction; | |||
import org.sonar.server.almintegration.ws.bitbucketserver.ListBitbucketServerProjectsAction; | |||
import org.sonar.server.almintegration.ws.bitbucketserver.SearchBitbucketServerReposAction; | |||
@@ -35,6 +38,9 @@ public class AlmIntegrationsWSModule extends Module { | |||
SearchBitbucketServerReposAction.class, | |||
ImportGitLabProjectAction.class, | |||
SearchGitlabReposAction.class, | |||
ImportAzureProjectAction.class, | |||
ListAzureProjectsAction.class, | |||
SearchAzureReposAction.class, | |||
AlmIntegrationsWs.class); | |||
} | |||
} |
@@ -0,0 +1,163 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.common.annotations.VisibleForTesting; | |||
import java.util.Optional; | |||
import org.sonar.alm.client.azure.AzureDevOpsHttpClient; | |||
import org.sonar.alm.client.azure.GsonAzureRepo; | |||
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.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.almintegration.ws.ImportHelper.PARAM_ALM_SETTING; | |||
import static org.sonar.server.almintegration.ws.ImportHelper.toCreateResponse; | |||
import static org.sonar.server.component.NewComponent.newComponentBuilder; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class ImportAzureProjectAction implements AlmIntegrationsWsAction { | |||
private static final String PARAM_REPOSITORY_NAME = "repositoryName"; | |||
private static final String PARAM_PROJECT_NAME = "projectName"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final AzureDevOpsHttpClient azureDevOpsHttpClient; | |||
private final ProjectDefaultVisibility projectDefaultVisibility; | |||
private final ComponentUpdater componentUpdater; | |||
private final ImportHelper importHelper; | |||
public ImportAzureProjectAction(DbClient dbClient, UserSession userSession, AzureDevOpsHttpClient azureDevOpsHttpClient, | |||
ProjectDefaultVisibility projectDefaultVisibility, ComponentUpdater componentUpdater, | |||
ImportHelper importHelper) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.azureDevOpsHttpClient = azureDevOpsHttpClient; | |||
this.projectDefaultVisibility = projectDefaultVisibility; | |||
this.componentUpdater = componentUpdater; | |||
this.importHelper = importHelper; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("import_azure_project") | |||
.setDescription("Create a SonarQube project with the information from the provided Azure DevOps project.<br/>" + | |||
"Autoconfigure pull request decoration mechanism.<br/>" + | |||
"Requires the 'Create Projects' permission") | |||
.setPost(true) | |||
.setInternal(true) | |||
.setSince("8.6") | |||
.setHandler(this); | |||
action.createParam(PARAM_ALM_SETTING) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("ALM setting key"); | |||
action.createParam(PARAM_PROJECT_NAME) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("Azure project name"); | |||
action.createParam(PARAM_REPOSITORY_NAME) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("Azure repository name"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
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()))); | |||
String projectName = request.mandatoryParam(PARAM_PROJECT_NAME); | |||
String repositoryName = request.mandatoryParam(PARAM_REPOSITORY_NAME); | |||
String url = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null"); | |||
GsonAzureRepo repo = azureDevOpsHttpClient.getRepo(url, pat, projectName, repositoryName); | |||
ComponentDto componentDto = createProject(dbSession, repo); | |||
populatePRSetting(dbSession, repo, componentDto, almSettingDto); | |||
return toCreateResponse(componentDto); | |||
} | |||
} | |||
private ComponentDto createProject(DbSession dbSession, GsonAzureRepo repo) { | |||
boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate(); | |||
return componentUpdater.create(dbSession, newComponentBuilder() | |||
.setKey(generateProjectKey(repo.getProject().getName(), repo.getName())) | |||
.setName(repo.getName()) | |||
.setPrivate(visibility) | |||
.setQualifier(PROJECT) | |||
.build(), | |||
userSession.isLoggedIn() ? userSession.getUuid() : null); | |||
} | |||
private void populatePRSetting(DbSession dbSession, GsonAzureRepo repo, ComponentDto componentDto, AlmSettingDto almSettingDto) { | |||
ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto() | |||
.setAlmSettingUuid(almSettingDto.getUuid()) | |||
.setAlmRepo(repo.getName()) | |||
.setAlmSlug(repo.getProject().getName()) | |||
.setProjectUuid(componentDto.uuid()) | |||
.setMonorepo(false); | |||
dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto); | |||
dbSession.commit(); | |||
} | |||
@VisibleForTesting | |||
String generateProjectKey(String projectName, String repoName) { | |||
String sqProjectKey = projectName + "_" + repoName; | |||
if (sqProjectKey.length() > 250) { | |||
sqProjectKey = sqProjectKey.substring(sqProjectKey.length() - 250); | |||
} | |||
return sqProjectKey.replace(" ", "_"); | |||
} | |||
} |
@@ -0,0 +1,115 @@ | |||
/* | |||
* 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.azure; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.stream.Collectors; | |||
import org.sonar.alm.client.azure.AzureDevOpsHttpClient; | |||
import org.sonar.alm.client.azure.GsonAzureProject; | |||
import org.sonar.alm.client.azure.GsonAzureProjectList; | |||
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.server.almintegration.ws.AlmIntegrationsWsAction; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.AlmIntegrations.AzureProject; | |||
import org.sonarqube.ws.AlmIntegrations.ListAzureProjectsWsResponse; | |||
import static java.util.Comparator.comparing; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class ListAzureProjectsAction implements AlmIntegrationsWsAction { | |||
private static final String PARAM_ALM_SETTING = "almSetting"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final AzureDevOpsHttpClient azureDevOpsHttpClient; | |||
public ListAzureProjectsAction(DbClient dbClient, UserSession userSession, AzureDevOpsHttpClient azureDevOpsHttpClient) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.azureDevOpsHttpClient = azureDevOpsHttpClient; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("list_azure_projects") | |||
.setDescription("List Azure projects<br/>" + | |||
"Requires the 'Create Projects' permission") | |||
.setPost(false) | |||
.setSince("8.6") | |||
.setHandler(this); | |||
action.createParam(PARAM_ALM_SETTING) | |||
.setRequired(true) | |||
.setMaximumLength(200) | |||
.setDescription("ALM setting key"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
ListAzureProjectsWsResponse wsResponse = doHandle(request); | |||
writeProtobuf(wsResponse, request, response); | |||
} | |||
private ListAzureProjectsWsResponse doHandle(Request request) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); | |||
String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); | |||
String userUuid = requireNonNull(userSession.getUuid(), "User UUID is not 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 pat = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found")); | |||
String url = requireNonNull(almSettingDto.getUrl(), "URL cannot be null"); | |||
GsonAzureProjectList projectList = azureDevOpsHttpClient.getProjects(url, pat); | |||
List<AzureProject> values = projectList.getValues().stream() | |||
.map(ListAzureProjectsAction::toAzureProject) | |||
.sorted(comparing(AzureProject::getName, String::compareToIgnoreCase)) | |||
.collect(Collectors.toList()); | |||
ListAzureProjectsWsResponse.Builder builder = ListAzureProjectsWsResponse.newBuilder() | |||
.addAllProjects(values); | |||
return builder.build(); | |||
} | |||
} | |||
private static AzureProject toAzureProject(GsonAzureProject project) { | |||
return AzureProject.newBuilder() | |||
.setName(project.getName()) | |||
.setDescription(Optional.ofNullable(project.getDescription()).orElse("")) | |||
.build(); | |||
} | |||
} |
@@ -0,0 +1,235 @@ | |||
/* | |||
* 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.azure; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.BinaryOperator; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.Nullable; | |||
import org.sonar.alm.client.azure.AzureDevOpsHttpClient; | |||
import org.sonar.alm.client.azure.GsonAzureRepo; | |||
import org.sonar.alm.client.azure.GsonAzureRepoList; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
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.project.ProjectDto; | |||
import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.AlmIntegrations.AzureRepo; | |||
import org.sonarqube.ws.AlmIntegrations.SearchAzureReposWsResponse; | |||
import static java.util.Comparator.comparing; | |||
import static java.util.Objects.requireNonNull; | |||
import static java.util.stream.Collectors.toList; | |||
import static java.util.stream.Collectors.toMap; | |||
import static java.util.stream.Collectors.toSet; | |||
import static org.apache.commons.lang.StringUtils.containsIgnoreCase; | |||
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class SearchAzureReposAction implements AlmIntegrationsWsAction { | |||
private static final Logger LOG = Loggers.get(SearchAzureReposAction.class); | |||
private static final String PARAM_ALM_SETTING = "almSetting"; | |||
private static final String PARAM_PROJECT_NAME = "projectName"; | |||
private static final String PARAM_SEARCH_QUERY = "searchQuery"; | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final AzureDevOpsHttpClient azureDevOpsHttpClient; | |||
public SearchAzureReposAction(DbClient dbClient, UserSession userSession, | |||
AzureDevOpsHttpClient azureDevOpsHttpClient) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.azureDevOpsHttpClient = azureDevOpsHttpClient; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("search_azure_repos") | |||
.setDescription("Search the Azure repositories<br/>" + | |||
"Requires the 'Create Projects' permission") | |||
.setPost(false) | |||
.setSince("8.6") | |||
.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.createParam(PARAM_SEARCH_QUERY) | |||
.setRequired(false) | |||
.setMaximumLength(200) | |||
.setDescription("Search query filter"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
SearchAzureReposWsResponse wsResponse = doHandle(request); | |||
writeProtobuf(wsResponse, request, response); | |||
} | |||
private SearchAzureReposWsResponse doHandle(Request request) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); | |||
String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); | |||
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 projectKey = request.param(PARAM_PROJECT_NAME); | |||
String searchQuery = request.param(PARAM_SEARCH_QUERY); | |||
String pat = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found")); | |||
String url = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null"); | |||
GsonAzureRepoList gsonAzureRepoList = azureDevOpsHttpClient.getRepos(url, pat, projectKey); | |||
Map<ProjectKeyName, ProjectDto> sqProjectsKeyByAzureKey = getSqProjectsKeyByCustomKey(dbSession, almSettingDto, gsonAzureRepoList); | |||
List<AzureRepo> repositories = gsonAzureRepoList.getValues() | |||
.stream() | |||
.filter(r -> isSearchOnlyByProjectName(searchQuery) || doesSearchCriteriaMatchProjectOrRepo(r, searchQuery)) | |||
.map(repo -> toAzureRepo(repo, sqProjectsKeyByAzureKey)) | |||
.sorted(comparing(AzureRepo::getName, String::compareToIgnoreCase)) | |||
.collect(toList()); | |||
LOG.debug(repositories.toString()); | |||
return SearchAzureReposWsResponse.newBuilder() | |||
.addAllRepositories(repositories) | |||
.build(); | |||
} | |||
} | |||
private Map<ProjectKeyName, ProjectDto> getSqProjectsKeyByCustomKey(DbSession dbSession, AlmSettingDto almSettingDto, | |||
GsonAzureRepoList azureProjectList) { | |||
Set<String> projectNames = azureProjectList.getValues().stream().map(r -> r.getProject().getName()).collect(toSet()); | |||
Set<ProjectKeyName> azureProjectsAndRepos = azureProjectList.getValues().stream().map(ProjectKeyName::from).collect(toSet()); | |||
List<ProjectAlmSettingDto> projectAlmSettingDtos = dbClient.projectAlmSettingDao() | |||
.selectByAlmSettingAndSlugs(dbSession, almSettingDto, projectNames); | |||
Map<String, ProjectAlmSettingDto> filteredProjectsByUuid = projectAlmSettingDtos | |||
.stream() | |||
.filter(p -> azureProjectsAndRepos.contains(ProjectKeyName.from(p))) | |||
.collect(toMap(ProjectAlmSettingDto::getProjectUuid, Function.identity())); | |||
Set<String> projectUuids = filteredProjectsByUuid.values().stream().map(ProjectAlmSettingDto::getProjectUuid).collect(toSet()); | |||
return dbClient.projectDao().selectByUuids(dbSession, projectUuids) | |||
.stream() | |||
.collect(Collectors.toMap( | |||
projectDto -> ProjectKeyName.from(filteredProjectsByUuid.get(projectDto.getUuid())), | |||
p -> p, | |||
resolveNameCollisionOperatorByNaturalOrder())); | |||
} | |||
private static boolean isSearchOnlyByProjectName(@Nullable String criteria) { | |||
return criteria == null || criteria.isEmpty(); | |||
} | |||
private static boolean doesSearchCriteriaMatchProjectOrRepo(GsonAzureRepo repo, String criteria) { | |||
boolean matchProject = containsIgnoreCase(repo.getProject().getName(), criteria); | |||
boolean matchRepo = containsIgnoreCase(repo.getName(), criteria); | |||
return matchProject || matchRepo; | |||
} | |||
private static AzureRepo toAzureRepo(GsonAzureRepo azureRepo, Map<ProjectKeyName, ProjectDto> sqProjectsKeyByAzureKey) { | |||
AzureRepo.Builder builder = AzureRepo.newBuilder() | |||
.setName(azureRepo.getName()) | |||
.setProjectName(azureRepo.getProject().getName()); | |||
ProjectDto projectDto = sqProjectsKeyByAzureKey.get(ProjectKeyName.from(azureRepo)); | |||
if (projectDto != null) { | |||
builder.setSqProjectName(projectDto.getName()); | |||
builder.setSqProjectKey(projectDto.getKey()); | |||
} | |||
return builder.build(); | |||
} | |||
private static BinaryOperator<ProjectDto> resolveNameCollisionOperatorByNaturalOrder() { | |||
return (a, b) -> b.getKey().compareTo(a.getKey()) > 0 ? a : b; | |||
} | |||
static class ProjectKeyName { | |||
final String projectName; | |||
final String repoName; | |||
ProjectKeyName(String projectName, String repoName) { | |||
this.projectName = projectName; | |||
this.repoName = repoName; | |||
} | |||
public static ProjectKeyName from(ProjectAlmSettingDto project) { | |||
return new ProjectKeyName(project.getAlmSlug(), project.getAlmRepo()); | |||
} | |||
public static ProjectKeyName from(GsonAzureRepo gsonAzureRepo) { | |||
return new ProjectKeyName(gsonAzureRepo.getProject().getName(), gsonAzureRepo.getName()); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
ProjectKeyName that = (ProjectKeyName) o; | |||
return Objects.equals(projectName, that.projectName) && | |||
Objects.equals(repoName, that.repoName); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(projectName, repoName); | |||
} | |||
} | |||
} |
@@ -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.azure; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,238 @@ | |||
/* | |||
* 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.azure; | |||
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.azure.AzureDevOpsHttpClient; | |||
import org.sonar.alm.client.azure.GsonAzureProject; | |||
import org.sonar.alm.client.azure.GsonAzureRepo; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.i18n.I18n; | |||
import org.sonar.core.util.SequenceUuidFactory; | |||
import org.sonar.db.DbTester; | |||
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.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.exceptions.BadRequestException; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
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.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.Projects; | |||
import static java.util.stream.Collectors.joining; | |||
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.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; | |||
import static org.sonar.db.permission.GlobalPermission.SCAN; | |||
public class ImportAzureProjectActionTest { | |||
@Rule | |||
public UserSessionRule userSession = UserSessionRule.standalone(); | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private final AzureDevOpsHttpClient azureDevOpsHttpClient = mock(AzureDevOpsHttpClient.class); | |||
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 ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession); | |||
private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class); | |||
private final ImportAzureProjectAction importAzureProjectAction = new ImportAzureProjectAction(db.getDbClient(), userSession, | |||
azureDevOpsHttpClient, projectDefaultVisibility, componentUpdater, importHelper); | |||
private final WsActionTester ws = new WsActionTester(importAzureProjectAction); | |||
@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().insertAzureAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setPersonalAccessToken(almSetting.getPersonalAccessToken()); | |||
dto.setUserUuid(user.getUuid()); | |||
}); | |||
GsonAzureRepo repo = getGsonAzureRepo(); | |||
when(azureDevOpsHttpClient.getRepo(almSetting.getUrl(), almSetting.getPersonalAccessToken(), "project-name", "repo-name")) | |||
.thenReturn(repo); | |||
Projects.CreateWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.setParam("projectName", "project-name") | |||
.setParam("repositoryName", "repo-name") | |||
.executeProtobuf(Projects.CreateWsResponse.class); | |||
Projects.CreateWsResponse.Project result = response.getProject(); | |||
assertThat(result.getKey()).isEqualTo(repo.getProject().getName() + "_" + repo.getName()); | |||
assertThat(result.getName()).isEqualTo(repo.getName()); | |||
Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey()); | |||
assertThat(projectDto).isPresent(); | |||
Optional<ProjectAlmSettingDto> projectAlmSettingDto = db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get()); | |||
assertThat(projectAlmSettingDto.get().getAlmRepo()).isEqualTo("repo-name"); | |||
assertThat(projectAlmSettingDto.get().getAlmSettingUuid()).isEqualTo(almSetting.getUuid()); | |||
assertThat(projectAlmSettingDto.get().getAlmSlug()).isEqualTo("project-name"); | |||
} | |||
@Test | |||
public void fail_when_not_logged_in() { | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", "azure") | |||
.setParam("projectName", "project-name") | |||
.setParam("repositoryName", "repo-name"); | |||
assertThatThrownBy(() -> request.execute()) | |||
.isInstanceOf(UnauthorizedException.class); | |||
} | |||
@Test | |||
public void fail_when_missing_project_creator_permission() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(SCAN); | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", "azure") | |||
.setParam("projectName", "project-name") | |||
.setParam("repositoryName", "repo-name"); | |||
assertThatThrownBy(() -> request.execute()) | |||
.isInstanceOf(ForbiddenException.class) | |||
.hasMessage("Insufficient privileges"); | |||
} | |||
@Test | |||
public void check_pat_is_missing() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting(); | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.setParam("projectName", "project-name") | |||
.setParam("repositoryName", "repo-name"); | |||
assertThatThrownBy(() -> request.execute()) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("personal access token for '" + almSetting.getKey() + "' is missing"); | |||
} | |||
@Test | |||
public void fail_check_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_project_already_exists() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setPersonalAccessToken(almSetting.getPersonalAccessToken()); | |||
dto.setUserUuid(user.getUuid()); | |||
}); | |||
GsonAzureRepo repo = getGsonAzureRepo(); | |||
String projectKey = repo.getProject().getName() + "_" + repo.getName(); | |||
db.components().insertPublicProject(p -> p.setDbKey(projectKey)); | |||
when(azureDevOpsHttpClient.getRepo(almSetting.getUrl(), almSetting.getPersonalAccessToken(), "project-name", "repo-name")).thenReturn(repo); | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.setParam("projectName", "project-name") | |||
.setParam("repositoryName", "repo-name"); | |||
assertThatThrownBy(() -> request.execute()) | |||
.isInstanceOf(BadRequestException.class) | |||
.hasMessage("Could not create null, key already exists: " + projectKey); | |||
} | |||
@Test | |||
public void sanitize_project_and_repo_names_with_invalid_characters() { | |||
assertThat(importAzureProjectAction.generateProjectKey("project name", "repo name")) | |||
.isEqualTo("project_name_repo_name"); | |||
} | |||
@Test | |||
public void sanitize_long_project_and_repo_names() { | |||
String projectName = IntStream.range(0, 260).mapToObj(i -> "a").collect(joining()); | |||
assertThat(importAzureProjectAction.generateProjectKey(projectName, "repo name")) | |||
.hasSize(250); | |||
} | |||
@Test | |||
public void define() { | |||
WebService.Action def = ws.getDef(); | |||
assertThat(def.since()).isEqualTo("8.6"); | |||
assertThat(def.isPost()).isTrue(); | |||
assertThat(def.params()) | |||
.extracting(WebService.Param::key, WebService.Param::isRequired) | |||
.containsExactlyInAnyOrder( | |||
tuple("almSetting", true), | |||
tuple("projectName", true), | |||
tuple("repositoryName", true)); | |||
} | |||
private GsonAzureRepo getGsonAzureRepo() { | |||
return new GsonAzureRepo("repo-id", "repo-name", "repo-url", | |||
new GsonAzureProject("project-name", "project-description")); | |||
} | |||
} |
@@ -0,0 +1,187 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.common.collect.ImmutableList; | |||
import java.util.List; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.alm.client.azure.AzureDevOpsHttpClient; | |||
import org.sonar.alm.client.azure.GsonAzureProject; | |||
import org.sonar.alm.client.azure.GsonAzureProjectList; | |||
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.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.AzureProject; | |||
import org.sonarqube.ws.AlmIntegrations.ListAzureProjectsWsResponse; | |||
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.anyString; | |||
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 ListAzureProjectsActionTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Rule | |||
public UserSessionRule userSession = UserSessionRule.standalone(); | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private final AzureDevOpsHttpClient azureDevOpsHttpClient = mock(AzureDevOpsHttpClient.class); | |||
private final WsActionTester ws = new WsActionTester(new ListAzureProjectsAction(db.getDbClient(), userSession, azureDevOpsHttpClient)); | |||
@Before | |||
public void before() { | |||
mockClient(ImmutableList.of(new GsonAzureProject("name", "description"), | |||
new GsonAzureProject("name", null))); | |||
} | |||
private void mockClient(List<GsonAzureProject> projects) { | |||
GsonAzureProjectList projectList = new GsonAzureProjectList(); | |||
projectList.setValues(projects); | |||
when(azureDevOpsHttpClient.getProjects(anyString(), anyString())).thenReturn(projectList); | |||
} | |||
@Test | |||
public void list_projects() { | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
ListAzureProjectsWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(ListAzureProjectsWsResponse.class); | |||
assertThat(response.getProjectsCount()).isEqualTo(2); | |||
assertThat(response.getProjectsList()) | |||
.extracting(AzureProject::getName, AzureProject::getDescription) | |||
.containsExactly(tuple("name", "description"), tuple("name", "")); | |||
} | |||
@Test | |||
public void list_projects_alphabetically_sorted() { | |||
mockClient(ImmutableList.of(new GsonAzureProject("BBB project", "BBB project description"), | |||
new GsonAzureProject("AAA project 1", "AAA project description"), | |||
new GsonAzureProject("zzz project", "zzz project description"), | |||
new GsonAzureProject("aaa project", "aaa project description"))); | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
ListAzureProjectsWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(ListAzureProjectsWsResponse.class); | |||
assertThat(response.getProjectsCount()).isEqualTo(4); | |||
assertThat(response.getProjectsList()) | |||
.extracting(AzureProject::getName, AzureProject::getDescription) | |||
.containsExactly(tuple("aaa project", "aaa project description"), tuple("AAA project 1", "AAA project description"), | |||
tuple("BBB project", "BBB project description"), tuple("zzz project", "zzz project description")); | |||
} | |||
@Test | |||
public void check_pat_is_missing() { | |||
insertUser(); | |||
AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting(); | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("No personal access token found"); | |||
} | |||
@Test | |||
public void fail_check_alm_setting_not_found() { | |||
insertUser(); | |||
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); | |||
} | |||
@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 definition() { | |||
WebService.Action def = ws.getDef(); | |||
assertThat(def.since()).isEqualTo("8.6"); | |||
assertThat(def.isPost()).isFalse(); | |||
assertThat(def.params()) | |||
.extracting(WebService.Param::key, WebService.Param::isRequired) | |||
.containsExactlyInAnyOrder(tuple("almSetting", true)); | |||
} | |||
private UserDto insertUser() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
return user; | |||
} | |||
private AlmSettingDto insertAlmSetting() { | |||
UserDto user = insertUser(); | |||
AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setUserUuid(user.getUuid()); | |||
}); | |||
return almSetting; | |||
} | |||
} |
@@ -0,0 +1,367 @@ | |||
/* | |||
* 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.azure; | |||
import com.google.common.collect.ImmutableList; | |||
import org.jetbrains.annotations.NotNull; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.alm.client.azure.AzureDevOpsHttpClient; | |||
import org.sonar.alm.client.azure.GsonAzureProject; | |||
import org.sonar.alm.client.azure.GsonAzureRepo; | |||
import org.sonar.alm.client.azure.GsonAzureRepoList; | |||
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 static java.util.Collections.emptyList; | |||
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.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; | |||
import static org.sonarqube.ws.AlmIntegrations.AzureRepo; | |||
import static org.sonarqube.ws.AlmIntegrations.SearchAzureReposWsResponse; | |||
public class SearchAzureReposActionTest { | |||
@Rule | |||
public UserSessionRule userSession = UserSessionRule.standalone(); | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private AzureDevOpsHttpClient azureDevOpsHttpClient = mock(AzureDevOpsHttpClient.class); | |||
private WsActionTester ws = new WsActionTester(new SearchAzureReposAction(db.getDbClient(), userSession, azureDevOpsHttpClient)); | |||
@Before | |||
public void before() { | |||
mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("project-1", "repoName-1"), | |||
getGsonAzureRepo("project-2", "repoName-2")))); | |||
} | |||
@Test | |||
public void define() { | |||
WebService.Action def = ws.getDef(); | |||
assertThat(def.since()).isEqualTo("8.6"); | |||
assertThat(def.isPost()).isFalse(); | |||
assertThat(def.params()) | |||
.extracting(WebService.Param::key, WebService.Param::isRequired) | |||
.containsExactlyInAnyOrder( | |||
tuple("almSetting", true), | |||
tuple("projectName", false), | |||
tuple("searchQuery", false)); | |||
} | |||
@Test | |||
public void search_repos() { | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple("repoName-1", "project-1"), tuple("repoName-2", "project-2")); | |||
} | |||
@Test | |||
public void search_repos_alphabetically_sorted() { | |||
mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("project-1", "Z-repo"), | |||
getGsonAzureRepo("project-1", "A-repo-1"), getGsonAzureRepo("project-1", "a-repo"), | |||
getGsonAzureRepo("project-1", "b-repo")))); | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName) | |||
.containsExactly( | |||
tuple("a-repo", "project-1"), tuple("A-repo-1", "project-1"), | |||
tuple("b-repo", "project-1"), tuple("Z-repo", "project-1")); | |||
} | |||
@Test | |||
public void search_repos_with_project_already_set_up() { | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
ProjectDto projectDto2 = insertProject(almSetting, "repoName-2", "project-2"); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesCount()).isEqualTo(2); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName, | |||
AzureRepo::getSqProjectKey, AzureRepo::getSqProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple("repoName-1", "project-1", "", ""), | |||
tuple("repoName-2", "project-2", projectDto2.getKey(), projectDto2.getName())); | |||
} | |||
@Test | |||
public void search_repos_with_project_already_set_u_and_collision_is_handled() { | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
ProjectDto projectDto2 = insertProject(almSetting, "repoName-2", "project-2"); | |||
insertProject(almSetting, "repoName-2", "project-2"); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesCount()).isEqualTo(2); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName, | |||
AzureRepo::getSqProjectKey, AzureRepo::getSqProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple("repoName-1", "project-1", "", ""), | |||
tuple("repoName-2", "project-2", projectDto2.getKey(), projectDto2.getName())); | |||
} | |||
@Test | |||
public void search_repos_with_projects_already_set_up_and_no_collision() { | |||
mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("project-1", "repoName-1"), | |||
getGsonAzureRepo("project", "1-repoName-1")))); | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
ProjectDto projectDto1 = insertProject(almSetting, "repoName-1", "project-1"); | |||
ProjectDto projectDto2 = insertProject(almSetting, "1-repoName-1", "project"); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesCount()).isEqualTo(2); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName, | |||
AzureRepo::getSqProjectKey, AzureRepo::getSqProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple("repoName-1", "project-1", projectDto1.getKey(), projectDto1.getName()), | |||
tuple("1-repoName-1", "project", projectDto2.getKey(), projectDto2.getName())); | |||
} | |||
@Test | |||
public void search_repos_with_same_name_and_different_project() { | |||
mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("project-1", "repoName-1"), | |||
getGsonAzureRepo("project-2", "repoName-1")))); | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
ProjectDto projectDto1 = insertProject(almSetting, "repoName-1", "project-1"); | |||
ProjectDto projectDto2 = insertProject(almSetting, "repoName-1", "project-2"); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesCount()).isEqualTo(2); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName, | |||
AzureRepo::getSqProjectKey, AzureRepo::getSqProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple("repoName-1", "project-1", projectDto1.getKey(), projectDto1.getName()), | |||
tuple("repoName-1", "project-2", projectDto2.getKey(), projectDto2.getName())); | |||
} | |||
@Test | |||
public void search_repos_with_project_name() { | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.setParam("projectName", "project-1") | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple("repoName-1", "project-1"), tuple("repoName-2", "project-2")); | |||
} | |||
@Test | |||
public void search_repos_with_project_name_and_empty_criteria() { | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.setParam("projectName", "project-1") | |||
.setParam("searchQuery", "") | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName) | |||
.containsExactlyInAnyOrder( | |||
tuple("repoName-1", "project-1"), tuple("repoName-2", "project-2")); | |||
} | |||
@Test | |||
public void search_and_filter_repos_with_repo_name() { | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.setParam("searchQuery", "repoName-2") | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName) | |||
.containsExactlyInAnyOrder(tuple("repoName-2", "project-2")); | |||
} | |||
@Test | |||
public void search_and_filter_repos_with_matching_repo_and_project_name() { | |||
mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("big-project", "repo-1"), | |||
getGsonAzureRepo("big-project", "repo-2"), | |||
getGsonAzureRepo("big-project", "big-repo"), | |||
getGsonAzureRepo("project", "big-repo"), | |||
getGsonAzureRepo("project", "small-repo")))); | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.setParam("searchQuery", "big") | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesList()) | |||
.extracting(AzureRepo::getName, AzureRepo::getProjectName) | |||
.containsExactlyInAnyOrder(tuple("repo-1", "big-project"), tuple("repo-2", "big-project"), | |||
tuple("big-repo", "big-project"), tuple("big-repo", "project")); | |||
} | |||
@Test | |||
public void return_empty_list_when_there_are_no_azure_repos() { | |||
when(azureDevOpsHttpClient.getRepos(any(), any(), any())).thenReturn(new GsonAzureRepoList(emptyList())); | |||
AlmSettingDto almSetting = insertAlmSetting(); | |||
SearchAzureReposWsResponse response = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()) | |||
.executeProtobuf(SearchAzureReposWsResponse.class); | |||
assertThat(response.getRepositoriesList()).isEmpty(); | |||
} | |||
@Test | |||
public void check_pat_is_missing() { | |||
insertUser(); | |||
AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting(); | |||
TestRequest request = ws.newRequest() | |||
.setParam("almSetting", almSetting.getKey()); | |||
assertThatThrownBy(request::execute) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage("No personal access token found"); | |||
} | |||
@Test | |||
public void fail_check_pat_alm_setting_not_found() { | |||
insertUser(); | |||
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); | |||
} | |||
@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"); | |||
} | |||
private ProjectDto insertProject(AlmSettingDto almSetting, String repoName, String projectName) { | |||
ProjectDto projectDto1 = db.components().insertPrivateProjectDto(); | |||
db.almSettings().insertAzureProjectAlmSetting(almSetting, projectDto1, projectAlmSettingDto -> projectAlmSettingDto.setAlmRepo(repoName), | |||
projectAlmSettingDto -> projectAlmSettingDto.setAlmSlug(projectName)); | |||
return projectDto1; | |||
} | |||
private void mockClient(GsonAzureRepoList repoList) { | |||
when(azureDevOpsHttpClient.getRepos(any(), any(), any())).thenReturn(repoList); | |||
} | |||
private AlmSettingDto insertAlmSetting() { | |||
UserDto user = insertUser(); | |||
AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting(); | |||
db.almPats().insert(dto -> { | |||
dto.setAlmSettingUuid(almSetting.getUuid()); | |||
dto.setUserUuid(user.getUuid()); | |||
dto.setPersonalAccessToken(almSetting.getPersonalAccessToken()); | |||
}); | |||
return almSetting; | |||
} | |||
@NotNull | |||
private UserDto insertUser() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user).addPermission(PROVISION_PROJECTS); | |||
return user; | |||
} | |||
private GsonAzureRepo getGsonAzureRepo(String projectName, String repoName) { | |||
GsonAzureProject project = new GsonAzureProject(projectName, "the best project ever"); | |||
GsonAzureRepo gsonAzureRepo = new GsonAzureRepo("repo-id", repoName, "url", project); | |||
return gsonAzureRepo; | |||
} | |||
} |
@@ -21,6 +21,7 @@ package org.sonar.server.platform.platformlevel; | |||
import java.util.List; | |||
import org.sonar.alm.client.TimeoutConfigurationImpl; | |||
import org.sonar.alm.client.azure.AzureDevOpsHttpClient; | |||
import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient; | |||
import org.sonar.alm.client.gitlab.GitlabHttpClient; | |||
import org.sonar.api.profiles.AnnotationProfileParser; | |||
@@ -494,6 +495,7 @@ public class PlatformLevel4 extends PlatformLevel { | |||
ImportHelper.class, | |||
BitbucketServerRestClient.class, | |||
GitlabHttpClient.class, | |||
AzureDevOpsHttpClient.class, | |||
AlmIntegrationsWSModule.class, | |||
// Branch |