]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20699 Auto-bind project to GitHub DevOps config
authorAurelien Poscia <aurelien.poscia@sonarsource.com>
Wed, 11 Oct 2023 06:46:26 +0000 (08:46 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 20 Oct 2023 20:02:40 +0000 (20:02 +0000)
22 files changed:
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/AppInstallationToken.java [new file with mode: 0644]
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubBinding.java
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/AppInstallationTokenTest.java [new file with mode: 0644]
server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/project/CreationMethod.java
server/sonar-server-common/src/main/java/org/sonar/server/component/ComponentCreationData.java [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/component/package-info.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/BranchReportSubmitterIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/ce/queue/ReportSubmitterIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/BranchSupport.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/queue/ReportSubmitter.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentCreationData.java [deleted file]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/queue/BranchSupportTest.java
sonar-core/src/main/java/org/sonar/core/ce/package-info.java [new file with mode: 0644]

diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/AppInstallationToken.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/AppInstallationToken.java
new file mode 100644 (file)
index 0000000..22e1f01
--- /dev/null
@@ -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;
+  }
+}
index 7ecfb54acecaa25fe9f331b2e9e54470712ba3a1..9a7db826bb3e62111350abbf115c14729a455756 100644 (file)
@@ -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;
index 6b242deba7b217f579da0985308f05fdd09752c8..be6914fa8a56288aa69caf956ab4a72f90ba1e51 100644 (file)
@@ -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));
index a86dcb13c6b6e440176eaca9b7134d64560ce867..bf5cf60af901fc6849496c7ab754f3e83d6fea83 100644 (file)
@@ -460,4 +460,13 @@ public class GithubBinding {
       return name;
     }
   }
+
+  public static class GsonInstallationToken {
+    @SerializedName("token")
+    String token;
+
+    public String getToken() {
+      return token;
+    }
+  }
 }
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/AppInstallationTokenTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/AppInstallationTokenTest.java
new file mode 100644 (file)
index 0000000..0f4bd9a
--- /dev/null
@@ -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"));
+  }
+}
index 855f8f66e7c5bc19342615553fd8291c4c5f36aa..0429beebdc5fbdabf3f429a7eaed09010bd2922b 100644 (file)
@@ -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);
index 2af92f93288a8ff01be8ceb72482948080bbfec5..cc9d89b98b45280c0447f9a67d83768a3e895864 100644 (file)
@@ -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;
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/component/ComponentCreationData.java b/server/sonar-server-common/src/main/java/org/sonar/server/component/ComponentCreationData.java
new file mode 100644 (file)
index 0000000..9751629
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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.server.component;
+
+import javax.annotation.Nullable;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.portfolio.PortfolioDto;
+import org.sonar.db.project.ProjectDto;
+
+public record ComponentCreationData(ComponentDto mainBranchComponent, @Nullable PortfolioDto portfolioDto, @Nullable BranchDto mainBranchDto,
+                                    @Nullable ProjectDto projectDto) {
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/component/package-info.java b/server/sonar-server-common/src/main/java/org/sonar/server/component/package-info.java
new file mode 100644 (file)
index 0000000..f44cb0a
--- /dev/null
@@ -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;
index a066fdb639a2b491f83f667584fd6173d4110660..d4c0480940cb2c35886fb7823561ec752c377283 100644 (file)
@@ -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;
   }
 
index ae6b606f6272cb39d2bd62a8e02d4f46c895e41e..d820b52dcb393dd019b1222fe4774dbfa244c962 100644 (file)
@@ -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));
-  }
-
 }
index 24a08d3fd966e50aec6cbddbc487544fd0bec8c9..22507bb5a15d9eacdcfd03ea2ae3ecd608b34d57 100644 (file)
 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()));
   }
 
index 7660f86b8695ccae4f96face70e67d88f8c09101..a403d13ee50783f392fa86577eb77aefff961137 100644 (file)
  */
 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());
-  }
 }
index 514b583e7348aa6b227efc72335998411825b84a..dd764473d9f3fe721cb29d36e6aa38096caba83d 100644 (file)
@@ -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))
index e685e1bf9d372e463903b187da7abe695ac5c7f8..fff70b8add453ff354810fd99646bb3df06f6012 100644 (file)
@@ -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);
 }
index 2afec24eea7c9403342dc9b8a17a555f158b92c3..8ac5d5eb8b0f7ae444585d24a781c9cdd450421e 100644 (file)
@@ -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());
   }
 
 }
index e21682295b1113f04698f76c0569ea8ff346232b..a7269836624abbeb20249bc5478a17adafc85801 100644 (file)
@@ -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) {
index a66927ee14336928dcf3927970838432e9b07e9c..a34ce3c87ac4392c8f3e0dd2e11ca18efd6fcebe 100644 (file)
@@ -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();
 
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentCreationData.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentCreationData.java
deleted file mode 100644 (file)
index 9751629..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.server.component;
-
-import javax.annotation.Nullable;
-import org.sonar.db.component.BranchDto;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.db.portfolio.PortfolioDto;
-import org.sonar.db.project.ProjectDto;
-
-public record ComponentCreationData(ComponentDto mainBranchComponent, @Nullable PortfolioDto portfolioDto, @Nullable BranchDto mainBranchDto,
-                                    @Nullable ProjectDto projectDto) {
-}
index 2cf2a07f83f6ccb76b9f066c24b35233ed0c54fc..7d2a4be977b956aa33f1e2a4dc9f747d46919f93 100644 (file)
@@ -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();
+  }
 }
index 8f825538eb6fa0a4f854e52b995fc5fe67466b64..e853831765ae5955324922ebb80ddcb6d9791144 100644 (file)
@@ -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);
 
diff --git a/sonar-core/src/main/java/org/sonar/core/ce/package-info.java b/sonar-core/src/main/java/org/sonar/core/ce/package-info.java
new file mode 100644 (file)
index 0000000..90dc668
--- /dev/null
@@ -0,0 +1,4 @@
+@ParametersAreNonnullByDefault
+package org.sonar.core.ce;
+
+import javax.annotation.ParametersAreNonnullByDefault;