Przeglądaj źródła

SONAR-20699 Auto-bind project to GitHub DevOps config

tags/10.3.0.82913
Aurelien Poscia 7 miesięcy temu
rodzic
commit
6a8aa6c62e
21 zmienionych plików z 1274 dodań i 552 usunięć
  1. 89
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/AppInstallationToken.java
  2. 14
    1
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
  3. 33
    10
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
  4. 9
    0
      server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubBinding.java
  5. 50
    0
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/AppInstallationTokenTest.java
  6. 413
    347
      server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
  7. 2
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/project/CreationMethod.java
  8. 0
    0
      server/sonar-server-common/src/main/java/org/sonar/server/component/ComponentCreationData.java
  9. 23
    0
      server/sonar-server-common/src/main/java/org/sonar/server/component/package-info.java
  10. 24
    26
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java
  11. 28
    30
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/BranchReportSubmitterIT.java
  12. 123
    19
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java
  13. 20
    78
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java
  14. 18
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java
  15. 6
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java
  16. 135
    9
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java
  17. 9
    5
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/BranchSupport.java
  18. 24
    15
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/ReportSubmitter.java
  19. 231
    8
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java
  20. 19
    3
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/queue/BranchSupportTest.java
  21. 4
    0
      sonar-core/src/main/java/org/sonar/core/ce/package-info.java

+ 89
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/AppInstallationToken.java Wyświetl plik

@@ -0,0 +1,89 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.alm.client.github;

import javax.annotation.concurrent.Immutable;
import org.sonar.alm.client.github.security.AccessToken;

import static java.util.Objects.requireNonNull;

/**
* Token that provides access to the Github API on behalf of
* the Github organization that installed the Github App.
*
* It expires after one hour.
*
* IMPORTANT
* Rate limit is 5'000 API requests per hour for the Github organization.
* Two different Github organizations don't share rate limits.
* Two different instances of {@link AppInstallationToken} of the same Github organization
* share the same quotas (two calls from the two different instances consume
* two hits).
*
* The limit can be higher than 5'000, depending on the number of repositories
* and users present in the organization. See
* https://developer.github.com/apps/building-github-apps/understanding-rate-limits-for-github-apps/
*
* When the token is expired, the rate limit is 60 calls per hour for the public IP
* of the machine. BE CAREFUL, THAT SHOULD NEVER OCCUR.
*
* See https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-an-installation
*/
@Immutable
public class AppInstallationToken implements AccessToken {

private final String token;

public AppInstallationToken(String token) {
this.token = requireNonNull(token, "token can't be null");
}

@Override
public String getValue() {
return token;
}

@Override
public String getAuthorizationHeaderPrefix() {
return "Token";
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AppInstallationToken that = (AppInstallationToken) o;
return token.equals(that.token);
}

@Override
public int hashCode() {
return token.hashCode();
}

@Override
public String toString() {
return token;
}
}

+ 14
- 1
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java Wyświetl plik

@@ -44,6 +44,19 @@ public interface GithubApplicationClient {
*/
UserAccessToken createUserAccessToken(String appUrl, String clientId, String clientSecret, String code);

/**
* Create an installation access token for the specified installation ID.
*
* IMPORTANT: each call consumes one hit of the App GLOBAL quotas (5'000 hits per hour).
*
* Token expires after one hour.
* See https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-an-installation
*
* @return {@code Optional.empty()} if Github is not configured or if token failed to be
* created (network issue, parsing error, Github error, ...).
*/
Optional<AppInstallationToken> createAppInstallationToken(GithubAppConfiguration githubAppConfiguration, long installationId);

GithubBinding.GsonApp getApp(GithubAppConfiguration githubAppConfiguration);

/**
@@ -78,7 +91,7 @@ public interface GithubApplicationClient {
/**
* Returns the repository identified by the repositoryKey owned by the provided organization.
*/
Optional<Repository> getRepository(String appUrl, AccessToken accessToken, String organization, String repositoryKey);
Optional<Repository> getRepository(String appUrl, AccessToken accessToken, String repositoryKey);

class Repositories {
private int total;

+ 33
- 10
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java Wyświetl plik

@@ -71,6 +71,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
protected final GithubAppSecurity appSecurity;
private final GitHubSettings gitHubSettings;
private final GithubPaginatedHttpClient githubPaginatedHttpClient;

public GithubApplicationClientImpl(GithubApplicationHttpClient appHttpClient, GithubAppSecurity appSecurity, GitHubSettings gitHubSettings,
GithubPaginatedHttpClient githubPaginatedHttpClient) {
this.appHttpClient = appHttpClient;
@@ -84,6 +85,25 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
checkArgument(pageSize > 0 && pageSize <= 100, "'pageSize' must be a value larger than 0 and smaller or equal to 100.");
}

@Override
public Optional<AppInstallationToken> createAppInstallationToken(GithubAppConfiguration githubAppConfiguration, long installationId) {
AppToken appToken = appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey());
String endPoint = "/app/installations/" + installationId + "/access_tokens";
return post(githubAppConfiguration.getApiEndpoint(), appToken, endPoint, GithubBinding.GsonInstallationToken.class)
.map(GithubBinding.GsonInstallationToken::getToken)
.filter(Objects::nonNull)
.map(AppInstallationToken::new);
}

private <T> Optional<T> post(String baseUrl, AccessToken token, String endPoint, Class<T> gsonClass) {
try {
GithubApplicationHttpClient.Response response = appHttpClient.post(baseUrl, token, endPoint);
return handleResponse(response, endPoint, gsonClass);
} catch (Exception e) {
LOG.warn(FAILED_TO_REQUEST_BEGIN_MSG + endPoint, e);
return Optional.empty();
}
}

@Override
public void checkApiEndpoint(GithubAppConfiguration githubAppConfiguration) {
@@ -150,7 +170,8 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
AppToken appToken = appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey());
String endpoint = String.format("/repos/%s/installation", repositorySlug);
return get(githubAppConfiguration.getApiEndpoint(), appToken, endpoint, GithubBinding.GsonInstallation.class)
.map(GithubBinding.GsonInstallation::getId);
.map(GithubBinding.GsonInstallation::getId)
.filter(installationId -> installationId != 0L);
}

@Override
@@ -216,7 +237,7 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
} catch (IOException e) {
LOG.warn(FAILED_TO_REQUEST_BEGIN_MSG + endpoint, e);
throw new IllegalStateException("An error occurred when retrieving your GitHup App installations. "
+ "It might be related to your GitHub App configuration or a connectivity problem.");
+ "It might be related to your GitHub App configuration or a connectivity problem.");
}
}

@@ -261,14 +282,17 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
}

@Override
public Optional<Repository> getRepository(String appUrl, AccessToken accessToken, String organization, String repositoryKey) {
public Optional<Repository> getRepository(String appUrl, AccessToken accessToken, String organizationAndRepository) {
try {
GetResponse response = appHttpClient.get(appUrl, accessToken, String.format("/repos/%s", repositoryKey));
return response.getContent()
GetResponse response = appHttpClient.get(appUrl, accessToken, String.format("/repos/%s", organizationAndRepository));
return Optional.of(response)
.filter(r -> r.getCode() == HTTP_OK)
.flatMap(GithubApplicationHttpClient.Response::getContent)
.map(content -> GSON.fromJson(content, GsonGithubRepository.class))
.map(GsonGithubRepository::toRepository);
} catch (Exception e) {
throw new IllegalStateException(format("Failed to get repository '%s' of '%s' accessible by user access token on '%s'", repositoryKey, organization, appUrl), e);
throw new IllegalStateException(format("Failed to get repository '%s' on '%s' (this might be related to the GitHub App installation scope)",
organizationAndRepository, appUrl), e);
}
}

@@ -295,9 +319,9 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {

Optional<String> content = response.getContent();
Optional<UserAccessToken> accessToken = content.flatMap(c -> Arrays.stream(c.split("&"))
.filter(t -> t.startsWith("access_token="))
.map(t -> t.split("=")[1])
.findAny())
.filter(t -> t.startsWith("access_token="))
.map(t -> t.split("=")[1])
.findAny())
.map(UserAccessToken::new);

if (accessToken.isPresent()) {
@@ -331,7 +355,6 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
}
}


protected static <T> Optional<T> handleResponse(GithubApplicationHttpClient.Response response, String endPoint, Class<T> gsonClass) {
try {
return response.getContent().map(c -> GSON.fromJson(c, gsonClass));

+ 9
- 0
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubBinding.java Wyświetl plik

@@ -460,4 +460,13 @@ public class GithubBinding {
return name;
}
}

public static class GsonInstallationToken {
@SerializedName("token")
String token;

public String getToken() {
return token;
}
}
}

+ 50
- 0
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/AppInstallationTokenTest.java Wyświetl plik

@@ -0,0 +1,50 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.alm.client.github;

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class AppInstallationTokenTest {

@Test
public void test_value() {
AppInstallationToken underTest = new AppInstallationToken("foo");

assertThat(underTest.toString())
.isEqualTo(underTest.getValue())
.isEqualTo("foo");
assertThat(underTest.getAuthorizationHeaderPrefix()).isEqualTo("Token");
}

@Test
public void test_equals_hashCode() {
AppInstallationToken foo = new AppInstallationToken("foo");

assertThat(foo.equals(foo)).isTrue();
assertThat(foo.equals(null)).isFalse();
assertThat(foo.equals(new AppInstallationToken("foo"))).isTrue();
assertThat(foo.equals(new AppInstallationToken("bar"))).isFalse();
assertThat(foo.equals("foo")).isFalse();

assertThat(foo).hasSameHashCodeAs(new AppInstallationToken("foo"));
}
}

+ 413
- 347
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java Wyświetl plik

@@ -32,6 +32,7 @@ import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.event.Level;
import org.sonar.alm.client.github.GithubApplicationHttpClient.RateLimit;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.alm.client.github.config.GithubAppInstallation;
@@ -39,11 +40,13 @@ import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.github.security.AppToken;
import org.sonar.alm.client.github.security.GithubAppSecurity;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.api.testfixtures.log.LogAndArguments;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;
import org.sonar.auth.github.GitHubSettings;
import org.sonarqube.ws.client.HttpException;

import static java.net.HttpURLConnection.HTTP_CREATED;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
@@ -53,6 +56,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.groups.Tuple.tuple;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -62,6 +66,7 @@ import static org.sonar.alm.client.github.GithubApplicationHttpClient.GetRespons
@RunWith(DataProviderRunner.class)
public class GithubApplicationClientImplTest {

private static final int INSTALLATION_ID = 1;
private static final String APP_JWT_TOKEN = "APP_TOKEN_JWT";
private static final String PAYLOAD_2_ORGS = """
[
@@ -204,12 +209,12 @@ public class GithubApplicationClientImplTest {
AppToken appToken = mockAppToken();

String json = "{"
+ " \"permissions\": {\n"
+ " \"checks\": \"read\",\n"
+ " \"metadata\": \"read\",\n"
+ " \"pull_requests\": \"read\"\n"
+ " }\n"
+ "}";
+ " \"permissions\": {\n"
+ " \"checks\": \"read\",\n"
+ " \"metadata\": \"read\",\n"
+ " \"pull_requests\": \"read\"\n"
+ " }\n"
+ "}";

when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));

@@ -223,12 +228,12 @@ public class GithubApplicationClientImplTest {
AppToken appToken = mockAppToken();

String json = "{"
+ " \"permissions\": {\n"
+ " \"checks\": \"write\",\n"
+ " \"metadata\": \"read\",\n"
+ " \"pull_requests\": \"write\"\n"
+ " }\n"
+ "}";
+ " \"permissions\": {\n"
+ " \"checks\": \"write\",\n"
+ " \"metadata\": \"read\",\n"
+ " \"pull_requests\": \"write\"\n"
+ " }\n"
+ "}";

when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));

@@ -391,8 +396,8 @@ public class GithubApplicationClientImplTest {
String appUrl = "https://github.sonarsource.com";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
String responseJson = "{\n"
+ " \"total_count\": 0\n"
+ "} ";
+ " \"total_count\": 0\n"
+ "} ";

when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
.thenReturn(new OkGetResponse(responseJson));
@@ -408,82 +413,82 @@ public class GithubApplicationClientImplTest {
String appUrl = "https://github.sonarsource.com";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
String responseJson = "{\n"
+ " \"total_count\": 2,\n"
+ " \"installations\": [\n"
+ " {\n"
+ " \"id\": 1,\n"
+ " \"account\": {\n"
+ " \"login\": \"github\",\n"
+ " \"id\": 1,\n"
+ " \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/orgs/github\",\n"
+ " \"repos_url\": \"https://github.sonarsource.com/api/v3/orgs/github/repos\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/orgs/github/events\",\n"
+ " \"hooks_url\": \"https://github.sonarsource.com/api/v3/orgs/github/hooks\",\n"
+ " \"issues_url\": \"https://github.sonarsource.com/api/v3/orgs/github/issues\",\n"
+ " \"members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/members{/member}\",\n"
+ " \"public_members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/public_members{/member}\",\n"
+ " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
+ " \"description\": \"A great organization\"\n"
+ " },\n"
+ " \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
+ " \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
+ " \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
+ " \"app_id\": 1,\n"
+ " \"target_id\": 1,\n"
+ " \"target_type\": \"Organization\",\n"
+ " \"permissions\": {\n"
+ " \"checks\": \"write\",\n"
+ " \"metadata\": \"read\",\n"
+ " \"contents\": \"read\"\n"
+ " },\n"
+ " \"events\": [\n"
+ " \"push\",\n"
+ " \"pull_request\"\n"
+ " ],\n"
+ " \"single_file_name\": \"config.yml\"\n"
+ " },\n"
+ " {\n"
+ " \"id\": 3,\n"
+ " \"account\": {\n"
+ " \"login\": \"octocat\",\n"
+ " \"id\": 2,\n"
+ " \"node_id\": \"MDQ6VXNlcjE=\",\n"
+ " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
+ " \"html_url\": \"https://github.com/octocat\",\n"
+ " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
+ " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
+ " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
+ " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
+ " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
+ " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
+ " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
+ " \"type\": \"User\",\n"
+ " \"site_admin\": false\n"
+ " },\n"
+ " \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
+ " \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
+ " \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
+ " \"app_id\": 1,\n"
+ " \"target_id\": 1,\n"
+ " \"target_type\": \"Organization\",\n"
+ " \"permissions\": {\n"
+ " \"checks\": \"write\",\n"
+ " \"metadata\": \"read\",\n"
+ " \"contents\": \"read\"\n"
+ " },\n"
+ " \"events\": [\n"
+ " \"push\",\n"
+ " \"pull_request\"\n"
+ " ],\n"
+ " \"single_file_name\": \"config.yml\"\n"
+ " }\n"
+ " ]\n"
+ "} ";
+ " \"total_count\": 2,\n"
+ " \"installations\": [\n"
+ " {\n"
+ " \"id\": 1,\n"
+ " \"account\": {\n"
+ " \"login\": \"github\",\n"
+ " \"id\": 1,\n"
+ " \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/orgs/github\",\n"
+ " \"repos_url\": \"https://github.sonarsource.com/api/v3/orgs/github/repos\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/orgs/github/events\",\n"
+ " \"hooks_url\": \"https://github.sonarsource.com/api/v3/orgs/github/hooks\",\n"
+ " \"issues_url\": \"https://github.sonarsource.com/api/v3/orgs/github/issues\",\n"
+ " \"members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/members{/member}\",\n"
+ " \"public_members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/public_members{/member}\",\n"
+ " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
+ " \"description\": \"A great organization\"\n"
+ " },\n"
+ " \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
+ " \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
+ " \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
+ " \"app_id\": 1,\n"
+ " \"target_id\": 1,\n"
+ " \"target_type\": \"Organization\",\n"
+ " \"permissions\": {\n"
+ " \"checks\": \"write\",\n"
+ " \"metadata\": \"read\",\n"
+ " \"contents\": \"read\"\n"
+ " },\n"
+ " \"events\": [\n"
+ " \"push\",\n"
+ " \"pull_request\"\n"
+ " ],\n"
+ " \"single_file_name\": \"config.yml\"\n"
+ " },\n"
+ " {\n"
+ " \"id\": 3,\n"
+ " \"account\": {\n"
+ " \"login\": \"octocat\",\n"
+ " \"id\": 2,\n"
+ " \"node_id\": \"MDQ6VXNlcjE=\",\n"
+ " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
+ " \"html_url\": \"https://github.com/octocat\",\n"
+ " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
+ " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
+ " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
+ " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
+ " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
+ " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
+ " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
+ " \"type\": \"User\",\n"
+ " \"site_admin\": false\n"
+ " },\n"
+ " \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
+ " \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
+ " \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
+ " \"app_id\": 1,\n"
+ " \"target_id\": 1,\n"
+ " \"target_type\": \"Organization\",\n"
+ " \"permissions\": {\n"
+ " \"checks\": \"write\",\n"
+ " \"metadata\": \"read\",\n"
+ " \"contents\": \"read\"\n"
+ " },\n"
+ " \"events\": [\n"
+ " \"push\",\n"
+ " \"pull_request\"\n"
+ " ],\n"
+ " \"single_file_name\": \"config.yml\"\n"
+ " }\n"
+ " ]\n"
+ "} ";

when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
.thenReturn(new OkGetResponse(responseJson));
@@ -565,12 +570,12 @@ public class GithubApplicationClientImplTest {
public void getWhitelistedGithubAppInstallations_whenGithubReturnsError_shouldThrow() throws IOException {
AppToken appToken = new AppToken(APP_JWT_TOKEN);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
when(githubPaginatedHttpClient.get(any(),any(),any(),any())).thenThrow(new IOException("io exception"));
when(githubPaginatedHttpClient.get(any(), any(), any(), any())).thenThrow(new IOException("io exception"));

assertThatThrownBy(() -> underTest.getWhitelistedGithubAppInstallations(githubAppConfiguration))
.isInstanceOf(IllegalStateException.class)
.hasMessage("An error occurred when retrieving your GitHup App installations. "
+ "It might be related to your GitHub App configuration or a connectivity problem.");
+ "It might be related to your GitHub App configuration or a connectivity problem.");
}

@Test
@@ -610,8 +615,8 @@ public class GithubApplicationClientImplTest {
String appUrl = "https://github.sonarsource.com";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
String responseJson = "{\n"
+ " \"total_count\": 0\n"
+ "}";
+ " \"total_count\": 0\n"
+ "}";

when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
.thenReturn(new OkGetResponse(responseJson));
@@ -627,79 +632,79 @@ public class GithubApplicationClientImplTest {
String appUrl = "https://github.sonarsource.com";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
String responseJson = "{\n"
+ " \"total_count\": 2,\n"
+ " \"incomplete_results\": false,\n"
+ " \"items\": [\n"
+ " {\n"
+ " \"id\": 3081286,\n"
+ " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
+ " \"name\": \"HelloWorld\",\n"
+ " \"full_name\": \"github/HelloWorld\",\n"
+ " \"owner\": {\n"
+ " \"login\": \"github\",\n"
+ " \"id\": 872147,\n"
+ " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
+ " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
+ " \"type\": \"User\"\n"
+ " },\n"
+ " \"private\": false,\n"
+ " \"html_url\": \"https://github.com/github/HelloWorld\",\n"
+ " \"description\": \"A C implementation of HelloWorld\",\n"
+ " \"fork\": false,\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
+ " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
+ " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
+ " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
+ " \"homepage\": \"\",\n"
+ " \"size\": 524,\n"
+ " \"stargazers_count\": 1,\n"
+ " \"watchers_count\": 1,\n"
+ " \"language\": \"Assembly\",\n"
+ " \"forks_count\": 0,\n"
+ " \"open_issues_count\": 0,\n"
+ " \"master_branch\": \"master\",\n"
+ " \"default_branch\": \"master\",\n"
+ " \"score\": 1.0\n"
+ " },\n"
+ " {\n"
+ " \"id\": 3081286,\n"
+ " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
+ " \"name\": \"HelloUniverse\",\n"
+ " \"full_name\": \"github/HelloUniverse\",\n"
+ " \"owner\": {\n"
+ " \"login\": \"github\",\n"
+ " \"id\": 872147,\n"
+ " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
+ " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
+ " \"type\": \"User\"\n"
+ " },\n"
+ " \"private\": false,\n"
+ " \"html_url\": \"https://github.com/github/HelloUniverse\",\n"
+ " \"description\": \"A C implementation of HelloUniverse\",\n"
+ " \"fork\": false,\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloUniverse\",\n"
+ " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
+ " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
+ " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
+ " \"homepage\": \"\",\n"
+ " \"size\": 524,\n"
+ " \"stargazers_count\": 1,\n"
+ " \"watchers_count\": 1,\n"
+ " \"language\": \"Assembly\",\n"
+ " \"forks_count\": 0,\n"
+ " \"open_issues_count\": 0,\n"
+ " \"master_branch\": \"master\",\n"
+ " \"default_branch\": \"master\",\n"
+ " \"score\": 1.0\n"
+ " }\n"
+ " ]\n"
+ "}";
+ " \"total_count\": 2,\n"
+ " \"incomplete_results\": false,\n"
+ " \"items\": [\n"
+ " {\n"
+ " \"id\": 3081286,\n"
+ " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
+ " \"name\": \"HelloWorld\",\n"
+ " \"full_name\": \"github/HelloWorld\",\n"
+ " \"owner\": {\n"
+ " \"login\": \"github\",\n"
+ " \"id\": 872147,\n"
+ " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
+ " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
+ " \"type\": \"User\"\n"
+ " },\n"
+ " \"private\": false,\n"
+ " \"html_url\": \"https://github.com/github/HelloWorld\",\n"
+ " \"description\": \"A C implementation of HelloWorld\",\n"
+ " \"fork\": false,\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
+ " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
+ " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
+ " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
+ " \"homepage\": \"\",\n"
+ " \"size\": 524,\n"
+ " \"stargazers_count\": 1,\n"
+ " \"watchers_count\": 1,\n"
+ " \"language\": \"Assembly\",\n"
+ " \"forks_count\": 0,\n"
+ " \"open_issues_count\": 0,\n"
+ " \"master_branch\": \"master\",\n"
+ " \"default_branch\": \"master\",\n"
+ " \"score\": 1.0\n"
+ " },\n"
+ " {\n"
+ " \"id\": 3081286,\n"
+ " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
+ " \"name\": \"HelloUniverse\",\n"
+ " \"full_name\": \"github/HelloUniverse\",\n"
+ " \"owner\": {\n"
+ " \"login\": \"github\",\n"
+ " \"id\": 872147,\n"
+ " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
+ " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
+ " \"type\": \"User\"\n"
+ " },\n"
+ " \"private\": false,\n"
+ " \"html_url\": \"https://github.com/github/HelloUniverse\",\n"
+ " \"description\": \"A C implementation of HelloUniverse\",\n"
+ " \"fork\": false,\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloUniverse\",\n"
+ " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
+ " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
+ " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
+ " \"homepage\": \"\",\n"
+ " \"size\": 524,\n"
+ " \"stargazers_count\": 1,\n"
+ " \"watchers_count\": 1,\n"
+ " \"language\": \"Assembly\",\n"
+ " \"forks_count\": 0,\n"
+ " \"open_issues_count\": 0,\n"
+ " \"master_branch\": \"master\",\n"
+ " \"default_branch\": \"master\",\n"
+ " \"score\": 1.0\n"
+ " }\n"
+ " ]\n"
+ "}";

when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
.thenReturn(new OkGetResponse(responseJson));
@@ -716,45 +721,45 @@ public class GithubApplicationClientImplTest {
String appUrl = "https://github.sonarsource.com";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
String responseJson = "{\n"
+ " \"total_count\": 2,\n"
+ " \"incomplete_results\": false,\n"
+ " \"items\": [\n"
+ " {\n"
+ " \"id\": 3081286,\n"
+ " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
+ " \"name\": \"HelloWorld\",\n"
+ " \"full_name\": \"github/HelloWorld\",\n"
+ " \"owner\": {\n"
+ " \"login\": \"github\",\n"
+ " \"id\": 872147,\n"
+ " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
+ " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
+ " \"type\": \"User\"\n"
+ " },\n"
+ " \"private\": false,\n"
+ " \"html_url\": \"https://github.com/github/HelloWorld\",\n"
+ " \"description\": \"A C implementation of HelloWorld\",\n"
+ " \"fork\": false,\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
+ " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
+ " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
+ " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
+ " \"homepage\": \"\",\n"
+ " \"size\": 524,\n"
+ " \"stargazers_count\": 1,\n"
+ " \"watchers_count\": 1,\n"
+ " \"language\": \"Assembly\",\n"
+ " \"forks_count\": 0,\n"
+ " \"open_issues_count\": 0,\n"
+ " \"master_branch\": \"master\",\n"
+ " \"default_branch\": \"master\",\n"
+ " \"score\": 1.0\n"
+ " }\n"
+ " ]\n"
+ "}";
+ " \"total_count\": 2,\n"
+ " \"incomplete_results\": false,\n"
+ " \"items\": [\n"
+ " {\n"
+ " \"id\": 3081286,\n"
+ " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
+ " \"name\": \"HelloWorld\",\n"
+ " \"full_name\": \"github/HelloWorld\",\n"
+ " \"owner\": {\n"
+ " \"login\": \"github\",\n"
+ " \"id\": 872147,\n"
+ " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
+ " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
+ " \"type\": \"User\"\n"
+ " },\n"
+ " \"private\": false,\n"
+ " \"html_url\": \"https://github.com/github/HelloWorld\",\n"
+ " \"description\": \"A C implementation of HelloWorld\",\n"
+ " \"fork\": false,\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
+ " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
+ " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
+ " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
+ " \"homepage\": \"\",\n"
+ " \"size\": 524,\n"
+ " \"stargazers_count\": 1,\n"
+ " \"watchers_count\": 1,\n"
+ " \"language\": \"Assembly\",\n"
+ " \"forks_count\": 0,\n"
+ " \"open_issues_count\": 0,\n"
+ " \"master_branch\": \"master\",\n"
+ " \"default_branch\": \"master\",\n"
+ " \"score\": 1.0\n"
+ " }\n"
+ " ]\n"
+ "}";

when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "world+fork:true+org:github", 1, 100)))
.thenReturn(new GetResponse() {
@@ -792,7 +797,7 @@ public class GithubApplicationClientImplTest {
when(httpClient.get(any(), any(), any()))
.thenReturn(new Response(404, null));

Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, new UserAccessToken("temp"), "octocat", "octocat/Hello-World");
Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, new UserAccessToken("temp"), "octocat/Hello-World");

assertThat(repository).isEmpty();
}
@@ -800,15 +805,14 @@ public class GithubApplicationClientImplTest {
@Test
public void getRepository_fails_on_failure() throws IOException {
String repositoryKey = "octocat/Hello-World";
String organization = "octocat";

when(httpClient.get(any(), any(), any()))
.thenThrow(new IOException("OOPS"));

UserAccessToken token = new UserAccessToken("temp");
assertThatThrownBy(() -> underTest.getRepository(appUrl, token, organization, repositoryKey))
assertThatThrownBy(() -> underTest.getRepository(appUrl, token, repositoryKey))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Failed to get repository '%s' of '%s' accessible by user access token on '%s'", repositoryKey, organization, appUrl);
.hasMessage("Failed to get repository 'octocat/Hello-World' on 'Any URL' (this might be related to the GitHub App installation scope)");
}

@Test
@@ -816,142 +820,142 @@ public class GithubApplicationClientImplTest {
String appUrl = "https://github.sonarsource.com";
AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
String responseJson = "{\n"
+ " \"id\": 1296269,\n"
+ " \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n"
+ " \"name\": \"Hello-World\",\n"
+ " \"full_name\": \"octocat/Hello-World\",\n"
+ " \"owner\": {\n"
+ " \"login\": \"octocat\",\n"
+ " \"id\": 1,\n"
+ " \"node_id\": \"MDQ6VXNlcjE=\",\n"
+ " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
+ " \"html_url\": \"https://github.com/octocat\",\n"
+ " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
+ " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
+ " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
+ " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
+ " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
+ " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
+ " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
+ " \"type\": \"User\",\n"
+ " \"site_admin\": false\n"
+ " },\n"
+ " \"private\": false,\n"
+ " \"html_url\": \"https://github.com/octocat/Hello-World\",\n"
+ " \"description\": \"This your first repo!\",\n"
+ " \"fork\": false,\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World\",\n"
+ " \"archive_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/{archive_format}{/ref}\",\n"
+ " \"assignees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/assignees{/user}\",\n"
+ " \"blobs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/blobs{/sha}\",\n"
+ " \"branches_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/branches{/branch}\",\n"
+ " \"collaborators_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/collaborators{/collaborator}\",\n"
+ " \"comments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/comments{/number}\",\n"
+ " \"commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/commits{/sha}\",\n"
+ " \"compare_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/compare/{base}...{head}\",\n"
+ " \"contents_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contents/{+path}\",\n"
+ " \"contributors_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contributors\",\n"
+ " \"deployments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/deployments\",\n"
+ " \"downloads_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/downloads\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/events\",\n"
+ " \"forks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/forks\",\n"
+ " \"git_commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/commits{/sha}\",\n"
+ " \"git_refs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/refs{/sha}\",\n"
+ " \"git_tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/tags{/sha}\",\n"
+ " \"git_url\": \"git:github.com/octocat/Hello-World.git\",\n"
+ " \"issue_comment_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/comments{/number}\",\n"
+ " \"issue_events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/events{/number}\",\n"
+ " \"issues_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues{/number}\",\n"
+ " \"keys_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/keys{/key_id}\",\n"
+ " \"labels_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/labels{/name}\",\n"
+ " \"languages_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/languages\",\n"
+ " \"merges_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/merges\",\n"
+ " \"milestones_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/milestones{/number}\",\n"
+ " \"notifications_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/notifications{?since,all,participating}\",\n"
+ " \"pulls_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/pulls{/number}\",\n"
+ " \"releases_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/releases{/id}\",\n"
+ " \"ssh_url\": \"git@github.com:octocat/Hello-World.git\",\n"
+ " \"stargazers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/stargazers\",\n"
+ " \"statuses_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/statuses/{sha}\",\n"
+ " \"subscribers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscribers\",\n"
+ " \"subscription_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscription\",\n"
+ " \"tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/tags\",\n"
+ " \"teams_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/teams\",\n"
+ " \"trees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/trees{/sha}\",\n"
+ " \"clone_url\": \"https://github.com/octocat/Hello-World.git\",\n"
+ " \"mirror_url\": \"git:git.example.com/octocat/Hello-World\",\n"
+ " \"hooks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/hooks\",\n"
+ " \"svn_url\": \"https://svn.github.com/octocat/Hello-World\",\n"
+ " \"homepage\": \"https://github.com\",\n"
+ " \"language\": null,\n"
+ " \"forks_count\": 9,\n"
+ " \"stargazers_count\": 80,\n"
+ " \"watchers_count\": 80,\n"
+ " \"size\": 108,\n"
+ " \"default_branch\": \"master\",\n"
+ " \"open_issues_count\": 0,\n"
+ " \"is_template\": true,\n"
+ " \"topics\": [\n"
+ " \"octocat\",\n"
+ " \"atom\",\n"
+ " \"electron\",\n"
+ " \"api\"\n"
+ " ],\n"
+ " \"has_issues\": true,\n"
+ " \"has_projects\": true,\n"
+ " \"has_wiki\": true,\n"
+ " \"has_pages\": false,\n"
+ " \"has_downloads\": true,\n"
+ " \"archived\": false,\n"
+ " \"disabled\": false,\n"
+ " \"visibility\": \"public\",\n"
+ " \"pushed_at\": \"2011-01-26T19:06:43Z\",\n"
+ " \"created_at\": \"2011-01-26T19:01:12Z\",\n"
+ " \"updated_at\": \"2011-01-26T19:14:43Z\",\n"
+ " \"permissions\": {\n"
+ " \"admin\": false,\n"
+ " \"push\": false,\n"
+ " \"pull\": true\n"
+ " },\n"
+ " \"allow_rebase_merge\": true,\n"
+ " \"template_repository\": null,\n"
+ " \"allow_squash_merge\": true,\n"
+ " \"allow_merge_commit\": true,\n"
+ " \"subscribers_count\": 42,\n"
+ " \"network_count\": 0,\n"
+ " \"anonymous_access_enabled\": false,\n"
+ " \"license\": {\n"
+ " \"key\": \"mit\",\n"
+ " \"name\": \"MIT License\",\n"
+ " \"spdx_id\": \"MIT\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/licenses/mit\",\n"
+ " \"node_id\": \"MDc6TGljZW5zZW1pdA==\"\n"
+ " },\n"
+ " \"organization\": {\n"
+ " \"login\": \"octocat\",\n"
+ " \"id\": 1,\n"
+ " \"node_id\": \"MDQ6VXNlcjE=\",\n"
+ " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
+ " \"html_url\": \"https://github.com/octocat\",\n"
+ " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
+ " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
+ " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
+ " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
+ " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
+ " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
+ " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
+ " \"type\": \"Organization\",\n"
+ " \"site_admin\": false\n"
+ " }"
+ "}";
+ " \"id\": 1296269,\n"
+ " \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n"
+ " \"name\": \"Hello-World\",\n"
+ " \"full_name\": \"octocat/Hello-World\",\n"
+ " \"owner\": {\n"
+ " \"login\": \"octocat\",\n"
+ " \"id\": 1,\n"
+ " \"node_id\": \"MDQ6VXNlcjE=\",\n"
+ " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
+ " \"html_url\": \"https://github.com/octocat\",\n"
+ " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
+ " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
+ " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
+ " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
+ " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
+ " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
+ " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
+ " \"type\": \"User\",\n"
+ " \"site_admin\": false\n"
+ " },\n"
+ " \"private\": false,\n"
+ " \"html_url\": \"https://github.com/octocat/Hello-World\",\n"
+ " \"description\": \"This your first repo!\",\n"
+ " \"fork\": false,\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World\",\n"
+ " \"archive_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/{archive_format}{/ref}\",\n"
+ " \"assignees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/assignees{/user}\",\n"
+ " \"blobs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/blobs{/sha}\",\n"
+ " \"branches_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/branches{/branch}\",\n"
+ " \"collaborators_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/collaborators{/collaborator}\",\n"
+ " \"comments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/comments{/number}\",\n"
+ " \"commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/commits{/sha}\",\n"
+ " \"compare_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/compare/{base}...{head}\",\n"
+ " \"contents_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contents/{+path}\",\n"
+ " \"contributors_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contributors\",\n"
+ " \"deployments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/deployments\",\n"
+ " \"downloads_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/downloads\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/events\",\n"
+ " \"forks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/forks\",\n"
+ " \"git_commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/commits{/sha}\",\n"
+ " \"git_refs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/refs{/sha}\",\n"
+ " \"git_tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/tags{/sha}\",\n"
+ " \"git_url\": \"git:github.com/octocat/Hello-World.git\",\n"
+ " \"issue_comment_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/comments{/number}\",\n"
+ " \"issue_events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/events{/number}\",\n"
+ " \"issues_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues{/number}\",\n"
+ " \"keys_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/keys{/key_id}\",\n"
+ " \"labels_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/labels{/name}\",\n"
+ " \"languages_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/languages\",\n"
+ " \"merges_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/merges\",\n"
+ " \"milestones_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/milestones{/number}\",\n"
+ " \"notifications_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/notifications{?since,all,participating}\",\n"
+ " \"pulls_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/pulls{/number}\",\n"
+ " \"releases_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/releases{/id}\",\n"
+ " \"ssh_url\": \"git@github.com:octocat/Hello-World.git\",\n"
+ " \"stargazers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/stargazers\",\n"
+ " \"statuses_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/statuses/{sha}\",\n"
+ " \"subscribers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscribers\",\n"
+ " \"subscription_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscription\",\n"
+ " \"tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/tags\",\n"
+ " \"teams_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/teams\",\n"
+ " \"trees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/trees{/sha}\",\n"
+ " \"clone_url\": \"https://github.com/octocat/Hello-World.git\",\n"
+ " \"mirror_url\": \"git:git.example.com/octocat/Hello-World\",\n"
+ " \"hooks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/hooks\",\n"
+ " \"svn_url\": \"https://svn.github.com/octocat/Hello-World\",\n"
+ " \"homepage\": \"https://github.com\",\n"
+ " \"language\": null,\n"
+ " \"forks_count\": 9,\n"
+ " \"stargazers_count\": 80,\n"
+ " \"watchers_count\": 80,\n"
+ " \"size\": 108,\n"
+ " \"default_branch\": \"master\",\n"
+ " \"open_issues_count\": 0,\n"
+ " \"is_template\": true,\n"
+ " \"topics\": [\n"
+ " \"octocat\",\n"
+ " \"atom\",\n"
+ " \"electron\",\n"
+ " \"api\"\n"
+ " ],\n"
+ " \"has_issues\": true,\n"
+ " \"has_projects\": true,\n"
+ " \"has_wiki\": true,\n"
+ " \"has_pages\": false,\n"
+ " \"has_downloads\": true,\n"
+ " \"archived\": false,\n"
+ " \"disabled\": false,\n"
+ " \"visibility\": \"public\",\n"
+ " \"pushed_at\": \"2011-01-26T19:06:43Z\",\n"
+ " \"created_at\": \"2011-01-26T19:01:12Z\",\n"
+ " \"updated_at\": \"2011-01-26T19:14:43Z\",\n"
+ " \"permissions\": {\n"
+ " \"admin\": false,\n"
+ " \"push\": false,\n"
+ " \"pull\": true\n"
+ " },\n"
+ " \"allow_rebase_merge\": true,\n"
+ " \"template_repository\": null,\n"
+ " \"allow_squash_merge\": true,\n"
+ " \"allow_merge_commit\": true,\n"
+ " \"subscribers_count\": 42,\n"
+ " \"network_count\": 0,\n"
+ " \"anonymous_access_enabled\": false,\n"
+ " \"license\": {\n"
+ " \"key\": \"mit\",\n"
+ " \"name\": \"MIT License\",\n"
+ " \"spdx_id\": \"MIT\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/licenses/mit\",\n"
+ " \"node_id\": \"MDc6TGljZW5zZW1pdA==\"\n"
+ " },\n"
+ " \"organization\": {\n"
+ " \"login\": \"octocat\",\n"
+ " \"id\": 1,\n"
+ " \"node_id\": \"MDQ6VXNlcjE=\",\n"
+ " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
+ " \"gravatar_id\": \"\",\n"
+ " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
+ " \"html_url\": \"https://github.com/octocat\",\n"
+ " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
+ " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
+ " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
+ " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
+ " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
+ " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
+ " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
+ " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
+ " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
+ " \"type\": \"Organization\",\n"
+ " \"site_admin\": false\n"
+ " }"
+ "}";

when(httpClient.get(appUrl, accessToken, "/repos/octocat/Hello-World"))
.thenReturn(new GetResponse() {
@@ -976,7 +980,7 @@ public class GithubApplicationClientImplTest {
}
});

Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, accessToken, "octocat", "octocat/Hello-World");
Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, accessToken, "octocat/Hello-World");

assertThat(repository)
.isPresent()
@@ -986,12 +990,74 @@ public class GithubApplicationClientImplTest {
.containsOnly(1296269L, "Hello-World", "octocat/Hello-World", "https://github.com/octocat/Hello-World", false, "master");
}

@Test
public void createAppInstallationToken_throws_IAE_if_application_token_cant_be_created() {
mockNoApplicationJwtToken();

assertThatThrownBy(() -> underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID))
.isInstanceOf(IllegalArgumentException.class);
}

private void mockNoApplicationJwtToken() {
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenThrow(IllegalArgumentException.class);
}

@Test
public void createAppInstallationToken_returns_empty_if_post_throws_IOE() throws IOException {
mockAppToken();
when(httpClient.post(anyString(), any(AccessToken.class), anyString())).thenThrow(IOException.class);
Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);

assertThat(accessToken).isEmpty();
assertThat(logTester.getLogs(Level.WARN)).extracting(LogAndArguments::getRawMsg).anyMatch(s -> s.startsWith("Failed to request"));
}

@Test
public void createAppInstallationToken_returns_empty_if_access_token_cant_be_created() throws IOException {
AppToken appToken = mockAppToken();
mockAccessTokenCallingGithubFailure();

Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);

assertThat(accessToken).isEmpty();
verify(httpClient).post(appUrl, appToken, "/app/installations/" + INSTALLATION_ID + "/access_tokens");
}

@Test
public void createAppInstallationToken_from_installation_id_returns_access_token() throws IOException {
AppToken appToken = mockAppToken();
AppInstallationToken installToken = mockCreateAccessTokenCallingGithub();

Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);

assertThat(accessToken).hasValue(installToken);
verify(httpClient).post(appUrl, appToken, "/app/installations/" + INSTALLATION_ID + "/access_tokens");
}

private void mockAccessTokenCallingGithubFailure() throws IOException {
Response response = mock(Response.class);
when(response.getContent()).thenReturn(Optional.empty());
when(response.getCode()).thenReturn(HTTP_UNAUTHORIZED);
when(httpClient.post(eq(appUrl), any(AppToken.class), eq("/app/installations/" + INSTALLATION_ID + "/access_tokens"))).thenReturn(response);
}

private AppToken mockAppToken() {
String jwt = randomAlphanumeric(5);
when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(new AppToken(jwt));
return new AppToken(jwt);
}

private AppInstallationToken mockCreateAccessTokenCallingGithub() throws IOException {
String token = randomAlphanumeric(5);
Response response = mock(Response.class);
when(response.getContent()).thenReturn(Optional.of("{" +
" \"token\": \"" + token + "\"" +
"}"));
when(response.getCode()).thenReturn(HTTP_CREATED);
when(httpClient.post(eq(appUrl), any(AppToken.class), eq("/app/installations/" + INSTALLATION_ID + "/access_tokens"))).thenReturn(response);
return new AppInstallationToken(token);
}

private static class OkGetResponse extends Response {
private OkGetResponse(String content) {
super(200, content);

+ 2
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/project/CreationMethod.java Wyświetl plik

@@ -27,7 +27,8 @@ public enum CreationMethod {
LOCAL_BROWSER(Category.LOCAL, true),
ALM_IMPORT_API(Category.ALM_IMPORT, false),
ALM_IMPORT_BROWSER(Category.ALM_IMPORT, true),
SCANNER_API(Category.SCANNER, false);
SCANNER_API(Category.SCANNER, false),
SCANNER_API_DEVOPS_AUTO_CONFIG(Category.SCANNER, false);

private final boolean isCreatedViaBrowser;
private final Category category;

server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentCreationData.java → server/sonar-server-common/src/main/java/org/sonar/server/component/ComponentCreationData.java Wyświetl plik


+ 23
- 0
server/sonar-server-common/src/main/java/org/sonar/server/component/package-info.java Wyświetl plik

@@ -0,0 +1,23 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
@ParametersAreNonnullByDefault
package org.sonar.server.component;

import javax.annotation.ParametersAreNonnullByDefault;

+ 24
- 26
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java Wyświetl plik

@@ -48,6 +48,7 @@ import org.sonar.db.project.ProjectDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.almintegration.ws.ImportHelper;
import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
import org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService;
import org.sonar.server.component.ComponentUpdater;
import org.sonar.server.es.EsTester;
import org.sonar.server.es.IndexersImpl;
@@ -98,6 +99,7 @@ import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_COD
public class ImportGithubProjectActionIT {

private static final String PROJECT_KEY_NAME = "PROJECT_NAME";
private static final String GENERATED_PROJECT_KEY = "generated_" + PROJECT_KEY_NAME;

@Rule
public UserSessionRule userSession = standalone();
@@ -122,15 +124,16 @@ public class ImportGithubProjectActionIT {
private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
private final PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);

private final GitHubSettings gitHubSettings = mock(GitHubSettings.class);
private NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider);
private final NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider);

private final ManagedProjectService managedProjectService = mock(ManagedProjectService.class);
private final GitHubDevOpsPlatformService gitHubDevOpsPlatformService = new GitHubDevOpsPlatformService(db.getDbClient(),
null, appClient, projectDefaultVisibility, projectKeyGenerator, userSession, componentUpdater, gitHubSettings);
private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(db.getDbClient(), managedProjectService, userSession,
projectDefaultVisibility, appClient, componentUpdater, importHelper, projectKeyGenerator, newCodeDefinitionResolver,
defaultBranchNameResolver, gitHubSettings));
componentUpdater, importHelper, newCodeDefinitionResolver, defaultBranchNameResolver, gitHubDevOpsPlatformService));

@Before
public void before() {
@@ -147,7 +150,7 @@ public class ImportGithubProjectActionIT {
Projects.CreateWsResponse response = callWebService(githubAlmSetting);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);
assertThat(result.getName()).isEqualTo(repository.getName());

Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
@@ -157,7 +160,7 @@ public class ImportGithubProjectActionIT {
assertThat(mainBranch).isPresent();
assertThat(mainBranch.get().getKey()).isEqualTo("default-branch");

verify(managedProjectService).queuePermissionSyncTask(userSession.getUuid(), mainBranch.get().getUuid() , projectDto.get().getUuid());
verify(managedProjectService).queuePermissionSyncTask(userSession.getUuid(), mainBranch.get().getUuid(), projectDto.get().getUuid());
}

@Test
@@ -177,11 +180,12 @@ public class ImportGithubProjectActionIT {
.executeProtobuf(Projects.CreateWsResponse.class);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);

Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
assertThat(projectDto).isPresent();

assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), projectDto.get().getUuid()))
assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), projectDto.get().getUuid()))
.isPresent()
.get()
.extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue, NewCodePeriodDto::getBranchUuid)
@@ -205,6 +209,7 @@ public class ImportGithubProjectActionIT {
.executeProtobuf(Projects.CreateWsResponse.class);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);

Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
assertThat(projectDto).isPresent();
@@ -225,11 +230,7 @@ public class ImportGithubProjectActionIT {

AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();

GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, PROJECT_KEY_NAME, false,
"octocat/" + PROJECT_KEY_NAME,
"https://github.sonarsource.com/api/v3/repos/octocat/" + PROJECT_KEY_NAME, null);
when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
mockGithubInteractions();

Projects.CreateWsResponse response = ws.newRequest()
.setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
@@ -239,6 +240,7 @@ public class ImportGithubProjectActionIT {
.executeProtobuf(Projects.CreateWsResponse.class);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);

Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
assertThat(projectDto).isPresent();
@@ -256,11 +258,7 @@ public class ImportGithubProjectActionIT {

AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();

GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, PROJECT_KEY_NAME, false,
"octocat/" + PROJECT_KEY_NAME,
"https://github.sonarsource.com/api/v3/repos/octocat/" + PROJECT_KEY_NAME, "mainBranch");
when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
mockGithubInteractions();

Projects.CreateWsResponse response = ws.newRequest()
.setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
@@ -270,6 +268,7 @@ public class ImportGithubProjectActionIT {
.executeProtobuf(Projects.CreateWsResponse.class);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);

Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
assertThat(projectDto).isPresent();
@@ -278,7 +277,7 @@ public class ImportGithubProjectActionIT {
.isPresent()
.get()
.extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue)
.containsExactly(REFERENCE_BRANCH, "mainBranch");
.containsExactly(REFERENCE_BRANCH, "default-branch");
}

@Test
@@ -286,10 +285,7 @@ public class ImportGithubProjectActionIT {
AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
db.components().insertPublicProject(p -> p.setKey("Hello-World")).getMainBranchComponent();

GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, "Hello-World", false, "Hello-World",
"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World", "main");
when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
GithubApplicationClient.Repository repository = mockGithubInteractions();

Projects.CreateWsResponse response = ws.newRequest()
.setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
@@ -298,7 +294,7 @@ public class ImportGithubProjectActionIT {
.executeProtobuf(Projects.CreateWsResponse.class);

Projects.CreateWsResponse.Project result = response.getProject();
assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);
assertThat(result.getName()).isEqualTo(repository.getName());
}

@@ -318,7 +314,7 @@ public class ImportGithubProjectActionIT {
ArgumentCaptor<EntityDto> projectDtoArgumentCaptor = ArgumentCaptor.forClass(EntityDto.class);
verify(permissionTemplateService).applyDefaultToNewComponent(any(DbSession.class), projectDtoArgumentCaptor.capture(), eq(userSession.getUuid()));
String projectKey = projectDtoArgumentCaptor.getValue().getKey();
assertThat(projectKey).isEqualTo(PROJECT_KEY_NAME);
assertThat(projectKey).isEqualTo(GENERATED_PROJECT_KEY);

}

@@ -346,6 +342,7 @@ public class ImportGithubProjectActionIT {

Projects.CreateWsResponse response = callWebService(githubAlmSetting);

assertThat(response.getProject().getKey()).isEqualTo(GENERATED_PROJECT_KEY);
Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), response.getProject().getKey());
assertThat(projectDto.orElseThrow().getCreationMethod()).isEqualTo(CreationMethod.ALM_IMPORT_API);
}
@@ -358,6 +355,7 @@ public class ImportGithubProjectActionIT {

Projects.CreateWsResponse response = callWebService(githubAlmSetting);

assertThat(response.getProject().getKey()).isEqualTo(GENERATED_PROJECT_KEY);
Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), response.getProject().getKey());
assertThat(projectDto.orElseThrow().getCreationMethod()).isEqualTo(CreationMethod.ALM_IMPORT_BROWSER);
}
@@ -431,8 +429,8 @@ public class ImportGithubProjectActionIT {
GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, PROJECT_KEY_NAME, false,
"octocat/" + PROJECT_KEY_NAME,
"https://github.sonarsource.com/api/v3/repos/octocat/" + PROJECT_KEY_NAME, "default-branch");
when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
when(appClient.getRepository(any(), any(), any())).thenReturn(Optional.of(repository));
when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(GENERATED_PROJECT_KEY);
return repository;
}


+ 28
- 30
server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/BranchReportSubmitterIT.java Wyświetl plik

@@ -25,8 +25,6 @@ import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Rule;
@@ -49,6 +47,8 @@ import org.sonar.db.component.ProjectData;
import org.sonar.db.project.CreationMethod;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.almsettings.ws.DevOpsPlatformService;
import org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService;
import org.sonar.server.component.ComponentCreationData;
import org.sonar.server.component.ComponentCreationParameters;
import org.sonar.server.component.ComponentUpdater;
@@ -74,6 +74,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.core.ce.CeTaskCharacteristics.BRANCH;
import static org.sonar.core.ce.CeTaskCharacteristics.BRANCH_TYPE;
import static org.sonar.db.component.ComponentTesting.newBranchDto;
import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
@@ -86,6 +88,10 @@ import static org.sonar.db.permission.GlobalPermission.SCAN;
public class BranchReportSubmitterIT {

private static final String PROJECT_UUID = "PROJECT_UUID";
private static final Map<String, String> CHARACTERISTICS = Map.of(
BRANCH, "branch_name",
BRANCH_TYPE, "branch"
);
@Rule
public final UserSessionRule userSession = UserSessionRule.standalone();
@Rule
@@ -100,8 +106,11 @@ public class BranchReportSubmitterIT {
private final BranchSupportDelegate branchSupportDelegate = mock(BranchSupportDelegate.class);
private final BranchSupport branchSupport = spy(new BranchSupport(branchSupportDelegate));

private final DevOpsPlatformService devOpsPlatformService = new GitHubDevOpsPlatformService(db.getDbClient(), null,
null, projectDefaultVisibility, null, userSession, componentUpdater, null);

private final ReportSubmitter underTest = new ReportSubmitter(queue, userSession, componentUpdater, permissionTemplateService, db.getDbClient(), branchSupport,
projectDefaultVisibility);
projectDefaultVisibility, devOpsPlatformService);

@Before
public void before() {
@@ -114,7 +123,7 @@ public class BranchReportSubmitterIT {
ProjectDto project = projectData.getProjectDto();
UserDto user = db.users().insertUser();
userSession.logIn(user).addProjectPermission(SCAN.getKey(), project)
.registerBranches(projectData.getMainBranchDto());
.registerBranches(projectData.getMainBranchDto());
mockSuccessfulPrepareSubmitCall();
InputStream reportInput = IOUtils.toInputStream("{binary}", StandardCharsets.UTF_8);

@@ -132,21 +141,20 @@ public class BranchReportSubmitterIT {
userSession.logIn(user).addProjectPermission(SCAN.getKey(), projectData.getProjectDto())
.registerBranches(projectData.getMainBranchDto())
.addProjectBranchMapping(projectData.projectUuid(), branch);
Map<String, String> randomCharacteristics = randomNonEmptyMap();
BranchSupport.ComponentKey componentKey = createComponentKeyOfBranch(mainBranch.getKey(), "branch1");
when(branchSupportDelegate.createComponentKey(mainBranch.getKey(), randomCharacteristics)).thenReturn(componentKey);
when(branchSupportDelegate.createComponentKey(mainBranch.getKey(), CHARACTERISTICS)).thenReturn(componentKey);
InputStream reportInput = IOUtils.toInputStream("{binary}", StandardCharsets.UTF_8);
String taskUuid = mockSuccessfulPrepareSubmitCall();

underTest.submit(mainBranch.getKey(), mainBranch.name(), randomCharacteristics, reportInput);
underTest.submit(mainBranch.getKey(), mainBranch.name(), CHARACTERISTICS, reportInput);

verifyNoInteractions(permissionTemplateService);
verifyNoInteractions(favoriteUpdater);
verify(branchSupport, times(0)).createBranchComponent(any(), any(), any(), any());
verify(branchSupportDelegate).createComponentKey(mainBranch.getKey(), randomCharacteristics);
verify(branchSupportDelegate).createComponentKey(mainBranch.getKey(), CHARACTERISTICS);
verify(branchSupportDelegate, times(0)).createBranchComponent(any(), any(), any(), any());
verifyNoMoreInteractions(branchSupportDelegate);
verifyQueueSubmit(mainBranch, branch, user, randomCharacteristics, taskUuid);
verifyQueueSubmit(mainBranch, branch, user, CHARACTERISTICS, taskUuid);

ProjectDto projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), componentKey.getKey()).orElseThrow();
assertThat(projectDto.getCreationMethod()).isEqualTo(CreationMethod.LOCAL_API);
@@ -160,25 +168,24 @@ public class BranchReportSubmitterIT {
UserDto user = db.users().insertUser();
userSession.logIn(user).addProjectPermission(SCAN.getKey(), projectData.getProjectDto())
.registerBranches(projectData.getMainBranchDto());
Map<String, String> randomCharacteristics = randomNonEmptyMap();
ComponentDto createdBranch = createButDoNotInsertBranch(mainBranch, projectData.projectUuid());
userSession.addProjectBranchMapping(projectData.projectUuid(), createdBranch);
BranchSupport.ComponentKey componentKey = createComponentKeyOfBranch(mainBranch.getKey(), "branch1");
when(branchSupportDelegate.createComponentKey(mainBranch.getKey(), randomCharacteristics)).thenReturn(componentKey);
when(branchSupportDelegate.createComponentKey(mainBranch.getKey(), CHARACTERISTICS)).thenReturn(componentKey);
when(branchSupportDelegate.createBranchComponent(any(DbSession.class), same(componentKey), eq(mainBranch), eq(exitingProjectMainBranch))).thenReturn(createdBranch);
InputStream reportInput = IOUtils.toInputStream("{binary}", StandardCharsets.UTF_8);
String taskUuid = mockSuccessfulPrepareSubmitCall();

underTest.submit(mainBranch.getKey(), mainBranch.name(), randomCharacteristics, reportInput);
underTest.submit(mainBranch.getKey(), mainBranch.name(), CHARACTERISTICS, reportInput);

verifyNoInteractions(permissionTemplateService);
verifyNoInteractions(favoriteUpdater);
verify(branchSupport).createBranchComponent(any(DbSession.class), same(componentKey), eq(mainBranch), eq(exitingProjectMainBranch));
verify(branchSupportDelegate).createComponentKey(mainBranch.getKey(), randomCharacteristics);
verify(branchSupportDelegate).createComponentKey(mainBranch.getKey(), CHARACTERISTICS);
verify(branchSupportDelegate).createBranchComponent(any(DbSession.class), same(componentKey), eq(mainBranch), eq(exitingProjectMainBranch));
verifyNoMoreInteractions(branchSupportDelegate);
verify(componentUpdater, times(0)).commitAndIndex(any(), any());
verifyQueueSubmit(mainBranch, createdBranch, user, randomCharacteristics, taskUuid);
verifyQueueSubmit(mainBranch, createdBranch, user, CHARACTERISTICS, taskUuid);
}

@Test
@@ -189,10 +196,9 @@ public class BranchReportSubmitterIT {
.addPermission(PROVISION_PROJECTS)
.addPermission(SCAN);

Map<String, String> randomCharacteristics = randomNonEmptyMap();
ComponentDto createdBranch = createButDoNotInsertBranch(nonExistingBranch, PROJECT_UUID);
BranchSupport.ComponentKey componentKey = createComponentKeyOfBranch(nonExistingBranch.getKey());
when(branchSupportDelegate.createComponentKey(nonExistingBranch.getKey(), randomCharacteristics)).thenReturn(componentKey);
when(branchSupportDelegate.createComponentKey(nonExistingBranch.getKey(), CHARACTERISTICS)).thenReturn(componentKey);
ComponentCreationData componentCreationData = mock(ComponentCreationData.class);
when(componentCreationData.mainBranchComponent())
.thenAnswer((Answer<ComponentDto>) invocation -> db.components().insertPrivateProject(PROJECT_UUID, nonExistingBranch).getMainBranchComponent());
@@ -202,14 +208,14 @@ public class BranchReportSubmitterIT {
String taskUuid = mockSuccessfulPrepareSubmitCall();
InputStream reportInput = IOUtils.toInputStream("{binary}", StandardCharsets.UTF_8);

underTest.submit(nonExistingBranch.getKey(), nonExistingBranch.name(), randomCharacteristics, reportInput);
underTest.submit(nonExistingBranch.getKey(), nonExistingBranch.name(), CHARACTERISTICS, reportInput);

BranchDto existingProjectMainBranch = db.getDbClient().branchDao().selectByUuid(db.getSession(), nonExistingBranch.uuid()).get();
verify(branchSupport).createBranchComponent(any(DbSession.class), same(componentKey), eq(nonExistingBranch), eq(existingProjectMainBranch));
verify(branchSupportDelegate).createComponentKey(nonExistingBranch.getKey(), randomCharacteristics);
verify(branchSupportDelegate).createComponentKey(nonExistingBranch.getKey(), CHARACTERISTICS);
verify(branchSupportDelegate).createBranchComponent(any(DbSession.class), same(componentKey), eq(nonExistingBranch), eq(existingProjectMainBranch));
verifyNoMoreInteractions(branchSupportDelegate);
verifyQueueSubmit(nonExistingBranch, createdBranch, user, randomCharacteristics, taskUuid);
verifyQueueSubmit(nonExistingBranch, createdBranch, user, CHARACTERISTICS, taskUuid);
verify(componentUpdater).commitAndIndex(any(DbSession.class), eq(componentCreationData));
assertProjectCreatedWithCreationMethodEqualsScanner();
}
@@ -226,13 +232,12 @@ public class BranchReportSubmitterIT {
ComponentDto project = projectData.getMainBranchComponent();
UserDto user = db.users().insertUser();
userSession.logIn(user).addProjectPermission(SCAN.getKey(), projectData.getProjectDto());
Map<String, String> randomCharacteristics = randomNonEmptyMap();
InputStream reportInput = IOUtils.toInputStream("{binary}", StandardCharsets.UTF_8);
RuntimeException expected = new RuntimeException("Faking an exception thrown by branchSupportDelegate");
when(branchSupportDelegate.createComponentKey(any(), any())).thenThrow(expected);

try {
underTest.submit(project.getKey(), project.name(), randomCharacteristics, reportInput);
underTest.submit(project.getKey(), project.name(), CHARACTERISTICS, reportInput);
fail("exception should have been thrown");
} catch (Exception e) {
assertThat(e).isSameAs(expected);
@@ -244,16 +249,15 @@ public class BranchReportSubmitterIT {
ComponentDto nonExistingBranch = newPrivateProjectDto();
UserDto user = db.users().insertUser();

Map<String, String> randomCharacteristics = randomNonEmptyMap();
ComponentDto createdBranch = createButDoNotInsertBranch(nonExistingBranch, PROJECT_UUID);
BranchSupport.ComponentKey componentKey = createComponentKeyOfBranch(nonExistingBranch.getKey());
String nonExistingProjectDbKey = nonExistingBranch.getKey();
when(branchSupportDelegate.createComponentKey(nonExistingProjectDbKey, randomCharacteristics)).thenReturn(componentKey);
when(branchSupportDelegate.createComponentKey(nonExistingProjectDbKey, CHARACTERISTICS)).thenReturn(componentKey);
when(branchSupportDelegate.createBranchComponent(any(DbSession.class), same(componentKey), any(), any())).thenReturn(createdBranch);
InputStream reportInput = IOUtils.toInputStream("{binary}", StandardCharsets.UTF_8);

String name = nonExistingBranch.name();
assertThatThrownBy(() -> underTest.submit(nonExistingProjectDbKey, name, randomCharacteristics, reportInput))
assertThatThrownBy(() -> underTest.submit(nonExistingProjectDbKey, name, CHARACTERISTICS, reportInput))
.isInstanceOf(ForbiddenException.class)
.hasMessage("Insufficient privileges");
}
@@ -300,10 +304,4 @@ public class BranchReportSubmitterIT {
return componentKey;
}

private static Map<String, String> randomNonEmptyMap() {
return IntStream.range(0, 1 + new Random().nextInt(5))
.boxed()
.collect(Collectors.toMap(i -> "key_" + i, i1 -> "val_" + i1));
}

}

+ 123
- 19
server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java Wyświetl plik

@@ -20,15 +20,21 @@
package org.sonar.server.ce.queue;

import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.alm.client.github.config.GithubAppInstallation;
import org.sonar.api.utils.System2;
import org.sonar.auth.github.GitHubSettings;
import org.sonar.ce.queue.CeQueue;
import org.sonar.ce.queue.CeQueueImpl;
import org.sonar.ce.queue.CeTaskSubmit;
@@ -36,13 +42,21 @@ import org.sonar.core.i18n.I18n;
import org.sonar.core.util.SequenceUuidFactory;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.ce.CeTaskTypes;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ProjectData;
import org.sonar.db.permission.GlobalPermission;
import org.sonar.db.project.CreationMethod;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
import org.sonar.server.almsettings.ws.DelegatingDevOpsPlatformService;
import org.sonar.server.almsettings.ws.DevOpsPlatformService;
import org.sonar.server.almsettings.ws.DevOpsProjectDescriptor;
import org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService;
import org.sonar.server.component.ComponentUpdater;
import org.sonar.server.es.TestIndexers;
import org.sonar.server.exceptions.BadRequestException;
@@ -60,16 +74,19 @@ import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyMap;
import static java.util.stream.IntStream.rangeClosed;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.core.ce.CeTaskCharacteristics.BRANCH;
import static org.sonar.db.component.BranchDto.DEFAULT_MAIN_BRANCH_NAME;
import static org.sonar.db.component.ComponentTesting.newDirectory;
import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
@@ -98,8 +115,19 @@ public class ReportSubmitterIT {
new FavoriteUpdater(db.getDbClient()), projectIndexers, new SequenceUuidFactory(), defaultBranchNameResolver, mock(PermissionUpdater.class), mock(PermissionService.class));
private final BranchSupport ossEditionBranchSupport = new BranchSupport(null);

private final GithubApplicationClient githubApplicationClient = mock();
private final GithubGlobalSettingsValidator githubGlobalSettingsValidator = mock();
private final GitHubSettings gitHubSettings = mock();
private final ProjectKeyGenerator projectKeyGenerator = mock();

private final DevOpsPlatformService devOpsPlatformService = new DelegatingDevOpsPlatformService(
Set.of(new GitHubDevOpsPlatformService(db.getDbClient(), githubGlobalSettingsValidator,
githubApplicationClient, projectDefaultVisibility, projectKeyGenerator, userSession, componentUpdater, gitHubSettings)));

private final DevOpsPlatformService devOpsPlatformServiceSpy = spy(devOpsPlatformService);

private final ReportSubmitter underTest = new ReportSubmitter(queue, userSession, componentUpdater, permissionTemplateService, db.getDbClient(), ossEditionBranchSupport,
projectDefaultVisibility);
projectDefaultVisibility, devOpsPlatformServiceSpy);

@Before
public void before() {
@@ -115,9 +143,7 @@ public class ReportSubmitterIT {
mockSuccessfulPrepareSubmitCall();
when(permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(any(), any(), eq(PROJECT_KEY)))
.thenReturn(true);
Map<String, String> nonEmptyCharacteristics = IntStream.range(0, 1 + new Random().nextInt(5))
.boxed()
.collect(Collectors.toMap(i -> randomAlphabetic(i + 10), i1 -> randomAlphabetic(i1 + 20)));
Map<String, String> nonEmptyCharacteristics = Map.of(BRANCH, "branch1");
InputStream reportInput = IOUtils.toInputStream("{binary}", UTF_8);

assertThatThrownBy(() -> underTest.submit(PROJECT_KEY, PROJECT_NAME, nonEmptyCharacteristics, reportInput))
@@ -152,9 +178,11 @@ public class ReportSubmitterIT {
verifyReportIsPersisted(TASK_UUID);
verifyNoInteractions(permissionTemplateService);
verify(queue).submit(argThat(submit -> submit.getType().equals(CeTaskTypes.REPORT)
&& submit.getComponent().filter(cpt -> cpt.getUuid().equals(project.getMainBranchComponent().uuid()) && cpt.getEntityUuid().equals(project.projectUuid())).isPresent()
&& submit.getSubmitterUuid().equals(user.getUuid())
&& submit.getUuid().equals(TASK_UUID)));
&& submit.getComponent()
.filter(cpt -> cpt.getUuid().equals(project.getMainBranchComponent().uuid()) && cpt.getEntityUuid().equals(project.projectUuid()))
.isPresent()
&& submit.getSubmitterUuid().equals(user.getUuid())
&& submit.getUuid().equals(TASK_UUID)));
}

@Test
@@ -173,8 +201,9 @@ public class ReportSubmitterIT {

verifyReportIsPersisted(TASK_UUID);
verify(queue).submit(argThat(submit -> submit.getType().equals(CeTaskTypes.REPORT)
&& submit.getComponent().filter(cpt -> cpt.getUuid().equals(createdProject.uuid()) && cpt.getEntityUuid().equals(projectDto.getUuid())).isPresent()
&& submit.getUuid().equals(TASK_UUID)));
&& submit.getComponent().filter(cpt -> cpt.getUuid().equals(createdProject.uuid()) && cpt.getEntityUuid().equals(projectDto.getUuid()))
.isPresent()
&& submit.getUuid().equals(TASK_UUID)));
assertThat(projectDto.getCreationMethod()).isEqualTo(CreationMethod.SCANNER_API);
}

@@ -231,17 +260,92 @@ public class ReportSubmitterIT {
}

@Test
public void submit_a_report_on_new_project_with_scan_permission() {
userSession
.addPermission(GlobalPermission.SCAN)
.addPermission(PROVISION_PROJECTS);
public void submit_whenReportIsForANewProjectWithoutDevOpsMetadata_createsLocalProject() {
userSession.addPermission(GlobalPermission.SCAN).addPermission(PROVISION_PROJECTS);
mockSuccessfulPrepareSubmitCall();
when(permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(any(DbSession.class), any(), eq(PROJECT_KEY)))
.thenReturn(true);

underTest.submit(PROJECT_KEY, PROJECT_NAME, emptyMap(), IOUtils.toInputStream("{binary}", UTF_8));

verify(queue).submit(any(CeTaskSubmit.class));
ProjectDto projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), PROJECT_KEY).orElseThrow();
assertThat(projectDto.getCreationMethod()).isEqualTo(CreationMethod.SCANNER_API);
assertThat(projectDto.getName()).isEqualTo(PROJECT_NAME);

BranchDto branchDto = db.getDbClient().branchDao().selectByBranchKey(db.getSession(), projectDto.getUuid(), "main").orElseThrow();
assertThat(branchDto.isMain()).isTrue();
}

@Test
public void submit_whenReportIsForANewProjectWithoutValidAlmSettings_createsProjectWithoutDevOpsBinding() {
userSession.addPermission(GlobalPermission.SCAN).addPermission(PROVISION_PROJECTS);
when(permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(any(DbSession.class), any(), eq(PROJECT_KEY))).thenReturn(true);
mockSuccessfulPrepareSubmitCall();

Map<String, String> characteristics = Map.of("random", "data");
DevOpsProjectDescriptor projectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, "apiUrl", "orga/repo");
when(devOpsPlatformServiceSpy.getDevOpsProjectDescriptor(characteristics)).thenReturn(Optional.of(projectDescriptor));

underTest.submit(PROJECT_KEY, PROJECT_NAME, characteristics, IOUtils.toInputStream("{binary}", UTF_8));

ProjectDto projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), PROJECT_KEY).orElseThrow();
assertThat(projectDto.getCreationMethod()).isEqualTo(CreationMethod.SCANNER_API);
assertThat(projectDto.getName()).isEqualTo(PROJECT_NAME);

BranchDto branchDto = db.getDbClient().branchDao().selectByBranchKey(db.getSession(), projectDto.getUuid(), "main").orElseThrow();
assertThat(branchDto.isMain()).isTrue();

assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.getUuid())).isEmpty();
}

@Test
public void submit_whenReportIsForANewProjectWithValidAlmSettings_createsProjectWithDevOpsBinding() {
userSession.addPermission(GlobalPermission.SCAN).addPermission(PROVISION_PROJECTS);
when(permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(any(DbSession.class), any(), eq(PROJECT_KEY))).thenReturn(true);
mockSuccessfulPrepareSubmitCall();

Map<String, String> characteristics = Map.of("random", "data");
DevOpsProjectDescriptor projectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, "apiUrl", "orga/repo");

mockInteractionsWithDevOpsPlatformServiceSpyBeforeProjectCreation(characteristics, projectDescriptor);

underTest.submit(PROJECT_KEY, PROJECT_NAME, characteristics, IOUtils.toInputStream("{binary}", UTF_8));

ProjectDto projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), PROJECT_KEY).orElseThrow();
assertThat(projectDto.getCreationMethod()).isEqualTo(CreationMethod.SCANNER_API_DEVOPS_AUTO_CONFIG);
assertThat(projectDto.getName()).isEqualTo("repoName");

BranchDto branchDto = db.getDbClient().branchDao().selectByBranchKey(db.getSession(), projectDto.getUuid(), "defaultBranch").orElseThrow();
assertThat(branchDto.isMain()).isTrue();

assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.getUuid())).isPresent();
}

private void mockInteractionsWithDevOpsPlatformServiceSpyBeforeProjectCreation(Map<String, String> characteristics, DevOpsProjectDescriptor projectDescriptor) {
doReturn(Optional.of(projectDescriptor)).when(devOpsPlatformServiceSpy).getDevOpsProjectDescriptor(characteristics);
AlmSettingDto almSettingDto = mock(AlmSettingDto.class);
when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB);
when(almSettingDto.getUrl()).thenReturn("https://www.toto.com");
when(almSettingDto.getUuid()).thenReturn("TEST_GH");
doReturn(Optional.of(almSettingDto)).when(devOpsPlatformServiceSpy).getValidAlmSettingDto(any(), eq(projectDescriptor));
mockGithubInteractions(almSettingDto);
}

private void mockGithubInteractions(AlmSettingDto almSettingDto) {
GithubAppConfiguration githubAppConfiguration = mock(GithubAppConfiguration.class);
when(githubGlobalSettingsValidator.validate(almSettingDto)).thenReturn(githubAppConfiguration);
GithubAppInstallation githubAppInstallation = mock(GithubAppInstallation.class);
when(githubAppInstallation.installationId()).thenReturn("5435345");
when(githubApplicationClient.getWhitelistedGithubAppInstallations(any())).thenReturn(List.of(githubAppInstallation));
when(githubApplicationClient.createAppInstallationToken(any(), anyLong())).thenReturn(Optional.of(mock(AppInstallationToken.class)));
when(githubApplicationClient.createAppInstallationToken(any(), anyLong())).thenReturn(Optional.of(mock(AppInstallationToken.class)));
when(githubApplicationClient.getInstallationId(eq(githubAppConfiguration), any())).thenReturn(Optional.of(5435345L));
GithubApplicationClient.Repository repository = mock(GithubApplicationClient.Repository.class);
when(repository.getDefaultBranch()).thenReturn("defaultBranch");
when(repository.getFullName()).thenReturn("orga/repoName");
when(repository.getName()).thenReturn("repoName");
when(githubApplicationClient.getRepository(any(), any(), any())).thenReturn(Optional.of(repository));
when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn("projectKey");
}

@Test
@@ -300,7 +404,7 @@ public class ReportSubmitterIT {
.extracting(throwable -> ((BadRequestException) throwable).errors())
.asList()
.contains(format("The project '%s' is already defined in SonarQube but as a module of project '%s'. " +
"If you really want to stop directly analysing project '%s', please first delete it from SonarQube and then relaunch the analysis of project '%s'.",
"If you really want to stop directly analysing project '%s', please first delete it from SonarQube and then relaunch the analysis of project '%s'.",
dir.getKey(), project.getKey(), project.getKey(), dir.getKey()));
}


+ 20
- 78
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java Wyświetl plik

@@ -19,49 +19,36 @@
*/
package org.sonar.server.almintegration.ws.github;

import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubApplicationClient.Repository;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.alm.client.github.security.UserAccessToken;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.auth.github.GitHubSettings;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.alm.pat.AlmPatDto;
import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import org.sonar.db.component.BranchDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
import org.sonar.server.almintegration.ws.ImportHelper;
import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
import org.sonar.server.almsettings.ws.DevOpsProjectDescriptor;
import org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService;
import org.sonar.server.component.ComponentCreationData;
import org.sonar.server.component.ComponentCreationParameters;
import org.sonar.server.component.ComponentUpdater;
import org.sonar.server.component.NewComponent;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.management.ManagedProjectService;
import org.sonar.server.newcodeperiod.NewCodeDefinitionResolver;
import org.sonar.server.project.DefaultBranchNameResolver;
import org.sonar.server.project.ProjectDefaultVisibility;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Projects;

import static java.util.Objects.requireNonNull;
import static org.sonar.api.resources.Qualifiers.PROJECT;
import static org.sonar.db.project.CreationMethod.Category.ALM_IMPORT;
import static org.sonar.db.project.CreationMethod.getCreationMethod;
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.newcodeperiod.NewCodeDefinitionResolver.NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION;
import static org.sonar.server.newcodeperiod.NewCodeDefinitionResolver.NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION;
import static org.sonar.server.newcodeperiod.NewCodeDefinitionResolver.checkNewCodeDefinitionParam;
@@ -70,54 +57,48 @@ import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_COD
import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_VALUE;

public class ImportGithubProjectAction implements AlmIntegrationsWsAction {

public static final String PARAM_ORGANIZATION = "organization";
public static final String PARAM_REPOSITORY_KEY = "repositoryKey";

private final DbClient dbClient;

private final ManagedProjectService managedProjectService;
private final UserSession userSession;
private final ProjectDefaultVisibility projectDefaultVisibility;
private final GithubApplicationClient githubApplicationClient;
private final ComponentUpdater componentUpdater;
private final ImportHelper importHelper;
private final ProjectKeyGenerator projectKeyGenerator;

private final NewCodeDefinitionResolver newCodeDefinitionResolver;

private final DefaultBranchNameResolver defaultBranchNameResolver;

private final GitHubSettings gitHubSettings;
private final GitHubDevOpsPlatformService gitHubDevOpsPlatformService;

@Inject
public ImportGithubProjectAction(DbClient dbClient, ManagedProjectService managedProjectService, UserSession userSession, ProjectDefaultVisibility projectDefaultVisibility,
GithubApplicationClientImpl githubApplicationClient, ComponentUpdater componentUpdater, ImportHelper importHelper,
ProjectKeyGenerator projectKeyGenerator, NewCodeDefinitionResolver newCodeDefinitionResolver,
DefaultBranchNameResolver defaultBranchNameResolver, GitHubSettings gitHubSettings) {
public ImportGithubProjectAction(DbClient dbClient, ManagedProjectService managedProjectService, UserSession userSession,
ComponentUpdater componentUpdater, ImportHelper importHelper,
NewCodeDefinitionResolver newCodeDefinitionResolver,
DefaultBranchNameResolver defaultBranchNameResolver, GitHubDevOpsPlatformService gitHubDevOpsPlatformService) {
this.dbClient = dbClient;
this.managedProjectService = managedProjectService;
this.userSession = userSession;
this.projectDefaultVisibility = projectDefaultVisibility;
this.githubApplicationClient = githubApplicationClient;
this.componentUpdater = componentUpdater;
this.importHelper = importHelper;
this.projectKeyGenerator = projectKeyGenerator;
this.newCodeDefinitionResolver = newCodeDefinitionResolver;
this.defaultBranchNameResolver = defaultBranchNameResolver;
this.gitHubSettings = gitHubSettings;
this.gitHubDevOpsPlatformService = gitHubDevOpsPlatformService;
}

@Override
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction("import_github_project")
.setDescription("Create a SonarQube project with the information from the provided GitHub repository.<br/>" +
"Autoconfigure pull request decoration mechanism. If Automatic Provisioning is enable for GitHub, it will also synchronize permissions from the repository.<br/>" +
"Requires the 'Create Projects' permission")
"Autoconfigure pull request decoration mechanism. If Automatic Provisioning is enable for GitHub, " +
"it will also synchronize permissions from the repository.<br/>" +
"Requires the 'Create Projects' permission")
.setPost(true)
.setSince("8.4")
.setHandler(this)
.setChangelog(
new Change("10.3", "Parameter organization is not necessary anymore"),
new Change("10.3", String.format("Parameter %s becomes optional if you have only one configuration for GitHub", PARAM_ALM_SETTING)),
new Change("10.3", "Endpoint visibility change from internal to public"));

@@ -125,15 +106,10 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction {
.setMaximumLength(200)
.setDescription("DevOps Platform configuration key. This parameter is optional if you have only one GitHub integration.");

action.createParam(PARAM_ORGANIZATION)
.setRequired(true)
.setMaximumLength(200)
.setDescription("GitHub organization");

action.createParam(PARAM_REPOSITORY_KEY)
.setRequired(true)
.setMaximumLength(256)
.setDescription("GitHub repository key");
.setDescription("GitHub repository key (organization/repoSlug");

action.createParam(PARAM_NEW_CODE_DEFINITION_TYPE)
.setDescription(NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION)
@@ -160,30 +136,27 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction {

AccessToken accessToken = getAccessToken(dbSession, almSettingDto);

String githubOrganization = request.mandatoryParam(PARAM_ORGANIZATION);
String repositoryKey = request.mandatoryParam(PARAM_REPOSITORY_KEY);

String url = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null");
Repository repository = githubApplicationClient.getRepository(url, accessToken, githubOrganization, repositoryKey)
.orElseThrow(() -> new NotFoundException(String.format("GitHub repository '%s' not found", repositoryKey)));
DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, url, repositoryKey);
ComponentCreationData componentCreationData = gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, almSettingDto, accessToken,
devOpsProjectDescriptor);

checkNewCodeDefinitionParam(newCodeDefinitionType, newCodeDefinitionValue);

ComponentCreationData componentCreationData = createProject(dbSession, repository, repository.getDefaultBranch());
ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
BranchDto mainBranchDto = Optional.ofNullable(componentCreationData.mainBranchDto()).orElseThrow();

populatePRSetting(dbSession, repository, projectDto, almSettingDto);

checkNewCodeDefinitionParam(newCodeDefinitionType, newCodeDefinitionValue);

if (newCodeDefinitionType != null) {
newCodeDefinitionResolver.createNewCodeDefinition(dbSession, projectDto.getUuid(), mainBranchDto.getUuid(),
Optional.ofNullable(repository.getDefaultBranch()).orElse(defaultBranchNameResolver.getEffectiveMainBranchName()),
Optional.ofNullable(mainBranchDto.getKey()).orElse(defaultBranchNameResolver.getEffectiveMainBranchName()),
newCodeDefinitionType, newCodeDefinitionValue);
}

componentUpdater.commitAndIndex(dbSession, componentCreationData);

String userUuid = Objects.requireNonNull(userSession.getUuid());
String userUuid = requireNonNull(userSession.getUuid());
managedProjectService.queuePermissionSyncTask(userUuid, mainBranchDto.getUuid(), projectDto.getUuid());

return toCreateResponse(projectDto);
@@ -198,35 +171,4 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction {
.orElseThrow(() -> new IllegalArgumentException("No personal access token found"));
}

private ComponentCreationData createProject(DbSession dbSession, Repository repo, String mainBranchName) {
boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
String uniqueProjectKey = projectKeyGenerator.generateUniqueProjectKey(repo.getFullName());
NewComponent projectComponent = newComponentBuilder()
.setKey(uniqueProjectKey)
.setName(repo.getName())
.setPrivate(visibility)
.setQualifier(PROJECT)
.build();
ComponentCreationParameters componentCreationParameters = ComponentCreationParameters.builder()
.newComponent(projectComponent)
.userLogin(userSession.getLogin())
.userUuid(userSession.getUuid())
.mainBranchName(mainBranchName)
.isManaged(gitHubSettings.isProvisioningEnabled())
.creationMethod(getCreationMethod(ALM_IMPORT, userSession.isAuthenticatedBrowserSession()))
.build();
return componentUpdater.createWithoutCommit(dbSession, componentCreationParameters);
}

private void populatePRSetting(DbSession dbSession, Repository repo, ProjectDto projectDto, AlmSettingDto almSettingDto) {
ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto()
.setAlmSettingUuid(almSettingDto.getUuid())
.setAlmRepo(repo.getFullName())
.setAlmSlug(null)
.setProjectUuid(projectDto.getUuid())
.setSummaryCommentEnabled(true)
.setMonorepo(false);
dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto, almSettingDto.getKey(),
projectDto.getName(), projectDto.getKey());
}
}

+ 18
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java Wyświetl plik

@@ -24,10 +24,12 @@ import java.util.Optional;
import java.util.Set;
import javax.annotation.Priority;
import org.apache.commons.lang.NotImplementedException;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.api.server.ServerSide;
import org.sonar.db.DbSession;
import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.server.component.ComponentCreationData;

@ServerSide
@Priority(1)
@@ -57,6 +59,22 @@ public class DelegatingDevOpsPlatformService implements DevOpsPlatformService {
.flatMap(delegate -> delegate.getValidAlmSettingDto(dbSession, devOpsProjectDescriptor));
}

@Override
public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, String projectKey, AlmSettingDto almSettingDto,
DevOpsProjectDescriptor devOpsProjectDescriptor) {
return findDelegate(almSettingDto.getAlm())
.map(delegate -> delegate.createProjectAndBindToDevOpsPlatform(dbSession, projectKey, almSettingDto, devOpsProjectDescriptor))
.orElseThrow(() -> new IllegalStateException("Impossible to bind project to ALM platform " + almSettingDto.getAlm()));
}

@Override
public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, AlmSettingDto almSettingDto, AccessToken accessToken,
DevOpsProjectDescriptor devOpsProjectDescriptor) {
return findDelegate(almSettingDto.getAlm())
.map(delegate -> delegate.createProjectAndBindToDevOpsPlatform(dbSession, almSettingDto, accessToken, devOpsProjectDescriptor))
.orElseThrow(() -> new IllegalStateException("Impossible to bind project to ALM platform " + almSettingDto.getAlm()));
}

private Optional<DevOpsPlatformService> findDelegate(ALM alm) {
return delegates.stream()
.filter(delegate -> delegate.getDevOpsPlatform().equals(alm))

+ 6
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java Wyświetl plik

@@ -21,9 +21,11 @@ package org.sonar.server.almsettings.ws;

import java.util.Map;
import java.util.Optional;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.db.DbSession;
import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.server.component.ComponentCreationData;

public interface DevOpsPlatformService {

@@ -33,4 +35,8 @@ public interface DevOpsPlatformService {

Optional<AlmSettingDto> getValidAlmSettingDto(DbSession dbSession, DevOpsProjectDescriptor devOpsProjectDescriptor);

ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, String projectKey, AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor);

ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, AlmSettingDto almSettingDto, AccessToken accessToken,
DevOpsProjectDescriptor devOpsProjectDescriptor);
}

+ 135
- 9
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java Wyświetl plik

@@ -21,30 +21,67 @@ package org.sonar.server.almsettings.ws;

import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.alm.client.github.security.AccessToken;
import org.sonar.api.server.ServerSide;
import org.sonar.auth.github.GitHubSettings;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.AlmSettingDao;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import org.sonar.db.project.CreationMethod;
import org.sonar.db.project.ProjectDto;
import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
import org.sonar.server.component.ComponentCreationData;
import org.sonar.server.component.ComponentCreationParameters;
import org.sonar.server.component.ComponentUpdater;
import org.sonar.server.component.NewComponent;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.project.ProjectDefaultVisibility;
import org.sonar.server.user.UserSession;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static org.sonar.api.resources.Qualifiers.PROJECT;
import static org.sonar.db.project.CreationMethod.Category.ALM_IMPORT;
import static org.sonar.db.project.CreationMethod.SCANNER_API_DEVOPS_AUTO_CONFIG;
import static org.sonar.db.project.CreationMethod.getCreationMethod;
import static org.sonar.server.component.NewComponent.newComponentBuilder;

@ServerSide
public class GitHubDevOpsPlatformService implements DevOpsPlatformService {
private static final Logger LOG = LoggerFactory.getLogger(GitHubDevOpsPlatformService.class);

public static final String DEVOPS_PLATFORM_URL = "devOpsPlatformUrl";
public static final String DEVOPS_PLATFORM_PROJECT_IDENTIFIER = "devOpsPlatformProjectIdentifier";

private final AlmSettingDao almSettingDao;
private final DbClient dbClient;
private final GithubGlobalSettingsValidator githubGlobalSettingsValidator;
private final GithubApplicationClient githubApplicationClient;
private final ProjectDefaultVisibility projectDefaultVisibility;
private final ProjectKeyGenerator projectKeyGenerator;
private final UserSession userSession;
private final ComponentUpdater componentUpdater;
private final GitHubSettings gitHubSettings;

public GitHubDevOpsPlatformService(AlmSettingDao almSettingDao, GithubGlobalSettingsValidator githubGlobalSettingsValidator,
GithubApplicationClient githubApplicationClient) {
this.almSettingDao = almSettingDao;
public GitHubDevOpsPlatformService(DbClient dbClient, GithubGlobalSettingsValidator githubGlobalSettingsValidator,
GithubApplicationClient githubApplicationClient, ProjectDefaultVisibility projectDefaultVisibility, ProjectKeyGenerator projectKeyGenerator, UserSession userSession,
ComponentUpdater componentUpdater, GitHubSettings gitHubSettings) {
this.dbClient = dbClient;
this.githubGlobalSettingsValidator = githubGlobalSettingsValidator;
this.githubApplicationClient = githubApplicationClient;
this.projectDefaultVisibility = projectDefaultVisibility;
this.projectKeyGenerator = projectKeyGenerator;
this.userSession = userSession;
this.componentUpdater = componentUpdater;
this.gitHubSettings = gitHubSettings;
}

@Override
@@ -64,15 +101,104 @@ public class GitHubDevOpsPlatformService implements DevOpsPlatformService {

@Override
public Optional<AlmSettingDto> getValidAlmSettingDto(DbSession dbSession, DevOpsProjectDescriptor devOpsProjectDescriptor) {
return almSettingDao.selectByAlm(dbSession, getDevOpsPlatform()).stream()
Optional<AlmSettingDto> configurationToUse = dbClient.almSettingDao().selectByAlm(dbSession, getDevOpsPlatform()).stream()
.filter(almSettingDto -> devOpsProjectDescriptor.url().equals(almSettingDto.getUrl()))
.filter(almSettingDto -> hasAccessToRepo(almSettingDto, devOpsProjectDescriptor.projectIdentifier()))
.filter(almSettingDto -> findInstallationIdToAccessRepo(almSettingDto, devOpsProjectDescriptor.projectIdentifier()).isPresent())
.findFirst();
if (configurationToUse.isPresent()) {
LOG.info("DevOps configuration {} auto-detected", configurationToUse.get().getKey());
} else {
LOG.info("Could not auto-detect a DevOps configuration for project {} (api url {})",
devOpsProjectDescriptor.projectIdentifier(), devOpsProjectDescriptor.url());
}
return configurationToUse;
}

private boolean hasAccessToRepo(AlmSettingDto almSettingDto, String repo) {
@Override
public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, String projectKey, AlmSettingDto almSettingDto,
DevOpsProjectDescriptor devOpsProjectDescriptor) {
GithubAppConfiguration githubAppConfiguration = githubGlobalSettingsValidator.validate(almSettingDto);
return githubApplicationClient.getInstallationId(githubAppConfiguration, repo).isPresent();
GithubApplicationClient.Repository repository = findInstallationIdToAccessRepo(almSettingDto, devOpsProjectDescriptor.projectIdentifier())
.flatMap(installationId -> findRepositoryOnGithub(devOpsProjectDescriptor.projectIdentifier(), githubAppConfiguration, installationId))
.orElseThrow(() -> new IllegalStateException(format("Impossible to find the repository %s on GitHub, using the devops config %s.",
devOpsProjectDescriptor.projectIdentifier(), almSettingDto.getKey())));

return createProjectAndBindToDevOpsPlatform(dbSession, projectKey, almSettingDto, repository, SCANNER_API_DEVOPS_AUTO_CONFIG);
}

private Optional<Long> findInstallationIdToAccessRepo(AlmSettingDto almSettingDto, String repositoryKey) {
try {
GithubAppConfiguration githubAppConfiguration = githubGlobalSettingsValidator.validate(almSettingDto);
return githubApplicationClient.getInstallationId(githubAppConfiguration, repositoryKey);
} catch (Exception exception) {
LOG.info(format("Could not use DevOps configuration '%s' to access repo %s. Error: %s", almSettingDto.getKey(), repositoryKey, exception.getMessage()));
return Optional.empty();
}
}

private Optional<GithubApplicationClient.Repository> findRepositoryOnGithub(String organizationAndRepository,
GithubAppConfiguration githubAppConfiguration, long installationId) {
AppInstallationToken accessToken = generateAppInstallationToken(githubAppConfiguration, installationId);
return githubApplicationClient.getRepository(githubAppConfiguration.getApiEndpoint(), accessToken, organizationAndRepository);
}

private AppInstallationToken generateAppInstallationToken(GithubAppConfiguration githubAppConfiguration, long installationId) {
return githubApplicationClient.createAppInstallationToken(githubAppConfiguration, installationId)
.orElseThrow(() -> new IllegalStateException(format("Error while generating token for GitHub Api Url %s (installation id: %s)",
githubAppConfiguration.getApiEndpoint(), installationId)));
}

@Override
public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, AlmSettingDto almSettingDto, AccessToken accessToken,
DevOpsProjectDescriptor devOpsProjectDescriptor) {
String url = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null");
GithubApplicationClient.Repository repository = githubApplicationClient.getRepository(url, accessToken, devOpsProjectDescriptor.projectIdentifier())
.orElseThrow(() -> new NotFoundException(String.format("GitHub repository '%s' not found", devOpsProjectDescriptor)));

CreationMethod creationMethod = getCreationMethod(ALM_IMPORT, userSession.isAuthenticatedBrowserSession());
return createProjectAndBindToDevOpsPlatform(dbSession, null, almSettingDto, repository, creationMethod);
}

private ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, @Nullable String projectKey, AlmSettingDto almSettingDto,
GithubApplicationClient.Repository repository, CreationMethod creationMethod) {
ComponentCreationData componentCreationData = createProject(dbSession, projectKey, repository, creationMethod);
ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
createProjectAlmSettingDto(dbSession, repository, projectDto, almSettingDto);
return componentCreationData;
}

private ComponentCreationData createProject(DbSession dbSession, @Nullable String projectKey, GithubApplicationClient.Repository repository, CreationMethod creationMethod) {
boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
NewComponent projectComponent = newComponentBuilder()
.setKey(Optional.ofNullable(projectKey).orElse(getUniqueProjectKey(repository)))
.setName(repository.getName())
.setPrivate(visibility)
.setQualifier(PROJECT)
.build();
ComponentCreationParameters componentCreationParameters = ComponentCreationParameters.builder()
.newComponent(projectComponent)
.userLogin(userSession.getLogin())
.userUuid(userSession.getUuid())
.mainBranchName(repository.getDefaultBranch())
.isManaged(gitHubSettings.isProvisioningEnabled())
.creationMethod(creationMethod)
.build();
return componentUpdater.createWithoutCommit(dbSession, componentCreationParameters);
}

private String getUniqueProjectKey(GithubApplicationClient.Repository repository) {
return projectKeyGenerator.generateUniqueProjectKey(repository.getFullName());
}

private void createProjectAlmSettingDto(DbSession dbSession, GithubApplicationClient.Repository repo, ProjectDto projectDto, AlmSettingDto almSettingDto) {
ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto()
.setAlmSettingUuid(almSettingDto.getUuid())
.setAlmRepo(repo.getFullName())
.setAlmSlug(null)
.setProjectUuid(projectDto.getUuid())
.setSummaryCommentEnabled(true)
.setMonorepo(false);
dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto, almSettingDto.getKey(), projectDto.getName(), projectDto.getKey());
}

}

+ 9
- 5
server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/BranchSupport.java Wyświetl plik

@@ -22,6 +22,7 @@ package org.sonar.server.ce.queue;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.server.ServerSide;
@@ -30,6 +31,9 @@ import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;

import static com.google.common.base.Preconditions.checkState;
import static org.sonar.core.ce.CeTaskCharacteristics.BRANCH;
import static org.sonar.core.ce.CeTaskCharacteristics.BRANCH_TYPE;
import static org.sonar.core.ce.CeTaskCharacteristics.PULL_REQUEST;

/**
* Branch code for {@link ReportSubmitter}.
@@ -38,6 +42,7 @@ import static com.google.common.base.Preconditions.checkState;
*/
@ServerSide
public class BranchSupport {
private static final Set<String> BRANCH_CHARACTERISTICS = Set.of(BRANCH, BRANCH_TYPE, PULL_REQUEST);
@CheckForNull
private final BranchSupportDelegate delegate;

@@ -46,13 +51,12 @@ public class BranchSupport {
}

ComponentKey createComponentKey(String projectKey, Map<String, String> characteristics) {
if (characteristics.isEmpty()) {
return new ComponentKeyImpl(projectKey);
} else {
boolean containsBranchCharacteristics = characteristics.keySet().stream().anyMatch(BRANCH_CHARACTERISTICS::contains);
if (containsBranchCharacteristics) {
checkState(delegate != null, "Current edition does not support branch feature");
return delegate.createComponentKey(projectKey, characteristics);
}

return delegate.createComponentKey(projectKey, characteristics);
return new ComponentKeyImpl(projectKey);
}

ComponentDto createBranchComponent(DbSession dbSession, ComponentKey componentKey, ComponentDto mainComponentDto, BranchDto mainComponentBranchDto) {

+ 24
- 15
server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/ReportSubmitter.java Wyświetl plik

@@ -34,10 +34,13 @@ import org.sonar.ce.queue.CeTaskSubmit;
import org.sonar.ce.task.CeTask;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.ce.CeTaskTypes;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.permission.GlobalPermission;
import org.sonar.server.almsettings.ws.DevOpsPlatformService;
import org.sonar.server.almsettings.ws.DevOpsProjectDescriptor;
import org.sonar.server.component.ComponentCreationData;
import org.sonar.server.component.ComponentCreationParameters;
import org.sonar.server.component.ComponentUpdater;
@@ -45,7 +48,6 @@ import org.sonar.server.component.NewComponent;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.permission.PermissionTemplateService;
import org.sonar.server.project.ProjectDefaultVisibility;
import org.sonar.server.project.Visibility;
import org.sonar.server.user.UserSession;

import static java.lang.String.format;
@@ -64,9 +66,11 @@ public class ReportSubmitter {
private final DbClient dbClient;
private final BranchSupport branchSupport;
private final ProjectDefaultVisibility projectDefaultVisibility;
private final DevOpsPlatformService devOpsPlatformService;

public ReportSubmitter(CeQueue queue, UserSession userSession, ComponentUpdater componentUpdater,
PermissionTemplateService permissionTemplateService, DbClient dbClient, BranchSupport branchSupport, ProjectDefaultVisibility projectDefaultVisibility) {
PermissionTemplateService permissionTemplateService, DbClient dbClient, BranchSupport branchSupport, ProjectDefaultVisibility projectDefaultVisibility,
DevOpsPlatformService devOpsPlatformService) {
this.queue = queue;
this.userSession = userSession;
this.componentUpdater = componentUpdater;
@@ -74,6 +78,7 @@ public class ReportSubmitter {
this.dbClient = dbClient;
this.branchSupport = branchSupport;
this.projectDefaultVisibility = projectDefaultVisibility;
this.devOpsPlatformService = devOpsPlatformService;
}

public CeTask submit(String projectKey, @Nullable String projectName, Map<String, String> characteristics, InputStream reportInput) {
@@ -89,7 +94,7 @@ public class ReportSubmitter {
mainBranchComponent = mainBranchComponentOpt.get();
validateProject(dbSession, mainBranchComponent, projectKey);
} else {
componentCreationData = createProject(dbSession, componentKey.getKey(), projectName);
componentCreationData = createProject(projectKey, projectName, characteristics, dbSession, componentKey);
mainBranchComponent = componentCreationData.mainBranchComponent();
}

@@ -98,7 +103,7 @@ public class ReportSubmitter {
ComponentDto branchComponent;
if (isMainBranch(componentKey, mainBranch)) {
branchComponent = mainBranchComponent;
} else if(componentKey.getBranchName().isPresent()) {
} else if (componentKey.getBranchName().isPresent()) {
branchComponent = dbClient.componentDao().selectByKeyAndBranch(dbSession, componentKey.getKey(), componentKey.getBranchName().get())
.orElseGet(() -> branchSupport.createBranchComponent(dbSession, componentKey, mainBranchComponent, mainBranch));
} else {
@@ -154,35 +159,39 @@ public class ReportSubmitter {
}
}

private ComponentCreationData createProject(DbSession dbSession, String projectKey, @Nullable String projectName) {
private ComponentCreationData createProject(String projectKey, @Nullable String projectName, Map<String, String> characteristics,
DbSession dbSession, BranchSupport.ComponentKey componentKey) {
userSession.checkPermission(GlobalPermission.PROVISION_PROJECTS);
String userUuid = userSession.getUuid();
String userName = userSession.getLogin();

boolean wouldCurrentUserHaveScanPermission = permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(dbSession, userUuid, projectKey);
boolean wouldCurrentUserHaveScanPermission = permissionTemplateService.wouldUserHaveScanPermissionWithDefaultTemplate(dbSession, userSession.getUuid(), projectKey);
if (!wouldCurrentUserHaveScanPermission) {
throw insufficientPrivilegesException();
}

Optional<DevOpsProjectDescriptor> devOpsProjectDescriptor = devOpsPlatformService.getDevOpsProjectDescriptor(characteristics);
Optional<AlmSettingDto> almSettingDto = devOpsProjectDescriptor.flatMap(descriptor -> devOpsPlatformService.getValidAlmSettingDto(dbSession, descriptor));
if (almSettingDto.isPresent()) {
return devOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, projectKey, almSettingDto.get(), devOpsProjectDescriptor.get());
}
return createProject(dbSession, componentKey.getKey(), defaultIfBlank(projectName, projectKey));
}

private ComponentCreationData createProject(DbSession dbSession, String projectKey, String projectName) {
NewComponent newProject = newComponentBuilder()
.setKey(projectKey)
.setName(defaultIfBlank(projectName, projectKey))
.setQualifier(Qualifiers.PROJECT)
.setPrivate(getDefaultVisibility(dbSession).isPrivate())
.setPrivate(projectDefaultVisibility.get(dbSession).isPrivate())
.build();
ComponentCreationParameters componentCreationParameters = ComponentCreationParameters.builder()
.newComponent(newProject)
.userLogin(userName)
.userUuid(userUuid)
.userLogin(userSession.getLogin())
.userUuid(userSession.getUuid())
.creationMethod(SCANNER_API)
.build();
return componentUpdater.createWithoutCommit(dbSession, componentCreationParameters);
}

private Visibility getDefaultVisibility(DbSession dbSession) {
return projectDefaultVisibility.get(dbSession);
}

private CeTask submitReport(DbSession dbSession, InputStream reportInput, ComponentDto branch, BranchDto mainBranch, Map<String, String> characteristics) {
CeTaskSubmit.Builder submit = queue.prepareSubmit();


+ 231
- 8
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java Wyświetl plik

@@ -22,45 +22,107 @@ package org.sonar.server.almsettings.ws;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.alm.client.github.AppInstallationToken;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.testfixtures.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;
import org.sonar.auth.github.GitHubSettings;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.alm.setting.ALM;
import org.sonar.db.alm.setting.AlmSettingDao;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.alm.setting.ProjectAlmSettingDao;
import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import org.sonar.db.project.CreationMethod;
import org.sonar.db.project.ProjectDto;
import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
import org.sonar.server.component.ComponentCreationData;
import org.sonar.server.component.ComponentCreationParameters;
import org.sonar.server.component.ComponentUpdater;
import org.sonar.server.component.NewComponent;
import org.sonar.server.project.ProjectDefaultVisibility;
import org.sonar.server.project.Visibility;
import org.sonar.server.user.UserSession;

import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService.DEVOPS_PLATFORM_PROJECT_IDENTIFIER;
import static org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService.DEVOPS_PLATFORM_URL;


@RunWith(MockitoJUnitRunner.class)
public class GitHubDevOpsPlatformServiceTest {
@Rule
public LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);

private static final DevOpsProjectDescriptor GITHUB_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITHUB, "url", "repo");
private static final long APP_INSTALLATION_ID = 534534534543L;
private static final String USER_LOGIN = "user-login-1";
private static final String USER_UUID = "user-uuid-1";
private static final String PROJECT_KEY = "projectKey";
private static final String PROJECT_NAME = "projectName";
private static final String MAIN_BRANCH_NAME = "defaultBranch";
private static final String ORGANIZATION_NAME = "orgname";
private static final String GITHUB_REPO_FULL_NAME = ORGANIZATION_NAME + "/" + PROJECT_NAME;
private static final String GITHUB_API_URL = "https://api.toto.com";

@Mock
private DbSession dbSession;
@Mock
private AlmSettingDao almSettingDao;
@Mock
private GithubGlobalSettingsValidator githubGlobalSettingsValidator;
@Mock
private GithubApplicationClient githubApplicationClient;

@Mock
private ComponentUpdater componentUpdater;

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private DbClient dbClient;
@Mock
private UserSession userSession;

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ProjectDefaultVisibility projectDefaultVisibility;

@Mock
private ProjectKeyGenerator projectKeyGenerator;

@Mock
private GitHubSettings gitHubSettings;

@InjectMocks
private GitHubDevOpsPlatformService gitHubDevOpsPlatformService;

@Captor
ArgumentCaptor<ComponentCreationParameters> componentCreationParametersCaptor;
@Captor
ArgumentCaptor<ProjectAlmSettingDto> projectAlmSettingDtoCaptor;

@Before
public void setup() {
when(userSession.getLogin()).thenReturn(USER_LOGIN);
when(userSession.getUuid()).thenReturn(USER_UUID);
}

@Test
public void getDevOpsPlatform_shouldReturnGitHub() {
assertThat(gitHubDevOpsPlatformService.getDevOpsPlatform())
@@ -88,8 +150,6 @@ public class GitHubDevOpsPlatformServiceTest {

@Test
public void getValidAlmSettingDto_whenNoAlmSetting_shouldReturnEmpty() {
when(almSettingDao.selectByAlm(dbSession, ALM.GITHUB)).thenReturn(emptyList());

Optional<AlmSettingDto> almSettingDto = gitHubDevOpsPlatformService.getValidAlmSettingDto(dbSession, GITHUB_PROJECT_DESCRIPTOR);

assertThat(almSettingDto).isEmpty();
@@ -99,7 +159,7 @@ public class GitHubDevOpsPlatformServiceTest {
public void getValidAlmSettingDto_whenMultipleAlmSetting_shouldReturnTheRightOne() {
AlmSettingDto mockGitHubAlmSettingDtoNoAccess = mockGitHubAlmSettingDto(false);
AlmSettingDto mockGitHubAlmSettingDtoAccess = mockGitHubAlmSettingDto(true);
when(almSettingDao.selectByAlm(dbSession, ALM.GITHUB)).thenReturn(List.of(mockGitHubAlmSettingDtoNoAccess, mockGitHubAlmSettingDtoAccess));
when(dbClient.almSettingDao().selectByAlm(dbSession, ALM.GITHUB)).thenReturn(List.of(mockGitHubAlmSettingDtoNoAccess, mockGitHubAlmSettingDtoAccess));

Optional<AlmSettingDto> almSettingDto = gitHubDevOpsPlatformService.getValidAlmSettingDto(dbSession, GITHUB_PROJECT_DESCRIPTOR);

@@ -117,4 +177,167 @@ public class GitHubDevOpsPlatformServiceTest {
return mockAlmSettingDto;
}

@Test
public void createProjectAndBindToDevOpsPlatform_whenRepoNotFound_throws() {
DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME);

AlmSettingDto almSettingDto = mockAlmSettingDto(devOpsProjectDescriptor);
GithubAppConfiguration githubAppConfiguration = mockGitHubAppConfiguration(almSettingDto);
when(githubApplicationClient.getInstallationId(githubAppConfiguration, GITHUB_REPO_FULL_NAME)).thenReturn(Optional.empty());

assertThatIllegalStateException().isThrownBy(
() -> gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto, devOpsProjectDescriptor))
.withMessage("Impossible to find the repository orgname/projectName on GitHub, using the devops config devops-platform-config-1.");
}

@Test
public void createProjectAndBindToDevOpsPlatform_whenRepoFoundOnGitHub_successfullyCreatesProject() {
// given
DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME);

AlmSettingDto almSettingDto = mockAlmSettingDto(devOpsProjectDescriptor);
mockExistingGitHubRepository(almSettingDto);

ComponentCreationData componentCreationData = mockProjectCreation();
ProjectAlmSettingDao projectAlmSettingDao = mock();
when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);

// when
ComponentCreationData actualComponentCreationData = gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto,
devOpsProjectDescriptor);

// then
assertThat(actualComponentCreationData).isEqualTo(componentCreationData);

ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters);
assertThat(componentCreationParameters.isManaged()).isFalse();
assertThat(componentCreationParameters.newComponent().isPrivate()).isFalse();

verify(projectAlmSettingDao).insertOrUpdate(eq(dbSession), projectAlmSettingDtoCaptor.capture(), eq("devops-platform-config-1"), eq(PROJECT_NAME), eq(PROJECT_KEY));
ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);

assertThat(logTester.getLogs()).isEmpty();
}

@Test
public void createProjectAndBindToDevOpsPlatform_whenRepoFoundOnGitHubAndAutoProvisioningOn_successfullyCreatesProject() {
// given
DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME);
when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE);
when(gitHubSettings.isProvisioningEnabled()).thenReturn(true);

AlmSettingDto almSettingDto = mockAlmSettingDto(devOpsProjectDescriptor);
mockExistingGitHubRepository(almSettingDto);

ComponentCreationData componentCreationData = mockProjectCreation();
ProjectAlmSettingDao projectAlmSettingDao = mock();
when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao);

// when
ComponentCreationData actualComponentCreationData = gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto,
devOpsProjectDescriptor);

// then
assertThat(actualComponentCreationData).isEqualTo(componentCreationData);

ComponentCreationParameters componentCreationParameters = componentCreationParametersCaptor.getValue();
assertComponentCreationParametersContainsCorrectInformation(componentCreationParameters);
assertThat(componentCreationParameters.isManaged()).isTrue();
assertThat(componentCreationParameters.newComponent().isPrivate()).isTrue();

verify(projectAlmSettingDao).insertOrUpdate(eq(dbSession), projectAlmSettingDtoCaptor.capture(), eq("devops-platform-config-1"), eq(PROJECT_NAME), eq(PROJECT_KEY));
ProjectAlmSettingDto projectAlmSettingDto = projectAlmSettingDtoCaptor.getValue();
assertAlmSettingsDtoContainsCorrectInformation(almSettingDto, requireNonNull(componentCreationData.projectDto()), projectAlmSettingDto);

assertThat(logTester.getLogs()).isEmpty();
}

@Test
public void createProjectAndBindToDevOpsPlatform_whenWrongToken_throws() {
DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME);
AlmSettingDto almSettingDto = mockAlmSettingDto(devOpsProjectDescriptor);
mockExistingGitHubRepository(almSettingDto);

when(githubApplicationClient.createAppInstallationToken(any(), anyLong())).thenReturn(Optional.empty());

assertThatIllegalStateException().isThrownBy(
() -> gitHubDevOpsPlatformService.createProjectAndBindToDevOpsPlatform(dbSession, PROJECT_KEY, almSettingDto, devOpsProjectDescriptor))
.withMessage("Error while generating token for GitHub Api Url https://api.toto.com (installation id: 534534534543)");
}

private void mockExistingGitHubRepository(AlmSettingDto almSettingDto) {
GithubAppConfiguration githubAppConfiguration = mockGitHubAppConfiguration(almSettingDto);
when(githubApplicationClient.getInstallationId(githubAppConfiguration, GITHUB_REPO_FULL_NAME)).thenReturn(Optional.of(APP_INSTALLATION_ID));
AppInstallationToken appInstallationToken = mockAppInstallationToken(githubAppConfiguration, APP_INSTALLATION_ID);
mockGitHubRepository(appInstallationToken);
}

private GithubAppConfiguration mockGitHubAppConfiguration(AlmSettingDto almSettingDto) {
GithubAppConfiguration githubAppConfiguration = mock();
when(githubGlobalSettingsValidator.validate(almSettingDto)).thenReturn(githubAppConfiguration);
when(githubAppConfiguration.getApiEndpoint()).thenReturn(GITHUB_API_URL);
return githubAppConfiguration;
}

private void mockGitHubRepository(AppInstallationToken appInstallationToken) {
GithubApplicationClient.Repository repository = mock();
when(repository.getDefaultBranch()).thenReturn(MAIN_BRANCH_NAME);
when(repository.getName()).thenReturn(PROJECT_NAME);
when(repository.getFullName()).thenReturn(GITHUB_REPO_FULL_NAME);
when(githubApplicationClient.getRepository(GITHUB_API_URL, appInstallationToken, PROJECT_NAME)).thenReturn(Optional.of(repository));
when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn("generated_" + PROJECT_KEY);
}

private AppInstallationToken mockAppInstallationToken(GithubAppConfiguration githubAppConfiguration, long appInstallationId) {
AppInstallationToken appInstallationToken = mock();
when(githubApplicationClient.createAppInstallationToken(githubAppConfiguration, appInstallationId)).thenReturn(Optional.of(appInstallationToken));
return appInstallationToken;
}

private static AlmSettingDto mockAlmSettingDto(DevOpsProjectDescriptor devOpsProjectDescriptor) {
AlmSettingDto almSettingDto = mock();
when(almSettingDto.getUuid()).thenReturn("almsetting-uuid-1");
when(almSettingDto.getKey()).thenReturn("devops-platform-config-1");
return almSettingDto;
}

private ComponentCreationData mockProjectCreation() {
ComponentCreationData componentCreationData = mock();
mockProjectDto(componentCreationData);
when(componentUpdater.createWithoutCommit(eq(dbSession), componentCreationParametersCaptor.capture())).thenReturn(componentCreationData);
return componentCreationData;
}

private static ProjectDto mockProjectDto(ComponentCreationData componentCreationData) {
ProjectDto projectDto = mock();
when(projectDto.getName()).thenReturn(PROJECT_NAME);
when(projectDto.getKey()).thenReturn(PROJECT_KEY);
when(projectDto.getUuid()).thenReturn("project-uuid-1");
when(componentCreationData.projectDto()).thenReturn(projectDto);
return projectDto;
}

private static void assertComponentCreationParametersContainsCorrectInformation(ComponentCreationParameters componentCreationParameters) {
assertThat(componentCreationParameters.creationMethod()).isEqualTo(CreationMethod.SCANNER_API_DEVOPS_AUTO_CONFIG);
assertThat(componentCreationParameters.mainBranchName()).isEqualTo(MAIN_BRANCH_NAME);
assertThat(componentCreationParameters.userLogin()).isEqualTo(USER_LOGIN);
assertThat(componentCreationParameters.userUuid()).isEqualTo(USER_UUID);

NewComponent newComponent = componentCreationParameters.newComponent();
assertThat(newComponent.isProject()).isTrue();
assertThat(newComponent.qualifier()).isEqualTo(Qualifiers.PROJECT);
assertThat(newComponent.key()).isEqualTo(PROJECT_KEY);
assertThat(newComponent.name()).isEqualTo(PROJECT_NAME);
}

private static void assertAlmSettingsDtoContainsCorrectInformation(AlmSettingDto almSettingDto, ProjectDto projectDto, ProjectAlmSettingDto projectAlmSettingDto) {
assertThat(projectAlmSettingDto.getAlmRepo()).isEqualTo(GITHUB_REPO_FULL_NAME);
assertThat(projectAlmSettingDto.getAlmSlug()).isNull();
assertThat(projectAlmSettingDto.getAlmSettingUuid()).isEqualTo(almSettingDto.getUuid());
assertThat(projectAlmSettingDto.getProjectUuid()).isEqualTo(projectDto.getUuid());
assertThat(projectAlmSettingDto.getMonorepo()).isFalse();
assertThat(projectAlmSettingDto.getSummaryCommentEnabled()).isTrue();
}
}

+ 19
- 3
server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/queue/BranchSupportTest.java Wyświetl plik

@@ -38,7 +38,9 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.core.ce.CeTaskCharacteristics.PULL_REQUEST;

@RunWith(DataProviderRunner.class)
public class BranchSupportTest {
@@ -54,17 +56,31 @@ public class BranchSupportTest {

ComponentKey componentKey = underTestNoBranch.createComponentKey(projectKey, NO_CHARACTERISTICS);

assertThat(componentKey)
.isEqualTo(underTestWithBranch.createComponentKey(projectKey, NO_CHARACTERISTICS));
assertThat(componentKey).isEqualTo(underTestWithBranch.createComponentKey(projectKey, NO_CHARACTERISTICS));
assertThat(componentKey.getKey()).isEqualTo(projectKey);
assertThat(componentKey.getBranchName()).isEmpty();
assertThat(componentKey.getPullRequestKey()).isEmpty();
verifyNoInteractions(branchSupportDelegate);
}

@Test
public void createComponentKey_delegates_to_delegate_if_characteristics_is_not_empty() {
public void createComponentKey_whenCharacteristicsIsRandom_returnsComponentKey() {
String projectKey = randomAlphanumeric(12);
Map<String, String> nonEmptyMap = newRandomNonEmptyMap();

ComponentKey componentKey = underTestWithBranch.createComponentKey(projectKey, nonEmptyMap);

assertThat(componentKey).isEqualTo(underTestWithBranch.createComponentKey(projectKey, NO_CHARACTERISTICS));
assertThat(componentKey.getKey()).isEqualTo(projectKey);
assertThat(componentKey.getBranchName()).isEmpty();
assertThat(componentKey.getPullRequestKey()).isEmpty();
verifyNoInteractions(branchSupportDelegate);
}

@Test
public void createComponentKey_whenCharacteristicsIsBranchRelated_delegates() {
String projectKey = randomAlphanumeric(12);
Map<String, String> nonEmptyMap = Map.of(PULL_REQUEST, "PR-2");
ComponentKey expected = mock(ComponentKey.class);
when(branchSupportDelegate.createComponentKey(projectKey, nonEmptyMap)).thenReturn(expected);


+ 4
- 0
sonar-core/src/main/java/org/sonar/core/ce/package-info.java Wyświetl plik

@@ -0,0 +1,4 @@
@ParametersAreNonnullByDefault
package org.sonar.core.ce;

import javax.annotation.ParametersAreNonnullByDefault;

Ładowanie…
Anuluj
Zapisz