Bladeren bron

SONAR-14371 move Azure Devops client and WS actions to CE

tags/8.7.0.41497
Zipeng WU 3 jaren geleden
bovenliggende
commit
c364b859dd
17 gewijzigde bestanden met toevoegingen van 2025 en 0 verwijderingen
  1. 1
    0
      server/sonar-alm-client/build.gradle
  2. 158
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java
  3. 41
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureError.java
  4. 48
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProject.java
  5. 56
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProjectList.java
  6. 63
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepo.java
  7. 49
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepoList.java
  8. 273
    0
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java
  9. 6
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java
  10. 163
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectAction.java
  11. 115
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ListAzureProjectsAction.java
  12. 235
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/SearchAzureReposAction.java
  13. 23
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/package-info.java
  14. 238
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectActionTest.java
  15. 187
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/azure/ListAzureProjectsActionTest.java
  16. 367
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/azure/SearchAzureReposActionTest.java
  17. 2
    0
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java

+ 1
- 0
server/sonar-alm-client/build.gradle Bestand weergeven

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


+ 158
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java Bestand weergeven

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

+ 41
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureError.java Bestand weergeven

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

+ 48
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProject.java Bestand weergeven

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

+ 56
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProjectList.java Bestand weergeven

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

+ 63
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepo.java Bestand weergeven

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

+ 49
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepoList.java Bestand weergeven

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

}

+ 273
- 0
server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java Bestand weergeven

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

+ 6
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java Bestand weergeven

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

+ 163
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectAction.java Bestand weergeven

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

}

+ 115
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ListAzureProjectsAction.java Bestand weergeven

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

}

+ 235
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/SearchAzureReposAction.java Bestand weergeven

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

}

+ 23
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/package-info.java Bestand weergeven

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

+ 238
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectActionTest.java Bestand weergeven

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

}

+ 187
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/azure/ListAzureProjectsActionTest.java Bestand weergeven

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

+ 367
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/azure/SearchAzureReposActionTest.java Bestand weergeven

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

+ 2
- 0
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java Bestand weergeven

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

Laden…
Annuleren
Opslaan